《剑指offer》解法总结

本文全面解析《剑指Offer》一书中的算法题目,涵盖数据结构、算法、编程技巧等核心内容,针对每道题提供详细解题思路与代码实现,强调实战应用与面试准备。

剑指offer这本书之前看过一次,今天又在牛客网上完成了最后一道题的编程。

第一次这本书,对于思路和编程都是难点;第二次在牛客网上刷题的时候,越来越感觉编程并非难点,难点在于没有解题思路,下不了手。如果知道了解题思路,大部分题目解出来只是时间问题。如果你也是这种情况,编程问题不大就是没有思路,那欢迎你来看我的文章。如果本身不会写代码,还是自己去做一遍的比较好。

在做完最后一题以后,返回去看前面的题目,却意外的发现很多题解法忘记了,还是不会做(虽然之前已经做出了结果),因此这次希望在一两天内把全部题目做一个解题思路上的总结,以防笔试面试时遇到做过的原题却无从下手。最终的目的是我能找到至少一个解法去求解这个问题,在追求效率的前提下,先得保证我可以做的出来。毕竟我的求职方向并不是C++研发。

这篇文章主要是给我自己看的,作为一个时不时的总结和提纲便于复习回忆。因此提前声明,每个人的状态不一样,所以可能对于各位没有太大的帮助。但当然也希望能帮助到大家。

 

目录

(一)数据结构

1.第一题赋值运算

2.第二题实现Singleton

3.第三题,数组中重复的数字:即找出共有n个数字的数组中出现了一次以上的数字。(所有数字都在0~n-1范围内)

4.第四题,二维数组中的查找:每一行从左到右递增,每一列从上到下递增。输入数组和整数,判断是否含有该整数:

5.第五题,替换空格:把字符串中每个空格替换成“%20”:

6.第六题,从尾到头打印链表:

7.第七题,重建二叉树:前序遍历和中序遍历,输出重建好的二叉树的根节点。

8.第八题,二叉树的下一个节点:给定二叉树和其中的一个节点,找出中序遍历序列的下一个节点。(有指向父节点的指针)

9.第九题,用两个栈实现队列:(每次复习的时候,需要用脑子想象一遍过程,而不是自认为自己会了眼高手低)

(二)算法与数据操作

10.第十题,斐波那契数列:

11.旋转数组的最小数字:把一个递增数组最开始的几个元素搬到最后,然后找出其中的最小元素。

12.十二题,矩阵中的路径:给定一个char*矩阵,行数、列数,和char*str,判断矩阵中有没有一条路径是str的。

13.机器人的运动范围:机器人从0,0位置出发,上下左右随便走,但是不能进入数位之和大于k的格子。

14.十四题,剪绳子:把长度为n的绳子剪成m段,都是整数,最后每段乘积最大是多少?(n给定,m自己决定)

15.四十二题,连续子数组的最大和:输入一个整型数组,有正有负。找出其中所有子数组的最大值。

16.第十五题,二进制中1的个数:输入一个整数,判断它的二进制中1的个数。

(三)代码的完整性

17.第十六题,数值的整数次方:就是求一个数值的整数次方。

18.第十七题,打印从1到最大的n位数:输入3,就打印从1~999。

19.第十八题,删除链表的节点:

20.第十九题,正则表达式匹配:实现函数匹配“.”和'“*”,点表示任意一个字符,星表示它前面的字符可以出现多次含0次。

21.第二十题,表示数值的字符串:用一个函数表示字符串是否表示数值:+100e-16…………

22.第二十一题。调整数组顺序使奇数位于偶数前:

(四)代码的鲁棒性

23.第二十二题。链表中倒数第k个节点。

24.第二十三题,链表中的环的入口节点:链表中有一个环,要找到环的入口节点。

25.第二十四题,反转链表:

26.第二十五题,合并两个排序的链表,使得合并以后仍然是递增排序的。

27.第二十六题,树的子结构:输入两科二叉树,判断B是不是A的子结构。

(五)画图让抽象问题形象化

28.第二十七题,二叉树的镜像:输入一棵二叉树,输出它的镜像。

29.第二十八题,对称的二叉树:判断一棵二叉树是不是关于最中间对称的。

30.第二十九题,顺时针打印矩阵:输入一个矩阵,按照从外向里顺时针的顺序依次打印每个数字。

(六)举例让抽象问题具体化

31.题目三十,包含min函数的栈,使得栈可以随时得到当前栈中的最小元素。

32.第三十一题,栈的压入、弹出序列:输入两个整数序列,第一个为压入序列,判断第二个是否为弹出序列。

33.第三十二题,从上到下打印二叉树。

34.第三十三题,二叉树的后序遍历序列:输入整数数组,判断是不是某二叉搜索树的后序遍历序列。

35.第三十四题,二叉树中和为某一值的路径:输入二叉树和一个整数,打印和为该值的路径。

(七)分解让复杂问题简单化

36.第三十五题,复杂链表的复制:一串链表,其中每个结点除了next指针,还有一个兄弟指针指向随机节点或nullptr。

37.第三十六题,二叉搜索树与双向链表:输入一个二叉搜索树,把它转换成一个排序的双向链表。

38.第三十七题,序列化二叉树:实现两个函数用来序列化和反序列化二叉树。

39.第三十八题,字符串的排列:输入字符串,打印出该字符串中字符的所有排列。

(八)时间效率

40.第三十九题,数组中出现次数超过一半的数字。

41.第四十题,最小的k个数:输入n个整数,找出其中的最小的k个数。

42.第四十一题,数据流中的中位数:

43.第四十三题,1~n中整数1出现的次数:给一个n,求从1到n中,所有十进制中1出现的次数。

44.第四十四题,数字序列中某一位的数字:01234567891011121314……从这个序列里读出第几位是几。

45.第四十五题, 把数组排成最小的数:输入一个正整数数组,把数组中所有数字拼接起来排成一个数,打印出最小的一个。

46.第四十六题,把数字翻译成字符串:给定一个数字,0翻译成a,1翻译成b,……11翻译成l,25翻译成z。问一个数字有多少种翻译方法。

47.第四十七题,礼物的最大值。一个m*n的棋盘里每一格都有一个数,从左上角到右下角行进,计算最多能拿多少的礼物(数的和最大)。

48.第四十八题,最长不含重复字符的子字符串:比如在arabcacfr中,最长不含重复字符的子字符串是acfr,长度为4。

(九)时间效率与空间效率的平衡

49.第四十九题,丑数:只包含因子2,3,5的数称为丑数。求从小到大顺序的第n个丑数。

50.第五十题,第一个只出现一次的字符。

51.第五十一题,统计数组中的逆序对。数组中如果有一个数字大于后面的数字,则组成一个逆序对。

52.第五十二题,两个链表的第一个公共节点。

(十)知识迁移能力

53.第五十三题 在排序数组中查找数字

54.第五十四题,二叉搜索树的第k大节点:给定一棵二叉搜索树,找出其中第k大的节点。

55.第五十五题,二叉树的深度。

56.第五十六题,数组中数字出现的次数

57.第五十七题,和为s的数字。

58.第五十八题,翻转字符串。

59.第五十九题,队列的最大值。

(十一)抽象建模能力

60.第六十题,n个骰子的点数:把n个骰子扔在地上,朝上的点数和为s,打印s的所有可能值出现的概率。

61.第六十一题,扑克牌中的顺子:从扑克牌中抽5张牌,判断是不是顺子。A为1,J为11……大小王可以互相顶任意数。

62.第六十二题,圆圈中最后剩下的数字:0~n-1排成一个圆圈,每次从圆圈中删除第m个数字,问最后剩下的是哪个。

63.第六十三题,股票的最大利润。把某时间的股票价格按照时间顺序存入数组,问买卖股票一次可获得的最大利润。

(十二)发散思维能力

64.第六十四题,求1+……到n:不用乘除法,for、while、ifelse、switchcase及条件判断语句。

65.第六十五题,不用加减乘除做加法。

66.第六十六题,构建乘积数组。给定A【0,1,……n-1】,构建B【0……n-1】,其中B【i】为A从头乘到尾,除了第i项。限制不能使用除法。

(十三)两个面试案例

67.第六十七题,把字符串转换成整数。给一个char*字符串表示的数字,把它转换成真正的数字。

68.第六十八题,树中两个节点的最低公共祖先。即输入两个树节点,找到他们的最低公共祖先。

(十四)其他内容


(一)数据结构

1.第一题赋值运算

2.第二题实现Singleton

两题是C++和C#的内容,与算法无太大关系。

3.第三题,数组中重复的数字:即找出共有n个数字的数组中出现了一次以上的数字。(所有数字都在0~n-1范围内)

该题目可通过map求解,即设置一个map,每次用find语句找键为当前数字data的元素,如果没找到(返回指向end),那么就把map[data]赋成1。找到了即为第一个重复了一次以上的数字。

当然书中还有巧妙解法,即如果是数字是m,就把数字摆在m位上。如果恰好m位上已经有了一个数字m了,那么这个数就是重复的数字。

4.第四题,二维数组中的查找:每一行从左到右递增,每一列从上到下递增。输入数组和整数,判断是否含有该整数:

方式很简单,从右上角开始,要找的数比当前数大,就往下走;要找的数比当前数小,就往左走。

5.第五题,替换空格:把字符串中每个空格替换成“%20”:

解法是先数一遍长度,得到空格的个数。然后新的长度等于原先长度+2*空格数。

然后两个索引,一个设置为新的长度,一个是旧的长度。相当于是字符串新的长度末尾从右至左走,如果左边指针不是空格,就复制到右边,如果是空格,右边就把替换的%20写上去。

此题求解时需要注意char*str,之后赋值的时候直接用中括号索引来赋值。

6.第六题,从尾到头打印链表:

这个题目非常简单,用栈解决。当然,装逼一点的话用递归,一直往里走,递归函数外面打印当前节点。

7.第七题,重建二叉树:前序遍历和中序遍历,输出重建好的二叉树的根节点。

此题牛客网的系统是用的vector来存放两个遍历的顺序。需要再写一个core函数用来递归,题目函数排除了空的情况以后,调用这个core函数。(下面我统统简称core式编程,这是我自己起的名字)

这个core函数的返回值就是根节点,因此可以传入四个形参,先序开头、先序结尾、中序开头、中序结尾。

先序开头的值new一个新节点,然后从中序开头开始依次往后找这个值,并且记录走了几步。这样就能把左子树的节点个数确定。假设走到了proot这里,走了m步,之后左子树等于core(先开头+1,先开头+m,中开头,proot),右子树等于core(先开头+m+1,先尾,proot+1,中尾),最后返回根节点。左右子树创建时需要确定各自的节点个数和大小再创建,因为子树可能不存在。

8.第八题,二叉树的下一个节点:给定二叉树和其中的一个节点,找出中序遍历序列的下一个节点。(有指向父节点的指针)

这题没啥意思,主要是具体情况具体分析。重点是二叉树的特性。

一个节点有右孩子,那么中序遍历一定是它的右孩子再一路向左下去。

如果没有右孩子,就需要看它自己是不是父节点的左孩子,是的话那下个节点就是它的父节点。如果自己是父节点的右孩子,就一路往上走,直到找到一个父节点自己是左孩子,那么输出就是这个父节点的爹。

9.第九题,用两个栈实现队列:(每次复习的时候,需要用脑子想象一遍过程,而不是自认为自己会了眼高手低)

题目说了,用两个栈实现;队列是先进先出的。

因此两个栈想象为一左一右,入栈的话统统都往左边的栈里入,出栈的时候先看右边的栈里有没有元素,右边没有,就把左边的弹出来,都压入右边,然后从右边pop;如果右边有,直接从右边pop。

扩展:用两个队列实现栈:

栈是先进后出,因此左右两个队,始终保持一个是空的。入栈的话就往有数的那个里面排队到最后,而出栈的话就是把这个队中的前面的数依次出队移到另一个空队当中,最后把最后的数弹出,这样这个队就变成空的了。


(二)算法与数据操作

10.第十题,斐波那契数列:

f(n)=\left\{\begin{array}{ll}{0} & {n=0} \\ {1} & {n=1} \\ {f(n-1)+f(n-2)} & {n>1}\end{array}\right.

本来这题用递归很好解决,但递归较慢,改进算法是用循环,从底向上,即:先算f2=f1+f0,f3=f2+f1……直到fn。

扩展:

(1)青蛙跳台阶:青蛙一次可以跳一级,一次也可以跳两级。n阶台阶多少种跳法?

第一次跳一级,那么还有f(n-1),第一次跳两级,那么还有f(n-2)。所以还是斐波那契数列。

但如果青蛙是一次可以跳1~n级,那么总跳法是2^(n-1)次,用数学归纳法可以证明。

(2)小矩形覆盖大矩形:一个2*8的大矩形,用8个2*1的小矩形填充,可以横着放,也可以竖着放。因此还是斐波那契数列。

11.旋转数组的最小数字:把一个递增数组最开始的几个元素搬到最后,然后找出其中的最小元素。

解法:先看头是不是比尾小,如果比尾小,证明没有旋转,就输出头元素;

再看头和尾是不是一样大,如果是再看和中间是不是一样,是的话就没办法只能用传统的顺序查找。

其他情况都是二分法。应该是前面是一个递增数组,后面也是递增的。一个头指针指向开头,一个尾指针指向结尾。中间的元素如果比前面第一个指针大,说明最小元素在后面的递增数组里,把头指针赋值为中间指针。如果中间元素比第尾指针小,就把尾指针赋值尾中间指针。结束的判断是当头指针-尾指针=1,则结束,输出尾指针。

整体的循环应该是保证while(number[头指针]>number[尾指针]),然后在里面判断上述的情况。

12.十二题,矩阵中的路径:给定一个char*矩阵,行数、列数,和char*str,判断矩阵中有没有一条路径是str的。

解法是core式编程,主函数先排除输入异常,然后创建visited数组记录有没有进入过,利用memset都赋值为0,然后设置变量pathlength记录走到了str的哪里。最后对矩阵中的每个点都调用core函数,如果找到了,返回true。

core函数中需要传入很多东西,包括矩阵,行数、列数,str,当前行,当前列,&pathlength引用,visited。

如果str[pathlength]到了结尾,就返回true。判断是否行/列在行/列数内,是否当前点没访问过,矩阵值和字符串对应值相不相等,都满足的话,就把pathlength++,记录visited数组,然后进一步或语句判断core左||上||右||下,判断有没有路径,如果没有,则pathlength回退,visited对应复原。

13.机器人的运动范围:机器人从0,0位置出发,上下左右随便走,但是不能进入数位之和大于k的格子。

也是core式编程。主函数排除异常,创建visited数组都赋值为0,然后count=core(阈值,行数,列数,当前行,当前列,visited)。

core函数中则判断能不能进入,如果能,就进一步count=1+core左+core右+core上+core下。

这种不需要回退了,因为是不合适的话直接就不计数了。

14.十四题,剪绳子:把长度为n的绳子剪成m段,都是整数,最后每段乘积最大是多少?(n给定,m自己决定)

这个牛客网上好像没有。

两种解法:(1)动态规划:f(n)=max ( f(i) * f(n-i) )。i是0~n之间的数。意思是两端的乘积最大值。为什么不是三段四段?因为这个两段里,其中某段可以再分成两段……所以这个一看可以用递归来求解,但是递归的问题是效率不好,因此从底向上。

如果长度是0或者1,那么最大乘积是0;如果长度是2,乘积最大为1;如果长度是3,乘积最大为2;

之后,控制变量m从4到n,依次循环:{结果=f[j]*f[m-j],j的取值应该是从1到m,但可以简化为1到m/2,因为超过了m/2,其实就是先后调了个顺序。找结果的最大值,作为最后确定的值。}

(2)贪心算法:

前三步一样,如果长度是0或者1,那么最大乘积是0;如果长度是2,乘积最大为1;如果长度是3,乘积最大为2;

然后n>=5的时候,尽可能减去长度为3的绳子。当剩下的一个绳子长4的时候,剪成2*2。

所以需要统计能剪成多少个3,如果最后剩个4,则把前面统计的减去1。最后算结果。

这种方式需要证明你的思路的正确性:当n>=5时,2(n-2)>n,3(n-3)>n,所以把它剪成长度为2或3的段比不剪好。此外当n>=5的时候,3(n-3)>=2(n-2),因此尽量剪成3的。

15.四十二题,连续子数组的最大和:输入一个整型数组,有正有负。找出其中所有子数组的最大值。

这个题目本来在后面才出现,但思路也是动态规划,因此我把它放在这里。

用f(i)代表以第i个数字结尾的连续子数组最大和,那么f(i-1)如果大于0,f(i)就是f(i-1)加上当前数。如果f(i-1)小于0,那么就丢弃掉,f(i)就是当前数。

编程的时候从底向上编程。

16.第十五题,二进制中1的个数:输入一个整数,判断它的二进制中1的个数。

常规解法是用一个1来与操作,然后把这个1一路左移。(之所以不是数字右移,因为如果这样的话对于负数就会出现死循环)

特殊解法是:自身和自身减去1之后的数进行与运算,就能把该整数二进制中最右边的一个1变成0。比如1100减去1,是1011,与运算结果是1000,把1100中的第二个1给消掉了。循环条件保证原数不是0,有多少次这种操作,就有几个1。


(三)代码的完整性

17.第十六题,数值的整数次方:就是求一个数值的整数次方。

题目难点在于要考虑指数是正数、负数和0。

特殊解法是,在纯粹的正整数次方求解时,可以分次方是奇数还是偶数。如果是偶数,相当于是两个一半指数的乘积。如果是奇数,再多乘一个底数。因此可以递归调用。

这种解法可以用来解斐波那契数列。斐波那契数列的一种特殊解法是:

\left(\begin{array}{cc}{f(n)} & {f(n-1)} \\ {f(n-1)} & {f(n-2)}\end{array}\right)=\left(\begin{array}{ll}{1} & {1} \\ {1} & {0}\end{array}\right)^{n-1}

后面的那个乘方就可以用这种方式。

18.第十七题,打印从1到最大的n位数:输入3,就打印从1~999。

这题貌似牛客网上也没有。直接循环打印,如果n很大,可能会溢出。

此题思路两种:一种模拟用字符串来模拟,从0开始打印,往上加数,感觉有点麻烦,懒得研究了。

第二种是弄全排列,也是用字符串来模拟,每一位都是从0到9排列一遍。递归操作,每一位从0~9循环,然后调用自身,index参数传入下一位。如果index到头了,等于length-1,就打印。

19.第十八题,删除链表的节点:

(1)O(1)时间内删除链表中的给定节点。

解法是把后面那个数覆盖到给定节点这里,然后删除掉后面那个节点。很巧妙。

(2)删除链表中重复的节点。

书上的思路不好。好的思路应该是先创建一个头结点,之后返回头结点的next。防止原先头结点也被删掉的情况。之后用三个指针控制,next指针往后找,指向第一个不同的数,然后重新连。

20.第十九题,正则表达式匹配:实现函数匹配“.”和'“*”,点表示任意一个字符,星表示它前面的字符可以出现多次含0次。

此题如果没有记住思路,则会颇有难度,应提高警惕。

解法是core式编程,主函数排除特殊情况,之后调用core函数。

core中递归,判断为:如果str和pattern都到结尾,则返回true;如果str到结尾了,pattern还没到,则返回false。

【1】如果pattern当前位置的下一个是*号,则再次判断:

(1)如果当前值和str值相等,或者是字符串结没结束 并且当前值是点,意思是pattern此时为"'.*",那么就return core(str+1,pattern+2)||core(str+1,pattern)||core(str,pattern+2)。依次含义为:匹配上了, 只出现了一次;匹配上了,出现了一次以上;匹配上了,但是只是表象,其实没出现,这里匹配的是pattern之后的。

(2)如果当前不满足1,那么就按没配对成功来看待,return core(str,pattern+2)。(这种情况如果字符串结束了,当前值是点,下一个是星,那么其实就是跳到了两个\0,最后返回true。

【2】下一个不是*,那么当前值是不是相等,或者字符串没结束且当前值是点,那么就正常后移,return core(str+1,pattern+1)。注意,如果字符串都已经结束了,pattern中还有个点,那是不行的,点必须有值,所以要保证字符串没结束。

最后return false,表示前面的都不满足。

21.第二十题,表示数值的字符串:用一个函数表示字符串是否表示数值:+100e-16…………

此题也有点难。

解法:此题需要三个函数,第一函数形参是char** str,是判断给定的无正负号的数字字符串中是否存在若干0~9的数字(同时指针后移),如果最后*str>before,则返回true;第二个函数形参也是char** str,如果存在“+”,“-”,就指针后移,然后调用返回第一函数。第三函数是主函数,形参是char *str。假设字符串是A.B e|E C格式,其中A可以有符号,也可没有值(如果有小数点的话)。B不能有符号。C可以有符号。

主函数中,排除了特殊情况,调用第一函数&str,返回一个bool变量numric;(此时判断了A存在不存在)

然后判断是否出现点,是的话就再后移一下,然后调用第一函数。返回numric=函数1(&str)||numric。因为有小数点的话,B合适了就行,A不存在也行,所以用或语句。注意这里千万不能写成numric=numric||函数1,因为一旦numric为真,之后的就不执行了,就会跳过B的判断过程。(踩过的坑要牢记)。

之后判断e和E是否存在,如果存在,后移一下,然后numric=numric&&函数2(&str),判断C那里对不对。

最后返回numric&&*str='\0',因为要确保前面的没问题了,并且字符串也走到最后了。

22.第二十一题。调整数组顺序使奇数位于偶数前:

解法可以是两个指针,前指针后指针,如果是前偶后奇就交换。

牛客网中提供的是vector,那么可以利用这个特性。如果是奇数,就记录下来,然后erase掉,把奇数push_back到后面去。否则向后移动。控制停止的变量就设置为是length,每次自减操作。


(四)代码的鲁棒性

23.第二十二题。链表中倒数第k个节点。

这个题目思路本身很简单,就是一个指针先走k-1步,(注意是k-1步不是k步)另一个跟上,前一个到结尾了,就返回后面那个。

难点在于防止程序崩溃。因此如果是空指针,就返回nullptr,如果是输入的k是0,也返回空指针。

更重要的是,如果链表总长度不够k个节点,也返回nullptr。

24.第二十三题,链表中的环的入口节点:链表中有一个环,要找到环的入口节点。

此题极易眼高手低,需要注意。

三步:先判断有没有环,再判断环中有几个,再找入口。

判断有没有环:两指针一快一慢,碰面说明有环。

判断环中有几个:碰面以后,一个停下来,一个继续走并且统计个数,再次碰面时停止计数。

找入口:假如环中有m个节点,两个指针从头出发,一个先走m步,之后一起走,两指针碰面则为入口节点。,(不好理解则死记即可)

25.第二十四题,反转链表:

此题听起来简单好像只是操作链表,但实际操作也非常容易眼高手低。

非递归做法:

四个指针,reversehead,pre=nullptr,node,next。

reversehead作为返回的指针。next是node的下一个,如果next是空,则把node赋值给它,返回。

否则:把node指向pre,pre变成了node,node变成了下一个next。

递归做法:

代码返回的是翻转后的头结点。思路是把当前指针的下一个节点的next指针指向自己,再断开当前指针的next节点。

如果输入的指针是空,或者输入的指针的下一个是空,就返回输入的这个指针。(头一个条件保证空节点输入也有返回,第二个条件确定正常输入时返回的最里面的那个节点作为头结点。

否则,设立newhead=函数(输入指针的下一个),然后输入指针的下一个的next指向输入指针自身。自身的next指向nullptr。从左往右一直递归进去,最后的会把最里面的那个节点返回出来。

26.第二十五题,合并两个排序的链表,使得合并以后仍然是递增排序的。

用递归求解,函数返回头结点。两个链表互相比,如果A当前的节点a比较小,就把它当做返回的节点mergehead,mergehead->next=函数(a->next,b)。

27.第二十六题,树的子结构:输入两科二叉树,判断B是不是A的子结构。

此题需要两个函数,第一个是找根相同的节点,第二个是判断两个根相同的节点的二叉树有没有从属关系。

第二个函数返回bool类型。第一个函数判断传入的两个根节点的值是否相等,相等就调用第二个。如果返回的是false,那么继续递归调用第一个函数(A->left,B);  之后再调用自身(A->right,B);

第二个函数则是如果B是空了,就返回true;之后再判断,假如B不是空,A空了,那么返回false;假如俩不相等,那么返回false。最后假如两个都不空,也相等,那么就返回函数2(A->right,B->right)&&函数2(A->left,B->left);


(五)画图让抽象问题形象化

28.第二十七题,二叉树的镜像:输入一棵二叉树,输出它的镜像。

此题比较简单,子节点左右互换即可。然后递归换左子树,右子树。注意判断非空条件再执行。

29.第二十八题,对称的二叉树:判断一棵二叉树是不是关于最中间对称的。

此题解法采用core式编程。core函数有两个形参,分别是要比较的两个节点。如果两节点都是空,返回true;之后如果两节点有一个是空,一个不是,则返回false;如果两节点值不相同,返回false;之后返回core(root1->left,root2->right)&&core(root1->right,root2->left); (两边和中间)

主函数将根节点的两个子节点传入core函数中。

30.第二十九题,顺时针打印矩阵:输入一个矩阵,按照从外向里顺时针的顺序依次打印每个数字。

此题看着简单,实际是整本书最难的几道题之一。需要想清楚了才能下手。

解法:(1)用循环来打印矩阵,每次打印矩阵中的一圈。重要先验原理:当前一圈,左上角坐标为(start,start),start*2的值是小于总的行或列数的。每转完一圈,start+1,如果start*2大于等于了总的行或列数,则已经打印完了,该停止了。

(2)打印一圈的函数:一进去以后,先要得到两个值,一个是打印这圈x的最大值endX=列-1-start(最右端是列-1,再减去当前start,因为左右对称),一个是打印这圈y的最大值endY=行-1-start。

然后就要开始打印了:从左到右一行是肯定要打印的;然后从上往下打印,需要满足的条件是start<endY;然后再从右往左打印,需要满足的条件是start<endX并且start<endY;然后再从下往上打印,需要满足start<endX并且start<endY-1。

(这个画图即可判断,把题目拆解成小问题,即容易很多)


(六)举例让抽象问题具体化

31.题目三十,包含min函数的栈,使得栈可以随时得到当前栈中的最小元素。

解法:此题解法较简单,使用两个栈,min栈和data栈。每次入栈,data栈正常入,min栈入新元素和原min栈顶二者之中的最小的元素。出栈两个正常弹出即可。要最小值直接取min栈顶最小值。

32.第三十一题,栈的压入、弹出序列:输入两个整数序列,第一个为压入序列,判断第二个是否为弹出序列。

解法:用栈来模拟,如果下一个弹出的数字刚好是栈顶元素,就直接弹出;如果下一个弹出的元素不在栈顶,就入栈,直到栈顶元素为下一个弹出的数字为止。如果所有的数字都压入了,都没找到,就返回false;除非是第二个序列空了,栈也是空的,才返回true。

33.第三十二题,从上到下打印二叉树。

(1) 最简单的层次遍历:用队解决,老题目,毫无意思。

(2)之字形打印:用两个栈,当前是奇数行,则孩子入栈时先入左孩子,再入右孩子;当前是偶数行,孩子入栈时先入右孩子再入左孩子。

(3)分行打印:在层次遍历的基础上额外再加俩变量,一个记录当前层有几个点,一个记录下一层有几个点。每入队一个,则下一个层计数加一。每出队一个,则当前层计数减一。当前层变成0,则打印换行,并且把下一层计数值赋给当前层。

34.第三十三题,二叉树的后序遍历序列:输入整数数组,判断是不是某二叉搜索树的后序遍历序列。

此题牛客网上有,但我却感觉很陌生。需要注意一下:

因为后序遍历,根节点是最后一个;因此在二叉搜索树的序列中,搜索左子树(都小于根节点),直到出现一个大于根节点的。然后继续往后搜索,看看是不是都是大于根节点,如果有不是的,返回false。

在经过了这个考验以后,left=递归(左半),right=递归(右半),最后return left&&right;

35.第三十四题,二叉树中和为某一值的路径:输入二叉树和一个整数,打印和为该值的路径。

解法为core式编程,采用递归的方法,路径用一个栈记录。每进入一层,和累加,节点进栈。如果和跟期望的值相等并且到了叶节点位置,就打印该路径。如果不是叶节点,就递归遍历左节点,右节点。返回父节点之前,把栈中的路径pop掉。当前的和变量如果是直接赋形参值的形式,就不需要再从中减掉当前的值,如果是引用的形式的话,应该得减掉当前值。


(七)分解让复杂问题简单化

36.第三十五题,复杂链表的复制:一串链表,其中每个结点除了next指针,还有一个兄弟指针指向随机节点或nullptr。

解法:这个问题可以分解成三个简单问题:

(1)每个节点复制一个自己到自己之后。

(2)每个节点的兄弟指针指向哪里,它之后的复制节点就指向对应位置的下一个。

(3)断开。

37.第三十六题,二叉搜索树与双向链表:输入一个二叉搜索树,把它转换成一个排序的双向链表。

此题思路上有些难度,需要仔细想想。

core式编程。主函数调用core函数,再一路左走找到头结点返回。

core函数中两个形参,一个是root,一个是lastNode的引用。lastNode表示左边已经转换好的节点的最后一个。最开始先赋值为nullptr。

采用中序遍历,前面core(root->left,lastNode),后面core(root->right,lastNode);

中间的操作为:当前的这个点左连lastNode,然后lastNode右连当前点,之后lastNode赋值为当前点。

之所以是中序遍历,是因为传入的root是根节点,core(root->left,lastNode)是连好左子树,并且lastNode返回左子树最后的一个节点。core(root->right,lastNode)是连好右子树,并且lastNode返回右子树最后的一个节点(可以给再往上的根节点用,当做它的左子树的最后一个点)。

38.第三十七题,序列化二叉树:实现两个函数用来序列化和反序列化二叉树。

序列化很简单,就是先序遍历,如果是空,就输出或者保存一个"$"符号。

反序列化稍微复杂一些,每次读一个数字或者"$"。如果是$,则返回nullptr; 如果是数字,就创建一个节点并且把数字赋进去,然后左右子树为nullptr。之后递归调用左子树=反序列化(字符流),右子树=反序列化(字符流),最后返回该节点。

这题有点不伦不类,可以看看牛客网。个人觉得难以出原题考察。

39.第三十八题,字符串的排列:输入字符串,打印出该字符串中字符的所有排列。

此题也有些难度。

解法:把字符串分成两部分,一部分是第一个字符,另一部分是第一个字符以后的所有字符。之后拿第一个字符和之后其他字符逐个交换。

采用core式编程,core函数两个形参str和begin,第一个是整体的字符串,第二个是传入的“除了第一个元素的其他字符串”。

大体思路为:大循环:i从begin第一个元素开始到最后不等于\0:

{swap(*begin,*i),然后递归(str,begin+1),最后swap(*begin,*i)再换回来。}

扩展:(1)如果不是求全排列,而是求全部组合:可以看成是求n个字符的长度为m(1<=m<=n)的组合。这个n个字符可以分成两部分,第一个字符和其他所有字符。如果组合中包含第一个字符,就从其他里面选m-1个;如果不包含,就从其他里选m个。最后就变成了两个子问题:求n-1个字符中长为m-1的组合,求n-1个字符中长为m的组合。(这题也没写过,回头自己动手写一下。暂时没有时间,先把思路记下来)

(2)8个数字放在8个顶点上,八皇后:都是求全排列,然后依次判断满不满足给定的关系。


(八)时间效率

40.第三十九题,数组中出现次数超过一半的数字。

这个题我之前在牛客网上的做法,是用map表示,记录出现次数,然后从头搜索一遍。(感觉还是这个方法直观一些)

剑指offer上有两个解法:

(1)用Partition函数求解。这个思路是:如果有个数字出现次数超过了数组长度的一半,那么排序以后出现在数值中间的数字就一定是出现次数超过数组长度一半的数字。所以就是找中位数:每次随机选一个数,然后比它小的放左边,比它大的放右边。如果这个数的下标刚好为n/2,说明是中位数。之后再搜索一下判断是不是出现了一半以上。

(2)遍历数组的时候保存两个值,一个是数,一个是次数。如果下一个数还是这个数,次数加1,否则次数减1。如果次数为0,则更新数,并把次数记为1。最后把最后保存下来的数检查一遍看看是不是出现了一半以上。

41.第四十题,最小的k个数:输入n个整数,找出其中的最小的k个数。

此题可以采用红黑树。在STL中,set和multiset都是基于红黑树实现的。

注意声明的时候要multiset<int,greater<int> > minset;用greater来从大到小排序。每次set中的值的个数如果小于k,就加入,如果大于k,之后每来一个数,如果比最大的小(minset.begin()),就把begin指向的给erase掉,然后插入该值。

42.第四十一题,数据流中的中位数:

这道题涉及到了新的STL知识点。

解法是左边一个最大堆,右边一个最小堆。每次要保证最大堆中的元素小于最小堆中的元素。因此如果是奇数,插入最小堆;如果是偶数,插入最大堆。但是如果该插入最小堆了,却发现这个数字比最大堆中最大的元素要小,所以把它插入最大堆,再把最大堆中的最大元素插入最小堆。

最后想得到中位数,如果总数是奇数,就从最小堆里拿;如果总数是偶数,就从最大堆的0和最小堆的0取平均值。

涉及到的新知识点是:

push_heap(vector1.begin(),vector2.end(),less<int>() )意思是对刚插入尾部的元素进行堆排序,然后begin()处成了最大值。

pop_heap是把第一个元素移到末尾,剩下的堆排序。不过此时那个元素还在vector中,需要手动pop_back掉。

43.第四十三题,1~n中整数1出现的次数:给一个n,求从1到n中,所有十进制中1出现的次数。

常规方法是对每个数都做除法和求余运算。,时间复杂度是O(nlogn)。

特殊方法:从数学规律入手,此题难度较大,值得注意:

采取core式编程。主函数中,采用sprintf(str,“%d”,n);把数字n存入到字符串str中去。之后调用core函数。

在core函数中,首先判断无效输入,之后用first记录第一位的数字,并且用strlen得到字符串的长度。如果第一位是0,字符串长度为1,则返回0;如果长度为1,但第一位大于0,则返回1;

之后用21345来分析,可以把21345分成两部分:1~1345,1346~24345。

1~1345递归调用core函数,形参为str+1,得到结果为A;

1346~21345又可以分成两部分:(按照第一位和后四位区分)

第一位是10000~19999中的1,出现了10000次,所以需要10^(5-1)次方。这种情况比较特殊,因为也可能第一位不是2。如果第一位是1以上的数字,那必然存在这个10^(5-1)。但是如果输入的数字是11345呢?第一位是1,那么就不能是10000~19999了,而是10000~11345。所以求解的方式就不再是10^(5-1),而是调用atoi(str+1)把字符串里的1345变成数字,然后再+1。因此这块需要分情况讨论,看第一位是不是1。如果第一位是1,则atoi(str+1)+1;如果第一位大于1,则是10^(length-1)。这块的结果记为B。

之后就是统计后四位中出现过的1了。之前只看第一位,因此不再细分。之后要看后四位了,因此再分成1346~11345,11346~21345两段,后四位可以从中固定一位是1,其他的自由搭配,排列组合原则,共有2*(length-1)*10^(length-2)个1,结果记为C。即first*(length-1)*10^(length-2)。

最后的结果是A+B+C。

44.第四十四题,数字序列中某一位的数字:01234567891011121314……从这个序列里读出第几位是几。

这道题牛客网的系统上没有出现。

 最直观的方式就是枚举,每枚举一个数字,就求出该数字是几位数,然后累加位数,最后得到对应的那一位。

更快的方法:此题用举例法先思考:

假如要找第1001位,前10位是0~9,因此还剩1001-10=991位。10~99有90个数字,每个数字有两位,共180位。因此还剩991-180=811位。100~999共有800个数,每个数3位,共有2700位。而811减不动2700,说明第811位是某个三位数中的一位。因为811=270*3+1,证明是从99开始,向后数270个数字,到了369这里走完了270*3,之后就应该从370这里开始计数了。因为是从0开始计数,因此最后输出3。(要找1001位其实是找从1开始的1002位)

编程思路如下:假设要找index位,刚开始先令digit为1,然后统计位数为digit的数字有多少个,假设为numbers个。

之后判断索引是否小于numbers*digit,如果是的话,就直接调用搜寻函数,否则更新digit(自增),并且用index减去numbers*digit。

搜寻函数为:当我们知道要找的那个数字位于某m位数之中,令number=m位的第一个数+index/digit。(即上面的3位的第一个数100+270=370)。然后判断还有几位,digits-index%digits(比如3-811%3得3-1=2),然后i从1开始,i<2,右移这个370(相当于只右移了1次变成37),最后%10得到7,返回。

个人感觉这个搜寻函数也可以不这么写,意思对了即可,比如从99开始往后数270个数,然后再往后走2位(而不是从100开始数270个数,往前走1位)

45.第四十五题, 把数组排成最小的数:输入一个正整数数组,把数组中所有数字拼接起来排成一个数,打印出最小的一个。

解法写一个比较函数,具体是把m和n拼起来,得到mn和nm,然后返回strcmp(mn,nm)。然后主函数里对所有数都调用这个规则排序,最后排好序后累加起来。

这个书中的做法是用的sprintf把数字读到字符串里,然后调用qsort函数对原先字符串排序。

void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))

这个是C编译器自带的快速排序函数,具体参数是:

  • base-- 指向要排序的数组的第一个元素的指针。

  • nitems-- 由 base 指向的数组中元素的个数。

  • size-- 数组中每个元素的大小,以字节为单位。

  • compar-- 用来比较两个元素的函数,即函数指针(回调函数)

最后排序好的字符串从头到尾拼起来。

当然还有别的方法,我比较喜欢的是牛客网上的解法:

主函数形参是vector,然后调用sort(begin,end,cmp),再从头到尾拼起来。

cmp函数自己写,注意写成静态的bool型,里面用to_string()函数,stra+=to_string(a)就可以把一个int a变量转成string类型的stra变量,最后return stra<strb;

这个的麻烦是要证明,两个数可以这么比,为什么好几个数都能这么比?这个回头再看,先说编程的问题。

46.第四十六题,把数字翻译成字符串:给定一个数字,0翻译成a,1翻译成b,……11翻译成l,25翻译成z。问一个数字有多少种翻译方法。

这道题牛客网上没有。思路是动态规划,先举例比较好:12258可以按成1+2258翻译也可以按12+258翻译,因此定义:

f(i)=f(i+1)+g(i,i+1)*f(i+2);其中g表示这两位数字在10~25之间,则为1否则为0。

因此用递归可以解决。但是照样,为了效率,自底向上解决。解法大致是从右至左,右边第一位就是1,然后i再回退,f(i)先等于f(i+1),然后再分析f(i+2)的情况。

47.第四十七题,礼物的最大值。一个m*n的棋盘里每一格都有一个数,从左上角到右下角行进,计算最多能拿多少的礼物(数的和最大)。

这道题牛客网上也没有。但思路也是动态规划

f(i,j)=max(f(i-1,j),f(i,j-1))+gift(i,j)

解法还是用循环:按行来更新,刚开始先更新第一行的f(i,j),然后第二行……为了节省空间,可以在更新第二行的时候覆盖掉第一行。

48.第四十八题,最长不含重复字符的子字符串:比如在arabcacfr中,最长不含重复字符的子字符串是acfr,长度为4。

这道题牛客网上也没有。思路动态规划

定义f(i)是不含重复字符的以i个字符结尾的子字符串最大长度(注意这里是子字符串以i结尾),如果第i个字符没出现过,那么f(i)=f(i-1)+1,。

如果第i个字符出现过:需要看第i个字符上次在哪出现的,距离这次的距离是多远。假设距离是d,如果d<=f(i-1),说明上次出现在了f(i-1)对应的最长子字符串中,那么f(i)=d。如果d大于f(i-1),那么f(i)还是等于f(i-1)+1。

写代码的时候还是不好用递归,用循环代替:

用一个26大小的辅助数组,数组中的每一位代表从a到z。从字符串开头开始,每次扫描到一个字符,就把它在字符串中出现的位置保存在数组中(好方便每次比较和上次的距离)

如果上次没出现,或者两次距离超过currentlength变量(用来保存f(i)),currentlength就自增。否则,currentlength就改成距离。

之后更新辅助数组,并且更新最大值。


(九)时间效率与空间效率的平衡

49.第四十九题,丑数:只包含因子2,3,5的数称为丑数。求从小到大顺序的第n个丑数。

常规直观算法是:一个数如果能除尽2,就尽可能的除以2,然后对3、5同理。如果最后数变成1了,说明是丑数。然后逐个判断是不是丑数,找到第n个。

高效解法:创建数组保存已经找到的丑数。用三个指针,分别代表*2,*3,*5。刚开始都指向头,之后比较各自指向的数字乘以对应值哪个最小,哪个最小就把结果加入到后面,然后移动指针。

50.第五十题,第一个只出现一次的字符。

(1)字符串中第一个只出现一次的字符:用哈希表做,创建256大小的数组,开始赋0。之后扫描字符串两次,出现则哈希表就+1;第二次发现哈希表中不是0就返回。

(2)字符流中第一个只出现一次的字符:这个题和上次的题目区别是,字符串不能允许你扫描两次。解法是还用哈希表做,刚开始哈希表初始化为-1,之后对应位置的字符第一次出现了,就把里面填成索引。如果再次出现,就改成-2。之后扫描哈希表,保存里面索引最小的数。

有个小问题,要保存最小的数,正常可以先拿第一个当最小的数,之后和它比。书上用了一个法子可以学习一下以后说不定会用到:

int minIndex=numeric_limits<int>::max();

这个返回编译器允许的int最大值。

51.第五十一题,统计数组中的逆序对。数组中如果有一个数字大于后面的数字,则组成一个逆序对。

这题非常难,而且还有个大坑,堪称本书中最难的一道题

先说简单做法:如果碰到编出来即可的题目,那么就顺序扫描整个数组,和后面比,O(n2)量级的时间复杂度。

具体做法是使用归并排序,先把数组分成子数组,统计内部的逆序对数目;然后统计两个相邻子数组之间的逆序对数目。在统计的过程中一边对数组进行排序。创建辅助数组,然后开始两个指针指向两个子数组的结尾,再来一个指针指向辅助数组的结尾。把大的数填入辅助数组中去,然后指针前移。此外,如果第一个子数组中的数组大于第二个子数组中的数字,则构成逆序对,逆序对数目等于第二个子数组中剩余数字的个数。

算法采取core式编程,主函数中先创建一个同样大的copy数组,然后把data内容全都复制过去。然后count=core(data,copy,0,length-1),最后销毁copy,返回count。

core函数中:如果start和end相等,那么无需排序,把data[start]赋值给copy[start],返回逆序对个数0;

之后,用length记录一半长度,left=core(copy,data,start,start+length),right=core(copy,data,start+length+1,end)。坑来了:要记住这里传入的copy和data,和形参中是反的!(原因是要保证二者不断交换)

得到了左右两边分别的逆序对个数后,之后就是统计二者之间:

i、j从两个数组结尾出发向前走,保证i指针大于等于start,j指针大于等于start+length+1,然后一边给copy数组中赋值,一边统计逆序对。如果左边的大,那么逆序对个数就是右边剩下的数字个数,即j-start-length。

最后一个数组没数字了,把另一个数组依次存到copy数组中。最后返回left+right+count;

52.第五十二题,两个链表的第一个公共节点。

这题编程是次要的,介绍一下不同的思路:

除了蛮力法以外,可以用两个栈解决,最后弹出来一一比较,直到找到最后一个相同的节点。

但也可以遍历两次长度,然后一个先走若干步,最后同时走,碰面即可。(和找链表中的环有异曲同工之妙)


(十)知识迁移能力

53.第五十三题 在排序数组中查找数字

(1)数字在排序数组中出现的次数:统计给定数字k在排序数组中出现的次数。

用二分查找法找出第一个k和最后一个k的索引,相减即可。

二分法找第一个k为:中间的数如果大于k,则在前端;中间的数如果小于k,则第一个k在后面;如果中间的数等于k,则看前一个数是不是k,如果是,则在前端,如果不是,则中间的这个数就是第一个k。

(2)0~n-1中缺失的数字:递增数组长度为n-1,每个数字都在0~n-1范围之内,这n个数有且只有一个数不在其中,找出这个数。

问题转换成:找出第一个值和下标不相等的元素。

这又转换成了上面这个问题,二分法,如果中间元素值m和下标相等,就在后半部分查找;如果不相等,则看前一个元素:前一个元素相等,则就是这个m,如果前一个元素也不等,那么在前半部分查找。

(3)数组中数值和下标相等的元素:单调递增数组每个元素唯一,找出任意一个数值等于其下标的元素。

还是二分法,如果中间元素相等,就返回中间元素;如果数值大于中间元素,就在前面找;否则在后面找。

54.第五十四题,二叉搜索树的第k大节点:给定一棵二叉搜索树,找出其中第k大的节点。

中序遍历,返回引用。此题毫无水准,水题一道。

55.第五十五题,二叉树的深度。

(1)二叉树的深度:输入二叉树的根节点,求树的深度。

用递归即可解决。比如可以return (left>right)?(left+1):(right+1); (left和right递归调用)

(2)平衡二叉树:输入二叉树的根节点,判断该树是不是平衡二叉树。

core式编程,core函数两个形参,一个是根节点,一个是深度的引用。返回值是bool类型。

如果根节点是空,返回true,修改深度为0。创建left和right变量,传入递归函数形参中,得到左子树深度和右子树深度,

如果左右子树都是平衡二叉树,则用变量diff得到二者深度之差,diff如果在-1到1之间,修改深度,返回true。

否则返回false。

这样每个节点只遍历一次。

当然, 也可以用之前(1)的函数,得到左右子树深度,如果不在-1到1之间,返回false;否则返回递归(左)&&递归(右)。这种算法会重复遍历节点,不是很高效。

56.第五十六题,数组中数字出现的次数

(1)找出数组中只出现一次的两个数字,别的数字都出现了两次。时间复杂度要求O(n),空间复杂度要求O(1)。

运用一个特性:任何数与0异或,结果都是其自身。任何数与自身异或,结果都是0。

那么此题可以把所有的数字一起异或,最后得到的其实就是要求的两个不同的数字异或的结果。那么这两个数字互相异或,得到的结果肯定不为0(异或是相同为0相异为1),那么在这个结果里找到第一个为1的位的位置,然后以这个为标准,把原先的数字分为两部分,使得两个不同的数字分在两组里。两组分别重新异或一下,得到所求的两个点。

(2)数组中唯一只出现过一次的数字(别的数字都出现过三次)

做法是把所有数字的二进制的每一位都加起来,如果某一位的和能被3整除,那么这位就是0,否则为1。

最后结果就是这个结果。具体编程操作上需要一个长度为32或64的辅助数组,用于保存这个int变量。

57.第五十七题,和为s的数字。

(1)输入一个递增排序的数组和一个数字s,在数组中查找两个数,使得和刚好为s。

常规方法当然是固定一个,另一个挨个去搜索。

比较好的方法:两个指针,一个指向开始,一个指向结尾。如果两相加大于s,那么后面那个前移;如果两个相加小于s,那么前面那个后移。

(2)和为s的连续正数序列。输入正数s,打印所有和为s的连续正数序列(至少包含两个数)

还是用两个指针,small和big表示序列中的最小值和最大值。刚开始,small指向第一个,big指向第二个。然后相当于一个大小可变的滑动窗口。和小于,则small不动,big向前。和大于s,则big不动,small向前。等于s,打印。因为至少要有两个数,所以small走到(1+s)/2就可以停了,因为这样的话两个small就已经超过s了,不需要再走了。

58.第五十八题,翻转字符串。

连续好几道题每个都有两三道扩展,我急着今天总结完,结果看样子还是好多,心烦。

(1)翻转单词顺序:输入英文句子,翻转句子中单词的顺序,但是单词内顺序不变。“I AM A STUDENT."变成"STUDENT. A AM I"。

两次翻转字符串,先整句翻转,再每个单词翻转。

(2)左旋字符串:把若干个字符转移到字符串尾部。

还是写个翻转函数,然后翻转前面n个字符,再翻转后面的字符,再整体翻转一下。

当然如果用的不是char*,而是string,那么太方便了,前面保存到temp里,然后直接erase掉,result=str+temp.c_str()到后面去。注意要用temp.c_str(),否则直接加会报错。

59.第五十九题,队列的最大值。

(1)滑动窗口的最大值:给定一个数组和滑动窗口的大小,找出滑动窗口中的最大值。

蛮力法用O(nk)量级即可解决。但是如果想降到O(n),需要特殊方法。

特殊方法暂时不太熟练,之后回顾时需要注意:

用一个双端开口的队列(deque),存元素时,不存数值而是存下标序号(因为之后要判断这个数字有没有从滑动窗口中移出)。如果当前值大于前面的,则加入队尾,排在它前面比它小的都出队,具体出队是从尾部出队;如果当前值小于前面的,则留下来。同时需要注意,如果(当前处理数字序号-窗口大小)>头一个元素的序号,就把头一个元素给出队,这时是从前面出队。

要得到最大值,直接看队头值即可。

(2)队列的最大值:定义一个队列,实现函数max得到队列中的最大值,要求max,pushback,popfront时间复杂度都是O(1)。

因为是队列,只能从后面入队,前面出队。因此后面如果有大值,前面的大值就没有保存的意义。而后面如果是小值,则需要加入,防止前面大值弹出去以后不再知道后面的大值是什么。

因此需要再定义一个max队列,定义用双端队列来。队列中保存一个struct结构,其中含有值和索引。

插入时,如果当前值大于max队列后面的值,就从后面弹出max队列小值,然后把它加入。

弹出时,如果要弹出的数据的索引和max队列中队头元素中的索引一样,就把max的也弹出,否则只弹出data。


(十一)抽象建模能力

60.第六十题,n个骰子的点数:把n个骰子扔在地上,朝上的点数和为s,打印s的所有可能值出现的概率。

这道题牛客网上没有出现。

n个骰子最小值为n,最大值为6n。需要统计每个点数和出现的次数,然后除以6^n,可以得到想求的概率。

其实个人感觉这个题目可以用map来求,n~6n,n个循环从1~6,然后把所有和加起来当成map的键,统计次数。不过由于n是自己给的,我感觉写代码可以换成,6个循环,每次从1到n,好像这样替代应该行。

当然书上不是这么写的,书上的解法一用递归,然后用的是数组,比较落后,效率也不高。

用循环求解的话,用两个数组来存放骰子点数之和。思路非常独特,需要注意

一轮循环中,一个数组中第n个数代表骰子和为n出现的次数。之后下一轮循环中,和为n的骰子出现的次数等于上一轮中n-1,n-2,n-3,n-4,n-5,n-6次数的总和。(比如第一轮循环,1~6出现的次数都是1,第二次的2~6出现的次数就变成了1~5。比如这次的6,代表第一个骰子取1~5的时候,第二个骰子总有对应的值(如5~1)使得两个骰子加起来的数是6,因此是前面的1~5出现的次数之和。)

之后交换两个数组,重复计算过程。用flag控制数组的交换。

开始第一个数组,前六位都是1。之后循环从2~骰子n:每次再从0~当前骰子数将数组清零。每次加入一个骰子,假如是第k个,真正要更新的就是从k到6*k,每次都更新成上一个数组中的k-1到k-n中累加的值。如果用+=控制,需要提前清零。每次结束时用1-flag更新flag,使二维数组第一维永远在0~1之间切换。

最后骰子全部加完了,从n到6n的数都除以6^n即可得到概率。

61.第六十一题,扑克牌中的顺子:从扑克牌中抽5张牌,判断是不是顺子。A为1,J为11……大小王可以互相顶任意数。

实际题目会传入一个数组,用0代替大小王。因此我们可以先调用sort对数组排序,然后统计其中0的个数,然后再统计相邻数字之间的空缺总数。如果空缺总数小于等于0的个数,那么就连续。(注意两点,第一个,有对子出现肯定不是顺子;第二,可以让gap+=data[i+1]-data[i]-1,因为相邻的两个数也会差1,所以需要减掉这个1。

62.第六十二题,圆圈中最后剩下的数字:0~n-1排成一个圆圈,每次从圆圈中删除第m个数字,问最后剩下的是哪个。

这个题目本身挺麻烦,但是现在有了stl库,就变得很方便。创建链表list即可,开始赋值,之后数数,erase掉即可。如果到了end,就再到begin那里。注意,之后erase的时候,比如要erase第a个节点,先拿一个迭代器保存下一个位置的节点b,然后erase掉a点,再把刚刚保存的b赋值给a点。因为list是链表,erase掉以后就没有了。

还有一个解法是“创新解法”,挺复杂的,以前看懂了,但是现在又不懂了,估计再啃会了也记不住,这个解法颇有炫技的意思,放弃。

63.第六十三题,股票的最大利润。把某时间的股票价格按照时间顺序存入数组,问买卖股票一次可获得的最大利润。

此题牛客网上没有。

如果用蛮力法,逐一求解差值用On2复杂度。

如果要求O(n)复杂度,则解法为:

用两个变量,一个表示前i-1个数字的最小值,一个表示目前的最大利润。

如果扫描到第i个数字,就更新最小值,同时计算最大利润并保存。


(十二)发散思维能力

64.第六十四题,求1+……到n:不用乘除法,for、while、ifelse、switchcase及条件判断语句。

几种方式:第一,利用构造函数求解,类内有俩静态变量,一个是当前值,一个是和。类外赋初值,都是0。构造函数每次创建n自增,更新sum。之后创建一个大小为n的数组就行。

第二,用递归来做。由于不能用if,else,所以需要一个进行终止的操作,即用虚函数:A类为终止返回0,B类继承A类,同名sum函数,外面定义A* Array[2],用Array[0]指向基类A的实例a,用Array[1]指向派生类B的实例b。在B中具体递归调用Array[!!n]->sum(n-1)+n。这里面的两个感叹号表示对n取反再取反。只有当n=0的时候,取反再取反才是Array[0](即终止条件)。

最后value=Array[1]->sum(n)。

还有两种方法不死记了,会两个即可。

65.第六十五题,不用加减乘除做加法。

可以想到二进制方法解决。

0加0是0,1+1是0,0+1是1,1+0是1,这和异或是一样的,所以用位异或代替没有进位的加,这是没有疑问的。

再说1和1与操作为1,进位也只有1+1才有进位,所以用与操作代替进位。为了对齐,需要左移一位。

所以开始两数异或,然后两数再进行与操作,左移。异或的结构和与操作左移的结果又构成两个数,重复进行,直到与操作左移的结果变成0。

66.第六十六题,构建乘积数组。给定A【0,1,……n-1】,构建B【0……n-1】,其中B【i】为A从头乘到尾,除了第i项。限制不能使用除法。

这个需要配图:

本身直观做法是连乘n-1次,遇到自己所在的数就不乘了。但是这样时间复杂度是O(n2)。

为了以O(n)来求解这个题目,思路简单来说,就是先从上到下算上面图里的下三角,再从下至上在下三角的基础上算上三角,进行更新。想明白了这个,写代码就方便多了。


(十三)两个面试案例

67.第六十七题,把字符串转换成整数。给一个char*字符串表示的数字,把它转换成真正的数字。

这个要考虑鲁棒性,要考虑空指针,空字符串,,无效输入,正负号和溢出问题。溢出可以通过判断当前数字如果是正数,是否大于0x7FFFFFFF,如果是负数,是否小于0x8000000。

涉及到一个知识点:atoi可以把字符串变成整数,里面的机制是用一个全局变量控制来区分字符串为空,返回0,还是输入的字符串是0返回0。

68.第六十八题,树中两个节点的最低公共祖先。即输入两个树节点,找到他们的最低公共祖先。

如果是二叉搜索树,找到第一个节点的值在二者中间即可。

如果是普通的树,如果有指向父节点的指针,那沿着他们向上走,就变成了求两个链表的第一个公共节点(第五十二题)这个问题。

如果是普通的树,没有指向父节点的指针:用辅助内存,比如两个链表,采用递归遍历,树遍历的时候,每往下一层就入栈,往上就出栈,,直到找到两个节点。之后就又变成了求公共节点的问题。


(十四)其他内容

到这里为止,剑指offer就已经总结完了。复制到wps里一看,竟然有一万七千多字,两天总结了这么多。

但是这还没完,还应该补充几点我心里有的虚的别的内容,心里才有底气:

1.数据结构中图那节的算法总结。近两天内应该从家里把前年考研的内容拿出来,再看看补上。

2.排序那节,把快排算法死记住,熟练默写没有bug。还有归并排序。

3.上上个学期学过的林晓慧老师主讲的算法设计与分析的内容,很重要可惜都忘了,需要补充一点点上来。

暂时就先想到这些,有想起来的再补充。

希望能找到一个满意的工作!这部分内容我还要多回来看看。

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值