《剑指offer》简略总结

来源:互联网 发布:windows光盘映像刻录机 编辑:程序博客网 时间:2024/05/21 17:26

剑指offer:

题目(页码以书为准而不是pdf):

  1. (P24)实现定义类的赋值运算符函数。要点是

    • 返回值类型声明为引用,返回实例自身;
    • 传入参数声明为const常量引用;
    • 分配新内存,释放旧内存;
    • 判断传入参数实例是否自身;
    • (高级)考虑异常安全,声明临时变量并交换内容,使得异常时不会破坏原实例。
  2. (P31)Singleton模式。要点是

    • 私有化构造函数;
    • 考虑多线程,加锁;
    • 两次判断是否创建,避免多次加锁。
  3. (P38)二维数组中的数字查找。要点是

    • 从矩阵的右上角来比较查找,这样就可以一次“解决”一行或是一列。
  4. (P44)将字符串中的空格替换成“%20”。要点:

    • 首先知道由来,空格ASCII码为32,十六进制为0x20;
    • 字符串会变长,所以如果从前到后替换,就会引起大量字符移动,应该预估位置,从后往前替换。
  5. (P51)从尾到头打印链表。要点是

    • 注意问清能不能破坏、改变原链表结构;
    • 注意处理链表为空的情况;
    • 不能改变链表的话,可以用栈或是递归调用来解决。
  6. (P55)给出二叉树的前序遍历序列和中序遍历序列,重建二叉树。要点是

    • 递归思想,每次将问题分解成三个:根节点处理,左子树处理,右子树处理,然后递归,注意递归的时候对边界条件比如NULL的小心处理;
    • 从前序获得根节点,然后在中序中查找该根节点,便可将中序分为左右。
  7. (P59)用两个栈实现队列。要点是

    • 为了思路清晰,可以模拟几次插入和删除;
    • 关键思路是,一个栈负责插入,另一个栈负责删除,当删除栈空的时候,就将插入栈倒数据过来。
  8. (P66)查找旋转数组的最小数字。要点是:

    • 借助二分查找思路,但是增加中间的判断条件,必须与head和tail都进行比较,缩小搜索范围;
    • 注意对旋转数组的特殊情况--无旋转,进行兼容处理;
    • 注意到如果head,tail,middle三者都相等时,无法缩小范围,此时只能用顺序查找。
    • 还可以扩展到查找任意数字而不仅仅是最小,这时候需要考虑的就多了点,就是考虑middle和head,tail形成的范围,而target在哪个范围。
  9. (P73)斐波那契数列。要点是:

    • 递归可以实现,但是效率低;所以最好用从底向上循环求数。更好的算法是数学技巧。
    • 上楼梯问题内在就是斐波那契数列。
  10. (P78)二进制中1的个数。要点是:

    • 直接向右移动原数字,这种情况在数字是负数是会死循环,所以应该移动flag,由0x0001变为0x0010变为0x0100...
    • 更优算法:一个整数减去1,再和原数做与运算,最终会把该整数最右边的一个1变为0,所以可以循环这样做,直到数字全变为0,这样就优化成有多少个1就运算多少次。
    • 更优算法可以用在判断2的整数次方。
  11. (P90)数值的整数次方。要点是:

    • 要考虑指数等于0或负数情况,再加上考虑底数为0的合法与非法情况,注意为0的浮点数特殊比较方法;
    • 优化:通过底数两倍指数地乘方,两倍指数计算时可以用>>右移来完成。
  12. (P94)打印1到最大的n位数。要点是:

    • 因为会有大数,所以应该用字符串来表示;需要实现大数加法,通过控制最后一位的叠加,然后观察进位来实现,最后返回值还要是能表示是否可以继续加;
    • 考虑打印问题,需要使最左边的0不打印出来;
    • 更优化一点:直接按数字全排列n大小的数组,循环处理当位,递归处理剩余位,然后排列输出。
  13. (P99)O(1)时间删除链表结点。要点是:

    • 将下一个结点内容复制到当前结点内容,然后删掉下一结点即可;
    • 注意要处理删除链尾、链头等边界情况。
  14. (P102)调整数组顺序使奇数位于偶数前面。要点是:

    • 首尾两个指针,向中间移动,分别寻找遇到的第一个偶/奇数,然后交换;
    • 可以抽象奇偶特性,换为大小或是求余等等。
  15. (P107)求链表中倒数第k个结点。要点是:

    • 用双指针达到目的,但是程序要注意边界情况:链表为空,链表长度小于k,输入的k为0为负等等。
  16. (P112)反转链表。要点是:

    • 注意边界情况,链表为空或只有一个结点的最后情况,链表断裂前的连接信息保存。
  17. (P114)合并两个排序的链表。要点是:

    • 同题16一样,注意两个链表为空、单节点、断裂信息等等。
  18. (P117)树的子结构。要点是:

    • 分成两步骤来特殊处理,步骤一是先找出子树根节点在母树的哪里;步骤二是从该子树根节点遍历下去是否都相等,有不等就要回到步骤一寻找另一个根节点;
    • 注意各种NULL的判断。
  19. (P125)二叉树镜像。要点是:

    • 递归地对每一个结点的左右子树都进行交换。
  20. (P127)顺时针打印矩阵。要点是:

    • 用暴力转向完成也是可以的;
    • 如果想思路清晰点,就画图找规律,分成两步骤,一是完成判断何时停止一圈,二是完成一圈的打印;但其实这样找规律有点不容易。
  21. (P132)包含min函数的栈;详细:实现一个栈,能够push,pop,min,而且复杂度都为O(1)。要点是:

    • 注意由于push,pop都需要调整min,所以干脆就整个辅助栈,把每一个数push或pop时产生的新栈状态都存下一个min,那么就可以在pop,push反操作时同步进行,得出min。
  22. (P134)知道栈的压入序列,问给定序列是否为合法弹出序列。要点是:

    • 以弹出序列为中心进行循环,如果下一个弹出的数字刚好是栈顶数字,那么久直接弹出;如果不在栈顶,就压栈;
    • 如果所有数字都压入栈仍没有找到下一个要弹出数字,那么久弹出序列非法。
  23. (P137)从上往下打印二叉树。 就是用队列,实现BFS。

  24. (P140)二叉搜索树的后续遍历序列,问题详细是,给定一个数组序列,判断该序列是否为某二叉搜索树的后续遍历结果。要点是:

    • 依然是一个二叉树递归的思想,先把根结点解决,然后递归解决左、右情况。
    • 所以先去最后一个数作为根,比其小的归为左序列,大的右序列,然后分别对左右序列进行递归判断是否未二叉搜索子树,即可。
  25. (P143)二叉树中和为某一值的路径,问题详细是,从根结点到子结点求和等于输入值的便是目标路径。要点是:

    • 前序遍历,累加求和保存,并把当前结点入栈;判断如果“和”与目标值相等,并且为叶子结点,就输出路径;否则,往下递归操作左、右结点;
    • 谨记需要在该结点层面的访问函数结束时,把“和”减去根结点值,并把当前结点出栈;这样就能恢复仿佛没有访问该点的原样;
    • 在这里,由于输出路径时需要遍历其中的结点,所以用队列来达成栈的结构会更灵活。
  26. (P147)复杂链表的复制,问题详细是,复杂链表的结点中,有一个指针指向下一个结点,另有一个指针指向任意结点或者NULL,现在需要复制该链表。要点是:

    • 注意关键是那个任意指针,**很难在一开始建立拷贝链表的时候就确定其值,只能够后期再赋值处理,所以这才是关键复杂的一步;**所以有三个方法。声明原链表为P,拷贝链表为Q。 - 方法一:暴力地检索P中结点的任意指针,然后重头对P遍历查找该指针指向的结点,从而迁移地在Q中找到对应结点;对每一个结点都进行这样的暴力操作,所以最终时间复杂度为O(n^2);
    • 方法二:拷贝结点时,保存<N, N'>这样的哈希表结构,那么在赋值“任意指针”时,可以O(1)时间由P中结点映射到Q中结点,即可把总的时间复杂度降为O(n),空间复杂度则为O(n);
    • 方法三:不用辅助空间,直接在P中复制结点时,在原结点后接上拷贝结点,这样也建立了“对应”,Q中结点的任意指针的值,也会是相应P中结点的任意指针的next。
  27. 二叉搜索树与双向链表,问题详细是,将一个二叉搜索树转换成排序的双向链表,要求不能创建任何新的结点,只能调整树中结点指针的指向。要点是:

    • 由于要求链表是要求排序的,所以对于原来的树应该采取中序遍历处理;
    • 所以我们可以假定顺序,对左子树完成转换,然后对根转换,再对右子树转换;
    • 用一个指针记录已完成转换的双向链表链尾,那么就可以衔接到根了。
  28. (P154)打印字符串的全排列。要点是:

    • 拆分成子问题。可以把字符串分为两部分,第一部分是字符串的第一个字符,第二部分是第一个字符后的所有字符,那么就可以求第二部分的所有字符排列,形成一个子过程;然后将第一个字符与后面的各字符循环交换,并重复上一步骤,完成一个遍历总过程。
    • 注意要对字符串的递归后恢复原值,以免错误重复处理;
    • 问题变种不少,八皇后就是其中一个。
  29. (P163)数组中出现次数超过一半的数字。要点是:

    • 有两种算法复杂度为O(n)的解法;
    • 解法一:需要修改原数组。仿照快排过程,通过partition,找出在有序序列中会位于n/2的中位数数字;因为如果某数字在该数组中出现超过一半,那么在其有序序列中一定会会出现在n/2位置。所以可以通过这个方法来解决。算法看起来像是O(nlogn)的复杂度,但实际上应该是近似O(n)的。
    • 解法二:计数法,保存某数的计数,出现就加1,不是就减1,减到0就更新新一个数;
    • 注意点是,需要在遍历数组前判断数组是否合法;解法一和二找出候选答案后应该验证是否的确符合题目要求。
  30. (P167)最小的k个数。要点是:

    • 解法一:与题目29相似,这里可以理解为划分,然后找出升序序列的第k个数即可,那么放在左边的就是目标值。所以依然用partition来完成这个过程,算法复杂度近似O(n)。
    • 解法二:如果数据很多很多,或者不允许修改原数组,那么可以用一个最大堆(注意最大最小)来存储k个数,然后每次判断更新替换堆顶最大元素,那么最后就能够得到最小的k个数;由于在笔试面试的时候不好实现堆,可以用标准库提供的二叉搜索树来完成,最方便就是用底层是红黑树的set/multiset来完成编程。算法复杂度为O(nlogk)。
  31. (P171)求连续子数组的最大和。要点是:

    • 由于数组中有正有负,有可能有些子数组加起来做的负功;
    • 设置一个GreatestSum,一个是CurrentSum;CurrentSum的更新规则是,若CurrentSum<=0,就直接设置为当前数,否则才将其加上当前数;每次更新CurrentSum后都与GreatestSum比较,若前者大就更新后者。
    • 也可以动态规划理解,但是编码跟上面基本一致。
  32. (P174)从1到n整数中1出现的次数。比如1到12,那么有1,10,11,12,“1”出现了5次。要点是:

    • 可以循环然后暴力用数不断除以10来求每一个数的1的个数,复杂度为O(nlogn);
    • 更好地,通过数字找规律,递归解决。比如书中距离21345,分割成1~1345,1346~21345,关键是统计后者,后者计数为 “2” * (length-1) * pow(10, length-2)。然后递归前者。
  33. (P177)把数组排成最小的数。比如{3,32,321}最小数字是321323。要点是:

    • 直接思路是穷举所有排列,n!复杂度,所以我们需要别的思路;考虑给定m,n,拼接两个数字时,需要mn或nm中较小的一个;所以需要对mn,nm进行比较,将m,n都化为字符串来衔接和比较更为方便;
    • 比较的结果可以看做为对m,n比较,同时这种比较特性是可以传递的,也就是很容易推理,如果按上面规则compare(m,n)<0,另有数字k,compare(n,k)<0,那么compare(m,k)<0;
    • 同时这种比较关系就能够拿来当做排序的基本信息,用qsort(array,length,sizeof(element),compare)即可完成字符串按我们的规则排序,然后按序拼接即可。
    • 最后能够理解这种比较的三个特性:自反性,对称性,传递性。(证明过程也可利用一些递归、归纳法)
  34. (P182)丑数。问题详细是,只包含因子2,3,5的数成为丑数,求从小到大的1500个丑数,例如6,8是丑数,14不是;认为1是第一个丑数。要点是:

    • 暴力解决是,除2除3除5到不能除,如果还有其他因子就返回false;然后循环所有数字;
    • 空间换时间思路,不断保存前面的丑数,形成一个有序序列,重点在寻找下一个丑数;分别保存某个“2丑数”*2,某个“3丑数”*3,某个“5丑数”*5的会是候选的下一个丑数的信息,然后每次选下一个丑数时便对这三个数比较取最小,添加到序列中,然后更新“2、3、5丑数”候选位置。
  35. (P186)第一个只出现一次的字符,如“abaccdeff",则输出“b"。要点是:

    • 扫描两遍,用一个256(或者128也够了)大小的数组当做哈希表来存储,第一遍记录字符出现次数,第二遍则是判断该字符是否符合条件:只出现一次,是就完成任务break。
  36. (P189)统计数组中的逆序对。比如{7,5,6,4}一共存在5个逆序对。要点是:

    • 我们可以看出,暴力地去遍历统计便可以达到目的,但复杂度为O(n^2);
    • 用归并方法的过程可以完成统计;递归地,一个全数组V的逆序对,等于左右两个有序数组各自的逆序对之和加上合并该两数组时所统计的交叉逆序对;所以需要小心地完成归并排序的过程,并计算其中的逆序,注意原数组和归并后数组的一些关系、区别。
  37. (P193)求两个链表的第一个公共结点。要点是:

    • 解法一:通过两个辅助栈,把两条链表倒过来了,那么从尾部开始pop,pop到最后一个相同的结点,就是第一个公共结点了;
    • 解法二:先计算链表长度差,然后统一“起跑点”,那么相遇点就是第一个公共结点了。
  38. (P204)数字k在排序数组中出现的次数。要点是:

    • 可以通过暴力遍历求解,复杂度为O(n);
    • 更优选择是通过二分查找找出第一个出现的k的位置和最后一个出现的k的位置,相减加一即可;这里二分查找需要加强,在里面多一些判断条件和选择策略。
  39. (P207)求二叉树的深度 && 问题变型:判断某一二叉树是否是平衡二叉树。要点是:

    • 如果是普通二叉树,那么只需要递归地取左右子树中深度较大者加一即可;
    • 问题变型:可以通过求求左右子树的深度,然后比较是否平衡,若平衡,还需要往下判断左右子树分别各自是否平衡。
    • 上面的方法会造成多次重复地计算同一个结点的深度,优化一下便是通过后序遍历的方式,将求深度的步骤嵌入到递归遍历里面,只要用变量保存深度返回到高一级函数步骤中即可进行比较判断。这里较重要的一点便是处理好保存深度返回到高一级的递归中。
  40. (P211)数组中只出现一次的数字。问题详细是:一个整形数组里除了两个数字a,b之外,其他数字都出现了两次,求这两个数字,要求时间复杂度O(n),空间复杂度O(1)。要点是:

    • 问题弱化成只有一个数字之外,其他都出现了两次,那么将数组全部异或,得出的数字就是答案;
    • 解法一:所以参考弱化问题,可以先全部异或,得出一个非零数X,X是a,b的异或结果,所以我们可以求X中的一个“1”的位置,然后以该位置为筛选,为“1”的分为一组异或,其他的为一组异或,得出的数字就是答案a和b了。
    • 解法二:可以把数组二分,然后分别异或,如果得出两个非0,则为答案;如果一个为0,另一个不为0,说明a,b都集中在后一个数组里,那么递归地(或说是循环)对该子数组求异或,最差也会在到达仅剩两个数的时候得出答案。
    • 解法一和二复杂度均符合要求,解法二甚至还要效率高一点。
  41. (P214)和为s的两个数字VS和为s的连续正数序列。要点是:

    • 题目一:一个递增序列,求其中相加和为s的两个数,有多对就输出一对。题目可以暴力解决,但复杂度为O(n^2);也可以通过二分查找来优化一下达到O(nlogn);**其实我们可以在每一次循环时,想法子利用上一次匹配失败的结果提供的信息。**也就是说,如果上一次匹配的结果比s小,那么就将较小数增大;若比s大,就将较大数减小;两个匹配的起始点分别是数组的两端(最小数和最大数),一直匹配到最终重合。
    • 题目二:输入一个整数s,打印出所有和为s的连续正数序列(至少包含两个数)。比如15 = 1+2+3+4+5 = 4+5+6 = 7+8。依然是尽量利用上一次的失败信息。由于这次需要保存的是连续序列两端,为了不遗漏情况以及数字之和能够简单控制地增长及减少(也就是面临大于s或是小于s时选择是唯一的),所以可以从数组最小的两个数字设置small,big开始匹配,如果序列和小于s,就增长big;如果大于s,就缩小small。一直让small匹配到(1+s)/2的为止。
    • 这两个题目的解法是相似的,只是标记的指针不一样,但都是为了让面临选择时只有唯一选择这个目的。解法过程是很简单的,但是需要证明这个过程是正确的这就不容易了。证明思路都可以用反证法,就是假设会忽略掉正确解答的情况,会出现矛盾;这样反过来说明解法不会遗漏正解。
  42. (P218)翻转单词顺序VS左旋转字符串。要点是:

    • 有两个问题版本:一是把一个句子中的单词顺序调换,比如"I am a student."变成"student. a am I";而是把一个字符串进行旋转,比如"abcdefg"左旋转2位结果是"cdefgab"。
    • 两个问题都是做一个各自(各单词、各字段)本地反转,然后再全局反转,便能够得到结果。
  43. (P223)n个骰子的点数。问题详细是:把n个骰子仍在地上,所有骰子朝上数字的和为s。输入n,打印出s的所有可能的值出现的概率。要点是:

    • 总的思路是,统计每个可能出现s的次数,然后将该次数除以总数(6^n),就可以得出概率;所以求各个s出现次数为重点。
    • 解法一是暴力地递归遍历,也就是分成1,n-1两份骰子,计算其和,然后记录加一;
    • 解法二是利用上“上次”信息,基于循环,对每一个骰子进行相加;下一循环中,加上一个骰子,那么和为M的次数应该等于上一次出现M-1,M-2,...M-6的情况相加,所以就可以快速地计算。
  44. (P226)扑克牌的顺子。问题详细是:从扑克牌中随机抽出5张牌,判断是不是一个顺子,即是否连续的。2~10是数字本身,A为1,J、Q、K为11、12、13。而大小王则是可以看做万能牌,可为任意数字。我们可以把大小王当做0.要点是:

    • 思路很直接,就是先对五个数字进行排序,然后统计其中0的个数,也就是可以拿来作为填补材料的个数;然后统计其他数字之间的空缺,这个可以通过相邻两个数字作差减一求得;然后比较填补材料和空缺的大小即可。
  45. (P228)圆圈中最后剩下的数字。问题详细是:0,1,...,n-1这n个数字排成一个圆圈,从数字0开始每次从这个圆圈中删去第m个数字。求出这个圆圈里剩下的最后一个数字。要点是:

    • 直接的思维是,用STL中的的List建立一个环形链表(当遇到end()时便变为begin()),然后按照题目过程不断地往下数m步删除数字,不过这样的时间复杂度为O(mn),空间复杂度为O(n)。
    • 另有一个巧妙的思路,得出的结果很简单,但是推理过程很复杂。推理的时候,是把一个摘掉k的数列,(0,1,2,...k-1,k+1,...,n-1)看成(k+1,k+2,...n-1,0,1,...k-1)的连续数列,然后映射成(0,1,2,...n-2)。这样就能够回到原来相似的问题;然后借助这个中间序列,(省略一些步骤)推理出n个数的序列最后一个数与n-1个数的序列最后一个数的关系是f(n,m)=[f(n-1,m)+m]%m 。而当n=0时,最后一个数便是0,所以倒推回去即可。写程序出来是很简单的。
    • 这个推理过程比较复杂,借助的是一个中间状态序列及其映射,还是记下结果答案和大致思想好了。
  46. (P233)求1+2+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句。要点是:

    • 按题目意思,在程序中不能够显式使用循环,同时也不能够通过switch、if来完成判断;那么我们基本可以这样定下:循环过程得交给编译器来完成,判断过程则使用"!"非符号来完成。(!n)如果n为非0,则返回0,否则返回1;
    • 解法一:通过构造函数求解。在构造函数里面完成递增和加和(注意变量需要声明为static),然后构造n个实例,也就是定义一个n数组即可;
    • 解法二:利用虚函数求解。在父类中定义递归终止函数,子类中定义递归过程函数(也可以调过来),然后通过"!!n" 来选取不同的指针来调用虚函数,实现多态调用,完成递归过程和递归终止。
    • 解法三:用函数指针完成,过程大致与解法二相同,只是无须多态,无须选取不同的对象指针,只要选择不同的函数指针即可。
    • 解法四:利用模板类型求解。在模板类型里面完成模板类型的递归推导。。。
  47. (P237)不用加减乘除做加法。要点是:

    • 将两数看成二进制,然后分三步走:一、不管进位,求出相加结果,这个可以通过异或求解;二、进位的处理,也就是让两数相与,然后左移一位;三、将上述两个结果“相加”。
    • 注意需要循环,判断若进位为0才能够结束循环。
    • 此解法应该对正、负、零都有效??
    • 迁移问题是,不用新的变量,交换两个变量的值;可以通过加减法、或是异或运算来解决。
  48. (P239)用C++设计一个不能被继承的类。要点是:

    • 解法一:把构造函数设为私有函数,这个需要配套地定义共有的静态函数来创建和释放类的实例;
    • 解法二:利用虚拟继承。这是一个很诡异的解法,标号如下:先声明一个类A,类A的构造函数和析构函数是私有的;然后类A中声明了一个友元类型B,而B类是虚继承自类A,这样B类构造函数中还是能够调用类A的私有构造函数(因为友元类型)。总结一句就是:B是A的虚继承子类,然后B又是A的友元类型。。A私有构造函数和析构函数。 那么B就是一个不能被继承的类。因为假设类C继承类B,但创建C类实例时,对于构造类A的部分(因为A类也相当于是C类的祖先类),由虚继承的构造函数机制,它不是通过调用B类构造函数来完成递归构造,而是直接调用类A的私有构造函数;又由于类C并不是类A的友元类型,这是非法的,因此编译错误。这样便能够强制不能继承类B,而且B类的行为看起来和一个普通类一样,不用通过静态函数类构造和析构实例。
  49. (P250)自己写一个atoi,字符串转换成整数。要点是:

    • 考虑NULL,“”和只有一个正负号的情况;
    • 用一个外部标志来判断是否正确转换;
    • 同时要注意看是否大于正整数最大值(0x7FFFFFF)或小于负整数最小值( (signed int)0x8000000 )。
  50. (P252)树中的两个结点p,q的最低公共祖先。要点是:

    • 如果是二叉搜索树,那么可以由根结点往下搜索,找到第一个结点,其值是处于p,q结点的值之间;
    • 如果不是二叉搜索树,甚至不是二叉树,那么如果树中结点有指向父结点的指针,就可以转化为求两个链表的第一个公共结点的问题;
    • 如果没有指向父结点的指针,可以通过暴力判断p,q是否存在于当前结点的子树中这样的做法,从根节点运行下来即可;
    • 优化下做法,就是从根节点向下寻找p,q结点,并且记录下两条路径,然后从根节点循路径走下来,遇到最后一个共同结点就是最低公共祖先了;
    • 还可以稍微优化一下,找出根节点到p结点的路径后,不断地从p结点回溯,然后搜索以p路径上各节点作为根节点的子树中是否包含q结点,这样在平均情况下能够减少很多搜索时间。

一些感悟要点:

  • 基础知识、解题思路很重要,一些边界测试
  • 代码无须太长
  • 自我介绍可短,最好多花口水在自己能发挥、能对应应聘的场地
  • 程序中错误处理的方法:通过返回值对应意义;设置一个全局变量值(这样可以解放返回值),抛出异常。
  • 防御性编程,提高代码鲁棒性,在函数入口验证各种特殊情况。
  • 考算法时,有把握地分析出算法复杂度会带来一些好印象。
  • 面试时不懂要挣扎一下,多沟通多学习,并且思路表达出来,即使错误也能够传递一种斗志出来。
  • 尽量思维敏捷地尽早提供一个思路回答问题,即使不是最优解;然后再优化改进,甚至推倒重来。

一些知识点:

1、C++标准不允许复制构造函数传值参数,否则会由于传值而引起复制构造函数内调用复制构造函数,形成递归调用而栈溢出,所以编译器编译时就要报错。

0 0
原创粉丝点击