剑指offer
(一)栈和队列
- python的list就是一个栈,使用append和pop
['1', '2', '3', '4']
pop 4
pop 3
['1', '2']
剑指 Offer 09. 用两个栈实现队列
剑指 Offer 30. 包含min函数的栈
使用辅助栈的方式,冗余的添加min_stack的数据
(二)链表
剑指 Offer 06. 从尾到头打印链表
剑指 Offer 24. 反转链表
- 用栈,效率低一些
- 使用pre指针,保留上一个节点,当前节点指向上一个节点
剑指 Offer 35. 复杂链表的复制
- 方法一:回溯 + 哈希表(简单,高效)
其实就是,没有就创建,一直到指针指到空节点,就开始回溯。
但是这个过程会有可能的问题是random指针可能是循环的,这个可以用哈希表进行判断,如果已经创建过了,就直接返回,这样也能避免重复创建。
class Solution:
node_map = {}
def copyRandomList(self, head: 'Node') -> 'Node':
if not head:
return head
if head not in self.node_map:
new = Node(head.val)
self.node_map[head] = new
new.next = self.copyRandomList(head.next)
new.random = self.copyRandomList(head.random)
return self.node_map[head]
- 方法二:迭代 + 节点拆分
其实就是,把列表
A -> B -> C -> D
拆分为
A -> A’ -> B -> B’ -> C -> C’ -> D -> D’
再把random指针从挪到random’上
再拆开
A -> B -> C -> D
A’ -> B’ -> C’ -> D’
得到一个复制的A’
(三)字符串
剑指 Offer 58 - II. 左旋转字符串
return s[n:] + s[:n]
剑指 Offer 05. 替换空格
return s.replace(" ", “%20”)
(四)数组
剑指 Offer 03. 数组中重复的数字
用hash set
剑指 Offer 53 - I. 在排序数组中查找数字 I
两次二分查找,寻找左边界和右边界,最后得到结果
剑指 Offer 53 - II. 0~n-1中缺失的数字
首先是0-n-1,即:
- 在缺失的数组元素之前,数组元素和下标一致
- 在缺失的数组元素之后,数组元素和下标不一致(将会小一位)
因此只需要二分查找到下标不一致的位置即可
剑指 Offer 04. 二维数组中的查找
线性查找,从右上角开始查(同理也可以从左下角开始查,只要列和行是相反的位置,就能进行比较)
剑指 Offer 11. 旋转数组的最小数字
-
方法一:暴力法,即如果 i 比 i+1大,那么i+1就是最小值,如果不存在这样的一个i,那么选第0个数
-
方法二:二分法
- 有序数组,因此可以使用二分查找
- 每一次知道mid之后能够知道,要找的数据在左边还是右边:
- 如果mid比右边的小,则证明右边的是有序的,那么分界点在左边
- 如果mid比右边的大,则证明分界点一定在右边
- 特殊情况就是因为数据会重复,所以可以用笨办法,如果left等于right的时候,就right–
剑指 Offer 50. 第一个只出现一次的字符
-
方法1:hash,遍历两次
-
方法2:hash + 队列
把数据放进hash里,如果没有出现过的字符就放进队列里,然后弹出队列,确认hash中的元素是只出现一次的。
之所以加一个队列,而不是重新遍历元素,是去除重复元素的影响
(五)树(搜索与回溯算法)
知识点:
- 一般树的遍历使用递归
- 树的层序遍历,涉及层相关的则使用队列
简单
剑指 Offer 32 - I. 从上到下打印二叉树 I 遍历所有节点
用队列,把左右节点放进队列里,先进先出则可以遍历
剑指 Offer 32 - II. 从上到下打印二叉树 II 层序遍历
用队列 + curr_size,一次性清空一层的数据,而不是一个个清
剑指 Offer 32 - III. 从上到下打印二叉树 III
基于上一题,使用双端队列collections.deque(),可以从两边插入的:
- 如果从左至右,我们每次将被遍历到的元素插入至双端队列的末尾。
- 如果从右至左,我们每次将被遍历到的元素插入至双端队列的头部。
- 用一个l2r指针,判断是头插还是尾插,最后记得反转l2r指针
剑指 Offer 26. 树的子结构
当我们遍历树的时候,一般的方法递归
代码设计分为三部分:
- 检查B树是否是A树的子结构的方法,递归的调用这个方法,缩小A的范围
- 递归检查这个节点以下是否相等的函数
- 返回条件:B树被递归的遍历完
剑指 Offer 27. 二叉树的镜像
从叶子节点开始互换,递归遍历左右子树之后,互换子树。
剑指 Offer 28. 对称的二叉树
镜像对称的树:
- 它们的两个根结点具有相同的值
- 每个树的右子树都与另一个树的左子树镜像对称
方法1:递归,树的遍历就离不开递归,递归什么时候结束:
- 成功结束:
- 如果两个节点都为空,证明对称,return True
- 如果a, b两个节点val相等,则看a.right和b.left,以及a.left和b.right相不相等
- 失败结束:
- 如果一个为空一个不为空,则失败
- 如果val不相等,则失败
方法2:迭代 + 队列,迭代的话一定会用到队列
只需要用到一个队列,交替的弹出左右节点即可
困难
(六)动态规划
简单
剑指 Offer 10- I. 斐波那契数列
f[n] = f[n-1] + f[n-2]
也可以用
a, b = b, a + b
当然也可以用斐波那契通项公式:(sqrt_5 / 5) * (math.pow((1 + sqrt_5) / 2, n) - math.pow((1 - sqrt_5) / 2, n))
剑指 Offer 10- II. 青蛙跳台阶问题
实际上就是变相的,斐波那契数列
理解一下,青蛙可以跳一台阶,也能跳两个台阶:
设跳上 n 级台阶有 f(n) 种跳法。在所有跳法中,青蛙的最后一步只有两种情况: 跳上 1 级或 2 级台阶。
- 当为 1 级台阶: 剩 n-1 个台阶,此情况共有 f(n-1) 种跳法;
- 当为 2 级台阶: 剩 n-2 个台阶,此情况共有 f(n-2) 种跳法。
因此对于f(n) = f(n-1) + f(n-2)
只是初始值不同,初始值是1,因为不会有0个台阶的情况
剑指 Offer 63. 股票的最大利润
-
动态规划(暴力法):遍历两次,记录每一个点与之后各个点的差值,然后记录最大值
-
抄底法:记录最低点,然后在最低点之后开始记录每一天的收益,就像买股票一样天天想着发财。然后直到最低点的再次出现,就重新刷新最低点。
中等
剑指 Offer 42. 连续子数组的最大和
子数组:字数组也是数组,需要区别于子序列
- 暴力法:记录以每一个数字开始的,后续的数组的所有和,取最大值
- 动态规划:
暴力法中,重复的部分就是,当一串数组的数字为负数的时候,加一个负数会更负数,那么还不如直接从这个当前这个数开始计算。
比如序列:
-1, -2, -3, 1
其中前三个-1, -2 -3的和是-6,那还比不上第四个1大,那其实就没必要加了,直接从1开始计算即可。
根据动态规划的思想,定义f(i)表示以i结尾的最大子数组和:
f(i) = max{f(i-1) + nums[i], nums[i]}
因此我们可以记录一个pre表示f(i-1),使pre + nums[i]和当前nums[i]比对,得到
pre = max{pre + nums[i], nums[i]}
f(i) = max{pre, max}
- 分治法,其实使用的是线段树,太过复杂,只在代码中帖了,具体解释在本题已接种:题解
剑指 Offer 47. 礼物的最大价值
动态规划问题最难的是定义:
- dp数组所表示的内容
- dp的下标所表示的内容
首先,题目求的是能拿到礼物的最大价值,我们可以考虑求出到达每一个格子能够拿到的最多的价值的礼物是多少。
我们设f(i, j)表示的是在i, j位置最大的礼物价值
因为只能向右或者向下选择礼物,而且每个格子上都有礼物,可以得到两个必然发生的事情:
- 对于i, j位置,只可能是从(i, j - 1)或者(i - 1, j)下来
- 如果一个格子会被选中,则一定是从f(i, j - 1)或者f(i - 1, j)两个路线中,继承了上一个路线的最大值的位置下来的
则可以得到状态转移方程:
f(i, j) = max{f(i - 1, j), f(i, j - 1)} + gift(i, j)
当然这个还需要考虑边界的情况,即:
- i=0, j=0,初始点,状态方程:f(i, j) = gift(i, j)
- i>0, j=0,第一行数据,不能j-1,状态方程:f(i, j) = f(i - 1, j) + gift(i, j)
- i=0, j>0,第一列数据,不能i-1,状态方程:f(i, j) = f(i, j - 1) + gift(i, j)
- i>0, j>0,状态方程:f(i, j) = max{f(i - 1, j), f(i, j - 1)} + gift(i, j)
优化:
-
判断条件优化
在循环当中,真正的操作耗时主要在于if语句的判断,因此考虑上述的边界情况,在循环中每次都要判断的问题。
优化思路可以是先把边界值先for循环算出来,则在判断的时候,就只需要计算i>0且j>0的情况即可。 -
空间复杂度优化
一般的动态规划算法,都需要自己事先定义一个dp数组去接收动态规划的数据。
这个地方因为是从上往下的计算,因此可以直接修改原数组的数值,这样就省去了开辟数组的空间。
当然这个地方还是提供一下各类语言初始化数组的语句:
- java
// 开辟一维数组
int[] arr1 = new int[5]; // 数组长度不一定要一个常量,但是数组长度固定在new的时候后的值
// 开辟二维数组
int[][] arr2 = new int[4][3];
- golang
// 开辟一维数组
// 开辟二维数组
- python
# 开辟一维数组
arr1 = [0] * 5 # python由于没有数组只有链表,因此要初始化数组就需要用0进行填充
# 开辟二维数组
# error: arr2 =
arr2 = [[0]*5 for _ in range(5)]
剑指 Offer 46. 把数字翻译成字符串
相似题目:打家劫舍
动态规划的题目最主要的是搞明白,dp的状态转移方程,也就是说f(i)表示的是什么东西。
在这里,可以有两种翻译:
- 每个数字单独翻译
- 两个数字组合在一起,范围在0 <= num < 26之间,也可以翻译
则设f(i)表示的是以i结尾有多少种字符串排序方法
考虑到以下两种情况:
- 如果只第i个单独翻译,即不与第i-1个组合,那么:
f(i) = f(i-1)
- 如果第i个和第i-1个组合,那么:
f(i) = f(i-1) + f(i - 2) // 条件是0 < i-1 <=2, 0 <= i < 6
则可以得到状态转移方程:
f(i) = f(i-1)
= f(i-1) + f(i - 2) // 条件是0 < i-1 <=2, 0 <= i < 6
剑指 Offer 48. 最长不含重复字符的子字符串
这个题使用的滑动窗口的方式
(七)双指针
剑指 Offer 18. 删除链表的节点
剑指 Offer 22. 链表中倒数第k个节点
双指针,用一个指针在前面,一个指针在后面,中间相隔k个值,等遍历完后把后面的指针输出
剑指 Offer 25. 合并两个排序的链表
-
迭代
-
递归,好理解,不好想到
剑指 Offer 52. 两个链表的第一个公共节点
-
使用hash,遍历list1,存入所有节点到set中,然后遍历list2的时候如果在set中则直接输出
-
使用双指针:
- 保证list1和list2都不为空
- 指针p1遍历list1,指针p2遍历list2
- 当指针p1遍历完list1之后,指向list2的头节点,继续遍历
- 当指针p2遍历完list2之后,指向list1的头结点,继续遍历
- 两个指针碰撞的点就是公共节点
为啥呢?
因为p1指针和p2指针遍历的长度都是:list1 + list2,因此碰撞的点一定是公共节点
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
双指针,左右指针,
- 如果left指到奇数,则直接left++
- 如果left指到偶数,则在right中寻找奇数与其交换,不然right–
- 如果left和right交换成功,则right–和left++
剑指 Offer 57. 和为s的两个数字
-
hashmap
-
左右指针
- 当l + r < target,则l++
- 当l + r > target,则r–
剑指 Offer 58 - I. 翻转单词顺序
-
使用内置reversed方法
-
实现reversed方法
-
双端队列