剑指Offer(第二版)-思路简述-第二期(中等难度)

本文章旨在用较少且通俗的话说明我做这些题的做题思路(题目链接直接点击各题目标题即可)。只有思路,不放代码,主要是方便以后的复习和二刷、三刷。以思路为主,答案次之。读者如果想避免边看答案边做的方式,以思路为起点动手学习,这也是个不错的学习资源。

中等难度

1.二维数组中的查找

使用双指针,定义l=0,r=n-1,也就是说从矩阵的右上角开始,如果当前位置值大于目标值,就左移即--r,如果小于目标值,就下移,即++l。直到找到为止。

2.矩阵中的路径

经典的回溯题。

①选择一个实例画图,略。

②确定结束条件:当得到的单词匹配且长度相等时返回结束。

③确定选择列表:一个点的四周。

④剪枝:已经访问过的不能重复利用且必须满足匹配单词的部分,所以需要去掉四周中已经访问过的和不匹配的。

⑤⑥⑦选择、递归、撤销:选择四周符合条件的某点并设置为访问过,递归(单词匹配数+1),撤销前面选择的点并设置为未访问。

3.机器人的运动范围

先写一个工具类函数求两个数的位数之和。使用dfs遍历所有满足条件的位置即可。

从第一个点[0,0]开始进行dfs:

①结束条件:该点的横纵坐标位数之和大于k。

②记录数量并标记为访问过。

③遍历四周(邻接结点)。

④四周越界判断以及是否访问过判断,未越界未访问过才行。

⑤符合条件的dfs递归遍历即可。

具体的dfs步骤总结可以看这一篇,超详细:DFS刷题总结

4.重建二叉树

很经典的题目使用递归。

①结束条件:由于这题的重点是确认根节点在中序遍历中的位置以此来划分左右子树,所以需要涉及到左右子树区间的划分,当区间为空也就是左右子树存在空的时候就返回空。

②返回结果:返回当前建好的树。

③本级递归:由于前序是根左右的顺序,所以根节点一定是该区间第一个,通过根节点去划分中序遍历的左右子树区间,比如一开始前序遍历区间是[l,r],中序遍历是[s,e]。找到左子树后区间后(假设根节点在中序遍历中的下标是index),左子树的前序遍历区间为[l+1,l+(index-s)],左子树的中序遍历区间为[s,index-1];右子树的前序遍历区间为[l+(index-s)+1,r],右子树的中序遍历区间为[index+1,e]。递归得到建成左右子树,把当前节点左右指向左右子树,建树完成。

5.剪绳子

经典的动态规划题目。

dp[i]代表长度为i的绳子被分成m端后的最大乘积。初始化dp[2]=1。

剪出长为j的端,剩下i-j不剪就是i-j,剪了就是dp[i-j],

所以状态转移方程就是dp[i]=max(max(j*(i-j),j*dp[i-j]), dp[i])。

6.剪绳子II

其实和第五题一样,不过由于答案需要取模,可能会导致动态规划历史记录不对。所以用贪心算法,当长度为2,3,4时均分为两段,结果为1,2,4。当长度大于4时,每次都分出长度为3的端,直到最后所剩长度不大于4,此时就不分段了,这样的方式满足最大乘积。

7.树的子结构

这题需要写一个辅助类函数,判断二叉树B是否与二叉树A中与B的根节点相同的节点的子树存在相同。递归:

①结束条件:B为空,代表B满足是其一部分结构;如果B不为空,A为空,那么,不满足;A、B都不为空,但是值不同,不满足。

②返回值:同时需要对当前节点的左右子节点进行相同的递归调用,最后返回左右子树匹配情况的与运算结果,也就是说左右子树都匹配,则返回true,反之false。

有了这个函数之后,在解决函数里递归:

①结束条件:因为空树不是任意一个树的子结构,所以若A、B均为空,则结束。

②返回值:如果A、B满足前面写的辅助类函数,则返回true,不满足,我们就继续递归遍历,让A的左子树以及右子树去和B通过辅助函数比较。返回值是左右子树匹配情况的或运算,也就是左右子树存在一个能匹配子结构,就返回true.

其实我觉得这一步可以不用递归的,直接while循环遍历A,找到与B根节点相同节点,然后调用辅助函数,满足则返回true,不满足继续遍历去找下一个与B根节点相同的点进行相同操作,如果最后都没找到或者找到但匹配不上就返回false。

8.表示数值的字符串

写两个工具类函数,检查该部分是否是无符号数以及是否是有符号数。

首先开始时,去掉前置空格,然后判断第一个部分是否是数,然后判断后面是否有小数点,如果有小数点,小数点后面需要判断是否是无符号数,小数点左右两边只需要一遍满足是数,另外一边可以为空;然后再判断是否有e/E,如果有,那么后面部分有需要判断是否是有符号数,e/E的前后都需要为数,最后判断其余部分是否全是空格,如果不是就不是数值。

9.数值的整数次方

偶数 x^n=x^(n/2)*x^(n/2);奇数 x^n=x^(n-1)*x;

使用递归偶数时,f(x,n)=f(x,n/2)*f(x,n/2);奇数是f(x,n)=f(x,n-1)*x;n=0,f(x,n)=1.0。

然后记得分类一下正负号的情况即可。

10.复杂链表的复制

使用unordered_map来存储节点,key表示员链表结点,value表示复制后对应的节点。因为链表复制无非就两点,值复制和结构复制。

①复制值,即map[cur]=new Node(cur->val)。

②复制结构,map[cur]->next=map[cur->next];map[cur]->random=map[cur->random]。

11.二叉搜索树与双向链表

一般这种涉及到二叉搜索树顺序的问题就应该先考虑中序遍历,因为中序遍历后节点值是从小到大的。通过中序遍历的过程来形成双向链表。定义当前遍历节点的前一个节点pre,从根节点开始进行中序遍历。

①结束条件:如果当前节点遍历完了为空就结束。

②返回结果:中序遍历注重过程,函数定义为void类型无返回结果。

③本级递归:选递归左子节点;处理当前节点,若当前节点前一个节点是空,代表当前节点是头节点,不是,则建立前驱节点到当前节点的后继指针,pre->right=cur。建立当前结点到前驱结点的前驱指针,cur->left=pre。建立完后,cur节点就到下一位了,那么pre同时也要下移一位。

结束时,cur=NULL,pre是最后一个节点,最后记得首位之间也要建立指针。即head->left=pre,pre->right=head。

12.栈的压入、弹出序列

这题是一道模拟题,模拟这个过程即可,我们定义一个栈根据入栈顺序入栈,然后入栈时根据出栈顺序,对于相等的入栈元素弹栈,然后继续入栈让栈顶与出栈顺序中的下一个进行对比,如果出栈和入栈符合的话,那么我们模拟的栈最后会空。

13.字符串的排列

和这篇文章里的全排列II一样,换汤不换药,详情可以看这篇,一个比较常规的回溯题。

14.从上到下打印二叉树

BFS打印各层数据即可。

15.数字序列中某一位的数字

使用迭代找到n所在的位数区间。

我们定义参数位数为digit的起始数start,那么位数为digit的数总共有9*start个,总位数是cnt=9*start*digit。

比如9*10*2,9*100*3,9*1000*4等等,这样可以算出n所在digit和start,然后得到n所在的数,以及所在的数的位数,这样就能得到结果了。

16.从上到下打印二叉树III

和14一样使用BFS,不过最后打印的时候,判断一下层数,偶数层翻转打印即可。

17.二叉搜索树的后序遍历序列

由于后序遍历是左右根的顺序,所以最后一个是根,那么我们需要写一个函数,入参左右子树区间,即[l,r]以及区间下一位根节点。

①结束条件:左右子树为空,即区间为空,那么就是l>r。

②返回值:返回是否当前区间是否满足后续遍历。

③本级递归:首先根据根节点找出区间中左右子树的分隔点。判断分隔点后的右子树是否全部满足大于根节点值,不满足就直接返回false。然后如果全是左子树或者全是右子树,则递归调用来判断这部分区间是否满足后续遍历。如果存在左右子树,那么需要分别对左右子树进行递归调用来判断这两部分区间是否满足后续遍历,如果都满足就表示当前节点所在子树也满足后续遍历。

18.二叉树中和为某一值的路径

回溯题,想更了解回溯可以看我的回溯总结专栏,第一期指路

①选择一个实例画图,略。

②确定结束条件:遍历到叶子节点且路径长度等于目标值。

③确定选择列表:一个节点的左右子节点,就是固定的两个选择。

④剪枝:这题不存在重复的情况,不需要。

⑤⑥⑦选择、递归、撤销:选择不为空的左右子节点,递归(剩余路径长度=目标-当前节点),撤销前面选择的点。

19.数组中数字出现的次数

在做这题之前需要知道一个前提,假如一个数组中除了一个数外其他数都出现了两次,那么这些数的异或结果就是出现一次的数,因为相同数异或为0,最后只剩一个出现一次的数。

所以说这题顺着这个思路就是要把题目中数组分组,分成两组,且每组是若干对相等数据和一个题目结果数之一组成的形式,使得两个只出现一次的数各在一组,这样对于每组就简化成如上所示的子问题。

其实分组相同的数没有难度,因为相同数与一个带二进制只带一个1的数进行与运算,结果要么都是1要么都是0,根据这个就能简单分组,主要的问题是如何保证我们最后求的结果被分开到两组呢?那么这题的话我们还是先求异或,得到的结果是只出现一次的两个数的异或结果。得到异或结果的任意一个二进制为1的位,这个位一定能保证结果的两个数该位不同,因为相同的话,就不会为1,知道这个位两数不同之后,我们选择只有该位为1的数进行与运算来分组,为0的分一组,为1的分一组。分好组后,在组中异或得到最终结果的两个数。

20.数组中数字出现的次数 II

这题就正常解法,先排序,分三种情况,在头,在尾,在中间。

21.把数组排成最小的数

把数字数组转为字符串向量,然后使用排序

sort(strs.begin(),strs.end(),[](string a,string b){
    return a+b<b+a;
});

这样使得得到的字符串向量拼接起来值最小(如果是字母就是字典序的)。

22.把数字翻译成字符串

dp问题。dp[i]表示前i个字符的翻译方式数。初始化dp[0]=1。

如果第i-1位和第i位只能分开编码,那么dp[i]=dp[i-1];

如果第i-1位和第i位满足第i-1位不为0,且第i-1位和第i位组成的两位数大小不能超过25,就能合并编码了。dp[i]=dp[i-1]+dp[i-2]。

23.礼物的最大价值

很经典的dp问题,相当于找一条矩阵中的最短或最大路径。

dp[i][j]表示到达位置ij能拿到的最大价值礼物。因为只能向右或者向下走,先要初始化,dp[i][1](ps:我做动态规划题目的习惯都是下标从1开始而不是0,因为这样在某些极端的题目上可以省略条件判断以及避免越界问题。)和dp[1][j]都需要初始化。

状态转移方程就是dp[i][j]=max(dp[i-1][j],dp[i][j-1])+grid[i-1][j-1]

24.最长不含重复字符的子字符串

滑动窗口。窗口右区间每次扩大得到的新数都要和之前的窗口里的数进行比对,如果发现重复,就把窗口左区间移动到重复数的右边一位。然后记录本次区间长度并进行最大值比对更新,然后继续移动右区间进行相同处理。

25.丑数

非常经典的dp问题。

①定义dp[i]表示第i个丑数

②定义初值,dp[1]=1,第一个丑数是1

③我可以想一下相邻丑数之间可能的关系,因为只包含质因子 2、3 和 5 ,所以可知,有三种情况:

dp[i]可能是由dp[i-m]*2或者dp[i-m]*3或者dp[i-m]*5(其中m可能去1<=m<=i-1)。

④所以说我们需要定义三个起点p2,p3,p5,均为1

dp[p2]*2,dp[p3]*3,dp[p5]*5中选择最小的作为下一个丑数,那么状态转移方程为:

dp[i]=min(dp[p2]*2,min(dp[p3]*3,dp[p5]*5)),找到较小的对应的起点p需要+1,然后继续去找下一个。

26.队列的最大值

维护两个队列,一个队列q正常出队入队,另一个队列d为双端队列,保证最大值在里面且保持整个队列递减。

①一个元素入队,q正常入队,d需要从队尾开始做比较,小于该入队元素的都移除,直到碰到不小于的,那么d就可以入队该元素。

②q正常从队头移除,d此时队头与出队元素相同,则d的队头也移除。

③最大值就返回d的队头即可。

27.构建乘积数组

因为不能用除法,我们就设置数组d[i]表示前i位的乘积,p[i]表示从结尾到第i位的乘积。

所以最后结果a[i](这里的i是从0开始,所以0~i-1有i位)就是前i位的乘积d[i](这里的i下标从1开始的)乘上从结尾到第i+1位的乘积p[i+1]。

28.n个骰子的点数

一个比较经典的动态规划问题,结果是求概率,我们可以试着转换一下,只要知道每个数据和出现的次数就能得到概率,因为次数/总次数(6^n)=概率。我们现在试着只求次数,这样的话其实这题就很像爬楼梯

爬楼梯:

排楼梯dp[i]表示爬上i层楼梯的方法数,由于一次只能一步或者两步,所以状态转移方程为:dp[i]=dp[i-1]+dp[i-2]

此题

此题中我们定义dp[i][j]表示前i个骰子的点数和为j的出现的次数,由于每次多一个骰子,就相当于多了1~6个数字和的可能。那么状态转移方程为:

dp[i][j]=dp[i-1][j-1]+dp[i-1][j-2]+dp[i-1][j-3]+dp[i-1][j-4]+dp[i-1][j-5]++dp[i-1][j-6]

也就是说i个骰子的点数和为j的次数等于,i-1个骰子点数和为j-1一直到j-6的次数的和,可以理解为这里的步数不止走1步或者2步,而是1~6步都可以。

29.把字符串转成整数

手写atoi函数。这种题重在逻辑步骤梳理,难点不在于思路,在与判断清晰且条件全覆盖到。

①去掉前导空格。如果全是空格返回0

②定义一个符号位sign=1,如果第一非空字符是-,就置为-1

③继续判断,碰到数字字符,就乘符号位并加到结果集;碰到非数字字符,直接后面省略结束循环。

ps:在加入新数字的过程中,即乘符号位并加到结果集的过程中,需要判断是否越界,如果越界则直接返回int型的最大值或者最小值。

这个写法比较固定,发一段代码

//如果当前结果大于最大值/10或者不大于刚好等于,但是我要加进来的数可以使其最终结果越界
if (res > INT_MAX / 10 || (res == INT_MAX / 10 && (curChar - '0') > INT_MAX % 10)) {
     return INT_MAX;
}
if (res < INT_MIN / 10 || (res == INT_MIN / 10 && (curChar - '0') > -(INT_MIN % 10))) {
     return INT_MIN;
}

30.股票的最大利润

我们先假设我们在第i天卖出,也就是说先固定住卖出的天i,那么买的天就是从0~i-1中找出最小值,这个最小值是0~i-1的局部最小值,我们可以保存这个计算出来的局部最大利润,通过移动我们卖出的天数i,并保证买的天始终是0~i-1的局部最小值,以此来继续去找最大利润更新,这样i移动到结尾时,就必然能找出最大利润。

31.求1+2+…+n

使用递归,f(n)=f(n-1)+n。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值