var=array[++i][j];
elseif(var>searchKey&&j>0)
var=array[i][--j];
else
returnfalse;
}
}
举一反三
1、给定n×n的实数矩阵,每行和每列都是递增的,求这n^2个数的中位数。
2、我们已经知道杨氏矩阵的每行的元素从左到右单调递增,每列的元素从上到下也单调递增的矩阵。
那么,如果给定从1-n这n个数,我们可以构成多少个杨氏矩阵呢?
例如n=4的时候,我们可以构成1行4列的矩阵:
1234
2个2行2列的矩阵:
12
34
和
13
24
还有一个4行1列的矩阵
1
2
3
4
因此输出4。
4.3出现次数超过一半的数字
题目描述
题目:
数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。
分析与解法
一个数组中有很多数,现在我们要找出其中那个出现次数超过总数一半的数字,怎么找呢?
大凡当我们碰到某一个杂乱无序的东西时,我们人的内心本质期望是希望把它梳理成有序的。
所以,我们得分两种情况来讨论,无序和有序。
解法一
如果无序,那么我们是不是可以先把数组中所有这些数字先进行排序(至于排序方法可选取最常用的快速排序)。
排完序后,直接遍历,在遍历整个数组的同时统计每个数字的出现次数,然后把那个出现次数超过一半的数字直接输出,题目便解答完成了。
总的时间复杂度为O(nlogn+n)。
但如果是有序的数组呢,或者经过排序把无序的数组变成有序后的数组呢?
是否在排完序O(nlogn)后,还需要再遍历一次整个数组?
我们知道,既然是数组的话,那么我们可以根据数组索引支持直接定向到某一个数。
我们发现,一个数字在数组中的出现次数超过了一半,那么在已排好序的数组索引的N/2处(从零开始编号),就一定是这个数字。
自此,我们只需要对整个数组排完序之后,然后直接输出数组中的第N/2处的数字即可,这个数字即是整个数组中出现次数超过一半的数字,总的时间复杂度由于少了最后一次整个数组的遍历,缩小到O(n*logn)。
然时间复杂度并无本质性的改变,我们需要找到一种更为有效的思路或方法。
解法二
既要缩小总的时间复杂度,那么可以用查找时间复杂度为O
(1)的hash表,即以空间换时间。
哈希表的键值(Key)为数组中的数字,值(Value)为该数字对应的次数。
然后直接遍历整个hash表,找出每一个数字在对应的位置处出现的次数,输出那个出现次数超过一半的数字即可。
解法三
Hash表需要O(n)的空间开销,且要设计hash函数,还有没有更好的办法呢?
我们可以试着这么考虑,如果每次删除两个不同的数(不管是不是我们要查找的那个出现次数超过一半的数字),那么,在剩下的数中,我们要查找的数(出现次数超过一半)出现的次数仍然超过总数的一半。
通过不断重复这个过程,不断排除掉其它的数,最终找到那个出现次数超过一半的数字。
这个方法,免去了排序,也避免了空间O(n)的开销,总得说来,时间复杂度只有O(n),空间复杂度为O
(1),貌似不失为最佳方法。
举个简单的例子,如数组a[5]={0,1,2,1,1};
很显然,若我们要找出数组a中出现次数超过一半的数字,这个数字便是1,若根据上述思路4所述的方法来查找,我们应该怎么做呢?
通过一次性遍历整个数组,然后每次删除不相同的两个数字,过程如下简单表示:
01211=>211=>1
最终1即为所找。
但是数组如果是{5,5,5,5,1},还能运用上述思路么?
很明显不能,咱们得另寻良策。
解法四
更进一步,考虑到这个问题本身的特殊性,我们可以在遍历数组的时候保存两个值:
一个candidate,用来保存数组中遍历到的某个数字;一个nTimes,表示当前数字的出现次数,其中,nTimes初始化为1。
当我们遍历到数组中下一个数字的时候:
∙如果下一个数字与之前candidate保存的数字相同,则nTimes加1;
∙如果下一个数字与之前candidate保存的数字不同,则nTimes减1;
∙每当出现次数nTimes变为0后,用candidate保存下一个数字,并把nTimes重新设为1。
直到遍历完数组中的所有数字为止。
举个例子,假定数组为{0,1,2,1,1},按照上述思路执行的步骤如下:
∙1.开始时,candidate保存数字0,nTimes初始化为1;
∙2.然后遍历到数字1,与数字0不同,则nTimes减1变为0;
∙3.因为nTimes变为了0,故candidate保存下一个遍历到的数字2,且nTimes被重新设为1;
∙4.继续遍历到第4个数字1,与之前candidate保存的数字2不同,故nTimes减1变为0;
∙5.因nTimes再次被变为了0,故我们让candidate保存下一个遍历到的数字1,且nTimes被重新设为1。
最后返回的就是最后一次把nTimes设为1的数字1。
思路清楚了,完整的代码如下:
//a代表数组,length代表数组长度
intFindOneNumber(int*a,intlength)
{
intcandidate=a[0];
intnTimes=1;
for(inti=1;i{
if(nTimes==0)
{
candidate=a[i];
nTimes=1;
}
else
{
if(candidate==a[i])
nTimes++;
else
nTimes--;
}
}
returncandidate;
}
即针对数组{0,1,2,1,1},套用上述程序可得:
i=0,candidate=0,nTimes=1;
i=1,a[1]!
=candidate,nTimes--,=0;
i=2,candidate=2,nTimes=1;
i=3,a[3]!
=candidate,nTimes--,=0;
i=4,candidate=1,nTimes=1;
如果是0,1,2,1,1,1的话,那么i=5,a[5]==candidate,nTimes++,=2;......
举一反三
加强版水王:
找出出现次数刚好是一半的数字
分析:
我们知道,水王问题:
有N个数,其中有一个数出现超过一半,要求在线性时间求出这个数。
那么,我的问题是,加强版水王:
有N个数,其中有一个数刚好出现一半次数,要求在线性时间内求出这个数。
因为,很明显,如果是刚好出现一半的话,如此例:
0,1,2,1:
遍历到0时,candidate为0,times为1
遍历到1时,与candidate不同,times减为0
遍历到2时,times为0,则candidate更新为2,times加1
遍历到1时,与candidate不同,则times减为0;我们需要返回所保存candidate(数字2)的下一个数字,即数字1。
第五章动态规划
5.0本章导读
学习一个算法,可分为3个步骤:
首先了解算法本身解决什么问题,然后学习它的解决策略,最后了解某些相似算法之间的联系。
例如图算法中,
∙广搜是一层一层往外遍历,寻找最短路径,其策略是采取队列的方法。
∙最小生成树是最小代价连接所有点,其策略是贪心,比如Prim的策略是贪心+权重队列。
∙Dijkstra是寻找单源最短路径,其策略是贪心+非负权重队列。
∙Floyd是多结点对的最短路径,其策略是动态规划。
而贪心和动态规划是有联系的,贪心是“最优子结构+局部最优”,动态规划是“最优独立重叠子结构+全局最优”。
一句话理解动态规划,则是枚举所有状态,然后剪枝,寻找最优状态,同时将每一次求解子问题的结果保存在一张“表格”中,以后再遇到重叠的子问题,从表格中保存的状态中查找(俗称记忆化搜索)。
5.1最大连续乘积子串
题目描述
给一个浮点数序列,取最大乘积连续子串的值,例如-2.5,4,0,3,0.5,8,-1,则取出的最大乘积连续子串为3,0.5,8。
也就是说,上述数组中,30.58这3个数的乘积30.58=12是最大的,而且是连续的。
分析与解法
此最大乘积连续子串与最大乘积子序列不同,请勿混淆,前者子串要求连续,后者子序列不要求连续。
也就是说,最长公共子串(LongestCommonSubstring)和最长公共子序列(LongestCommonSubsequence,LCS)是:
∙子串(Substring)是串的一个连续的部分,
∙子序列(Subsequence)则是从不改变序列的顺序,而从序列中去掉任意的元素而获得的新序列;
更简略地说,前者(子串)的字符的位置必须连续,后者(子序列LCS)则不必。
比如字符串“acdfg”同“akdfc”的最长公共子串为“df”,而它们的最长公共子序列LCS是“adf”,LCS可以使用动态规划法解决。
解法一
或许,读者初看此题,可能立马会想到用最简单粗暴的方式:
两个for循环直接轮询。
doublemaxProductSubstring(double*a,intlength)
{
doublemaxResult=a[0];
for(inti=0;i{
doublex=1;
for(intj=i;j{
x*=a[j];
if(x>maxResult)
{
maxResult=x;
}
}
}
returnmaxResult;
}
但这种蛮力的方法的时间复杂度为O(n^2),能否想办法降低时间复杂度呢?
解法二
考虑到乘积子序列中有正有负也还可能有0,我们可以把问题简化成这样:
数组中找一个子序列,使得它的乘积最大;同时找一个子序列,使得它的乘积最小(负数的情况)。
因为虽然我们只要一个最大积,但由于负数的存在,我们同时找这两个乘积做起来反而方便。
也就是说,不但记录最大乘积,也要记录最小乘积。
假设数组为a[],直接利用动态规划来求解,考虑到可能存在负数的情况,我们用maxend来表示以a[i]结尾的最大连续子串的乘积值,用minend表示以a[i]结尾的最小的子串的乘积值,那么状态转移方程为:
maxend=max(max(maxend*a[i],minend*a[i]),a[i]);
minend=min(min(maxend*a[i],minend*a[i]),a[i]);
初始状态为maxend=minend=a[0]。
参考代码如下:
doubleMaxProductSubstring(double*a,intlength)
{
doublemaxEnd=a[0];
doubleminEnd=a[0];
doublemaxResult=a[0];
for(inti=1;i{
doubleend1=maxEnd*a[i],end2=minEnd*a[i];
maxEnd=max(max(end1,end2),a[i]);
minEnd=min(min(end1,end2),a[i]);
maxResult=max(maxResult,maxEnd);
}
returnmaxResult;
}
动态规划求解的方法一个for循环搞定,所以时间复杂度为O(n)。
举一反三
1、给定一个长度为N的整数数组,只允许用乘法,不能用除法,计算任意(N-1)个数的组合中乘积最大的一组,并写出算法的时间复杂度。
分析:
我们可以把所有可能的(N-1)个数的组合找出来,分别计算它们的乘积,并比较大小。
由于总共有N个(N-1)个数的组合,总的时间复杂度为O(N2),显然这不是最好的解法。
5.2字符串编辑距离
题目描述
给定一个源串和目标串,能够对源串进行如下操作:
1.在给定位置上插入一个字符
2.替换任意字符
3.删除任意字符
写一个程序,返回最小操作数,使得对源串进行这些操作后等于目标串,源串和目标串的长度都小于2000。
分析与解法
此题常见的思路是动态规划,假如令dp[i][j]表示源串S[0…i]和目标串T[0…j]的最短编辑距离,其边界:
dp[0][j]=j,dp[i][0]=i,那么我们可以得出状态转移方程:
∙dp[i][j]=min{
odp[i-1][j]+1,S[i]不在T[0…j]中
odp[i-1][j-1]+1/0,S[i]在T[j]
odp[i][j-1]+1,S[i]在T[0…j-1]中
}
接下来,咱们重点解释下上述3个式子的含义
∙关于dp[i-1][j]+1,s.t.s[i]不在T[0…j]中的说明
os[i]没有落在T[0…j]中,即s[i]在中间的某一次编辑操作被删除了。
因为删除操作没有前后相关性,不妨将其在第1次操作中删除。
除首次操作时删除外,后续编辑操作是将长度为i-1的字符串,编辑成长度为j的字符串:
即dp[i-1][j]。
o因此:
dp[i][j]=dp[i-1][j]+1。
∙关于dp[i-1][j-1]+0/1,s.t.s[i]在T[j]的说明
o若s[i]经过编辑,最终落在T[j]的位置。
o则要么s[i]==t[j],s[i]直接落在T[j]。
这种情况,编辑操作实际上是将长度为i-1的S’串,编辑成长度为j-1的T’串:
即dp[i-1][j-1];
o要么s[i]≠t[j],s[i]落在T[j]后,要将s[i]修改成T[j],即在上一种情况的基础上,增加一次修改操作:
即dp[i-1][j-1]+1。
∙关于dp[i][j-1]+1,s.t.s[i]在T[0…j-1]中的说明
o若s[i]落在了T[1…j-1]的某个位置,不妨认为是k,因为最小编辑步数的定义,那么,在k+1到j-1的字符,必然是通过插入新字符完成的。
因为共插入了(j-k)个字符,故编辑次数为(j-k)次。
而字符串S[1…i]经过编辑,得到了T[1…k],编辑次数为dp[i][k]。
故:
dp[i][j]=dp[i][k]+(j-k)。
o由于最后的(j-k)次是插入操作,可以讲(j-k)逐次规约到dp[i][k]中。
即:
dp[i][k]+(j-k)=dp[i][k+1]+(j-k-1)规约到插入操作为1次,得到dp[i][k]+(j-k)=dp[i][k+1]+(j-k-1)=dp[i][k+2]+(j-k-2)=…=dp[i][k+(j-k-1)]+(j-k)-(j-k-1)=dp[i][j-1]+1。
上述的解释清晰规范,但为啥这样做呢?
换一个角度,其实就是字符串对齐的思路。
例如把字符串“ALGORITHM”,变成“ALTRUISTIC”,那么把相关字符各自对齐后,如下图所示:
把图中上面的源串S[0…i]=“ALGORITHM”编辑成下面的目标串T[0…j]=“ALTRUISTIC”,我们枚举字符串S和T最后一个字符s[i]、t[j]对应四种情况:
(字符-空白)(空白-字符)(字符-字符)(空白-空白)。
由于其中的(空白-空白)是多余的编辑操作。
所以,事实上只存在以下3种情况:
∙下面的目标串空白,即S+字符X,T+空白,S变成T,意味着源串要删字符
odp[i-1,j]+1
∙上面的源串空白,S+空白,T+字符,S变成T,最后,在S的最后插入“字符”,意味着源串要添加字符
odp[i,j-1]+1
∙上面源串中的的字符跟下面目标串中的字符不一样,即S+字符X,T+字符Y,S变成T,意味着源串要修改字符
odp[i-1,j-1]+(s[i]==t[j]?
0:
1)
综上,可以写出简单的DP状态方程:
//dp[i,j]表示表示源串S[0…i]和目标串T[0…j]的最短编辑距离
dp[i,j]=min{dp[i-1,j]+1,dp[i,j-1]+1,dp[i-1,j-1]+(s[i]==t[j]?
0:
1)}
//分别表示:
删除1个,添加1个,替换1个(相同就不用替换)。
参考代码如下:
//dp[i][j]表示源串source[0-i)和目标串target[0-j)的编辑距离
intEditDistance(char*pSource,char*pTarget)
{
intsrcLength=strlen(pSource);
inttargetLength=strlen(pTarget);
inti,j;
//边界dp[i][0]=i,dp[0][j]=j
for(i=1;i<=srcLength;++i)
{
dp[i][0]=i;
}
for(j=1;j<=targetLength;++j)
{
dp[0][j]=j;
}
for(i=1;i<=srcLength;++i)
{
for(j=1;j<=targetLength;++j)
{
if(pSource[i