‘’‘’
标题对应《编程之美》题号
思路:包括解题思路和编程中的技巧
教训:编程过程中需要注意的地方以及存在的惯性错误
‘’‘’
2.2 关于阶乘的问题
1.给定一个整数N,问N的阶乘N!末尾有多少个0?
思路:首先思考N!=K10M ,其中k不能被10整除,则N!末尾就有M个0。
再根据质因数分解,N!=(2x)(3y)(5z),因为10=25,所以M只和x和z有关,因此M=min(x,z),很显然x是大于等于z的,因此M=z,因此只要求出z的值,就得到了N!末尾0的个数。
ret=0;
for(int i=1;i<=N;i++)
{
int j=i;
while(j % 5 == 0)
{
ret++;
j / =5;
}
}
2.求N!的二进制表示中最低位为1的位置。
思路:由于N!中含有质因数2的个数为 [N/2] +[N/4] + [N/8] + … (这是一个结论公式,证明略)
int lowestOne(int N)
{
int ret=0;
while(N)
{
N>>1;
ret +=N;
}
return ret;
}
2.4 1的数目
1.1~N中1出现的次数?
思路:同剑指No.43,不再赘述
2.满足条件f(N)=N的最大的N是多少?
思路:本质上来说,N是最大满足条件f(n)=n的一个上界,如果能估计出这个N,那么只要让n从N往0递减,每个数分别检查是否有f(n)=n,第一个满足条件的数就是我们要求的整数。
核心公式:当n增加10k时,f(n)至少增加 k*10k-1
把n按10进制展开,n=a * 10k+b * 10k-1+…
则有:f(n)=f(0+a * 10k+b * 10k-1+…)>a * k * 10k-1+b * (k-1) * 10k-2+…
又有:n=a * 10k+b * 10k-1+…k+(b+1) * 10k-1
由:a * k * 10k+b * (k-1)*10k-2>=a * 10k+b * 10k-1
得到:k>=10+(b+10) / (b+10 * a)
显然当k>11,或者说n的整数位数大于等于12时,f(n)>n恒成立,因此一个满足条件的上界为N=1011,然后从N到0递减,逐个检查f(n)=n即可。
2.5 寻找最大的k个数(包含了大数据处理问题)
思路:剑指里是最小的k个数问题,思路是借助了最大堆的数据结构以及一些操作,较为直观;
这里是最大的k个数问题,思路类似,采用的是最小堆的数据结构。
2.7最大公约数问题
思路:
1.辗转相除法:f(x,y) = f(y, x%y) ,(x>=y>0)
int gcd(int x, int y)
{
return (!y)?x:gcd(y,x%y);
}
2.将取模运算转化为减法运算
基于的是:如果一个数能够同时整除x和y,那么它也必定可以整除x-y和y,即f(x,y)=f(x-y,y)。
BigInt gcd(int x, int y)
{
if(x
2.9斐波那契数列
思路:
1.根据递推公式进行计算
2.利用数组存储已计算过的项,以空间换时间
3.求解通项公式
4.分治策略:略
2.10 寻找数组中的最大值和最小值
思路:
1.顺序遍历,需要2N次的比较才能得到结果
2.把相邻两个数分为一组,较大的放在偶数位,较小的放在奇数位,这样数组就分为了两组,而后在偶数位组里找最大值,在奇数位里找最小值,这样只要1.5N次的比较就能得到结果
3.解法2破坏了原数组的顺序,这里的方法是同样分两组,将其中较小的数与当前的min值比较,如果小于min值则更新min。同理将其中较大的值与当前的max值比较,如果该值大于max值,则更新max,如此直至遍历完整个数组。
这种方法的比较次数也为1.5N次,但是相较于解法2,不会破坏数组原来的顺序。
4.当然我们可以采用分治思想来处理,但是总的比较次数并没有减少
2.11 寻找最近点对
略
2.13 子数组的最大乘积
思路:
就是定义两个数组:s[i]表示数组前i个元素的乘积,t[i]表示数组后(N-i)个元素的乘积,设p[i]为数组除第i个元素外,其他N-1个元素的乘积,即:p[i]=s[i]*t[i]。
遍历两次数组即可得到数组s[]和t[],进而可以在线性时间内得到p[],这样就很容易得到p[]的最大值,由此时间复杂度为O(N)。
2.14 求数组的子数组之和的最大值
思路:
同剑指No.42,不再赘述
2.15 子数组之和的最大值(二维)
思路:
假设这个二维数组的尺寸为N*M。
2.16 求数组中最大递增子序列
参考代码
附:无后效性:将各阶段按照一定次序排列好之后,对于某个给定阶段的状态来说,它之前各阶段的状态无法直接影响它未来的策略,而只能间接地通过当前状态来影响。换句话说,每个状态都是过去历史的一个完整总结。
思路1:由于该问题满足无后效性,所有可以使用动态规划来解决。
假设在目标数组array[]的前i个元素中,最长递增子序列的长度为LIS[i],那么:
LIS[i+1]=max{1,LIS[k]+1},array[i+1]>array[k],for any k<=i
按照上面的思路,时间复杂度为O(N2)
思路2:
2.17 数组循环移位
思路:同剑指No.58:左旋字符串
概括为“拆-本翻-整翻”
step1.根据给定的翻转个数,将字符串分为前后两个部分;
step2.先分别翻转前后部分;
step3.再翻转整个字符串,这样就得到了结果。
2.18 数组分割
思路1:排序后按奇数和偶数位置分为两组,然后交换两个数组中的一对数,目标是使得差值尽可能小。
这种思路可行性是有的,但是得到的结果并不是最优的。
思路2:动态规划解,时间复杂度为O(2N)
思路3:
2.19 区间重合判断
思路1:逐个区间进行比对,考虑到计算有哪些源区间数组被覆盖需要O(log2N)的时间复杂度,更新尚未覆盖的区间需要O(N)的时间复杂度,所以总的时间复杂度为O(N2),太高了无法接受。
思路2:
step1.先将目标区间数组按从小到大顺序进行排序(排序可以采用快排等排序方法),然后将这些区间数组进行合并,成为若干个不相交的区间;
step2.然后在排序的基础上,运用二分查找来判定源区间是否被合并后的这些互不相交的区间中的某一个包含。
这种思路的时间复杂度:
【1】排序需要O(Nlog2N);【2】合并需要O(N);【3】单次查找需要log2N;
所以总的时间复杂度为:O(Nlog2N+klog2N)
3.1 字符串移位包含的问题
思路1:暴力解,就是双层循环,每次生成新的移动后的字符串和目标字符串进行对比,时间复杂度较高;
思路2:如果s2可以由s1循环移位得到,那么s2一定在s1s1上,这样就可以将问题转换为考察s2是否在s1s1上,这样问题就转为了在一个字符串内找另一个字符串的问题。
(显然思路2是一种空间换时间的思路,适用于对时间复杂度要求较高的场合)
3.2 电话号码对应英语单词
思路1:考虑到电话号码形成的是一个树结构,因此可以使用多层循环的暴力解,来进行穷举遍历(当然略微改进的话,可以使用while来代替多层for);
思路2:采用递归的方法实现上面的穷举遍历;
3.3 计算字符串的相似度(Leetcode原题)
参考代码
思路:很明显这是一个动态规划问题。
声明一个数组arr[i][j]表示字符串word1[0…i-1]到word2[0…j-1]的距离。
在arr[i+1][j+1]和arr[i][j]之间有一个关系,比如word1结束字符为x,word2结束字符为y:
如果x=y,则arr[i+1][j+1]=arr[i][j];
如果x!=y,则我们可以从word1里删除一个元素,arr[i+1][j+1]=arr[i+1][j]+1;
我们也可以从word1插入一个元素,arr[i+1][j+1]=arr[i][j+1]+1;
我们也可以从word1替换一个元素,arr[i+1][j+1]=arr[i][j]+1;
3.4 从无头单链表中删除节点
思路:(同剑指No.18)狸猫换太子
用后一个节点中的数据替换要删除节点中的数据,然后删除后一个节点即可。
3.5 最短摘要的生成
参考代码
思路:就是用两个游标标记找到的摘要的第一个字符和最后一个字符位置,然后下一次把第一个游标位置后移一格,这样包含的序列中将减少第一个关键词,那么我们就把第二个游标的位置后移,直到再包含所有关键词,然后计算一下这一次的摘要长度(cur2-cur1+1),如果当前是最短的,则更新一下这个最短值,否则继续上述操作,直到遍历到文章的结尾。
3.6 编程判断两个链表是否相交
思路1:直接对比第一个链表中的每一个节点是否在第二个链表中。时间复杂度O(Length(h1)*Length(h2));
思路2:用哈希表进行查询,具体就是建立第一个链表的哈希表,然后针对第二个链表的每个节点的地址查询哈希表,如果有出现,则说明有公共节点;
思路3:把链表2首尾相连,这样就形成一个带环链表,然后只需要从第二个链表头结点开始遍历,看是否会回到起始点就能判断出来是否有环结构;
思路4:核心思想:如果两个链表相较于某一点,那么在这个节点之后的所有节点都是两个链表所共有的,因此最后一个节点是共有的,因此我们只要遍历完两个链表,然后比较两个链表的尾节点即可得出结果。
衍生问题:两个链表相交的第一个节点(即剑指No.52的问题)
3.7 队列中最大值操作问题
思路:从两个栈实现队列的角度,这个问题同剑指No.9,
而获得队列最大值方面,可以采用一个最大值序列来保证访问时间复杂度为O(1),相当于用空间换取时间。
(这一问的最大价值就是:反映出我们可以使用不同的底层结构来实现队列这个抽象的容器,并且可以用空间换时间的方法来降低时间复杂度)
3.8 求二叉树中节点的最大距离
参考代码
思路:计算一个二叉树的最大距离有两个情况:
情况A: 路径经过左子树的最深节点,通过根节点,再到右子树的最深节点。
情况B: 路径不穿过根节点,而是左子树或右子树的最大距离路径,取其大者。
对于情况A来说,只需要知道左右子树的深度,然后加起来即可。
对于情况B来说,需要知道左子树的最远距离,右子树的最远距离。
只需要计算这两种情况的路径距离,并取其最大值,就是该二叉树的最大距离。
int HeightOfBinaryTree(BinaryTreeNode*pNode, int & nMaxDistance){
if (pNode == NULL)
return -1; //空节点的高度为-1
//递归
int nHeightOfLeftTree = HeightOfBinaryTree(pNode->m_pLeft, nMaxDistance) + 1; //左子树的的高度加1
int nHeightOfRightTree = HeightOfBinaryTree(pNode->m_pRight, nMaxDistance) + 1; //右子树的高度加1
int nDistance = nHeightOfLeftTree + nHeightOfRightTree; //距离等于左子树的高度加上右子树的高度+2
nMaxDistance = nMaxDistance > nDistance ? nMaxDistance : nDistance; //得到距离的最大值
return nHeightOfLeftTree > nHeightOfRightTree ? nHeightOfLeftTree : nHeightOfRightTree;
}
3.10 分层遍历二叉树
思路:同剑指No.32,
当然,编程之美中也提供了另一种思路,就是只用一个数组作为存放结果的数据结构,用两个游标来标记移动。
具体就是:用一个游标cur记录当前访问的节点,另一个游标last指示当前层次的最后一个节点的下一个位置,以cur==last作为当前层次访问结束的条件,在访问某一层的同时,将该层的所有节点的子节点压入数组,在访问完某一层之后,检查是否还有新的层次可以访问,直到访问完所有的层次。
3.11 程序改错
注意考虑全面,包括边界、溢出等极端情况。