题目 | 简述、方案、死人坑 |
---|---|
二叉树的层序遍历 | BFS比较好,这里我们要用python里面的deque 和popleft ,另外记得不能用len(q) 而是要把len(q) 记录下来 |
两数之和 | 哈希表,记得我们记录的是坐标而不是数字就行 |
岛屿数量 | DFS,不过从来没见人考过 |
搜索旋转排序数组 | 二分查找,第一次找大于等于队头的,第二次找小于等于目标元素的,注意两次中间必须要有排除异常值也就是elif那一句,不然肯定会有问题if target >= nums[0]: elif l == len(nums) - 1: else: |
全排列 | 回溯,用一个visit 就可以了 |
买卖股票的最佳时机 | 同样用Kadane 算法即可min_price = min(p, min_price) max_profit = max(p - min_price, max_profit) |
合并两个有序数组 | 这里逆序即可,其他的和合并两个有序链表一样 |
有效的括号 | 栈,用栈来实现就行了 |
环形链表 | 快慢指针即可,如果找找环入口,就相遇之后齐步走,就能遇到 |
二叉树层序遍历
题目描述
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
|
|
方法:双端队列 deque
- 初始化队列:将根节点放入队列中。
- 逐层遍历:
- 在每一层开始时,记录当前层的节点数量(即队列的长度)。
- 遍历当前层的所有节点,将其值加入结果列表,并将其子节点(左子节点和右子节点)加入队列。
- 重复上述步骤,直到队列为空。
- 返回结果:最终返回按层存储的结果列表。
|
|
时间复杂度分析
- 时间复杂度:
O(n)
,其中n
是二叉树的节点总数。每个节点会被访问一次,并且每个节点的入队和出队操作的时间复杂度都是O(1)
。 - 空间复杂度:
O(n)
,队列中最多会存储一层的节点数。在最坏情况下(完全二叉树),最后一层的节点数为O(n/2)
,因此空间复杂度为O(n)
。
易错点
- 根节点为空:如果根节点为空,需要直接返回空列表,而不是继续遍历。
- 队列的初始化:队列的初始化应该以列表形式传入根节点,而不是直接传入根节点。
- 当前层的节点数量:需要在每一层遍历之前记录当前层的节点数量,否则队列的长度会动态变化,导致无法正确划分层次。
- 结果列表的存储:每一层的节点值需要单独存储为一个列表,然后加入结果列表中
变体与应对方法
-
自底向上的层序遍历:
- 问题描述:要求从最后一层开始,逐层向上遍历。
- 应对方法:在遍历过程中,将每一层的结果插入到结果列表的头部(例如使用
result.insert(0, level)
),或者最后将结果列表反转(例如使用result.reverse()
)。
-
锯齿形层序遍历:
- 问题描述:要求一层从左到右,下一层从右到左,交替进行。
- 应对方法:在遍历每一层时,判断当前层的奇偶性(例如通过
len(result) % 2
),如果为奇数,则反转当前层的节点值。
-
层序遍历的节点深度:
- 问题描述:要求返回每个节点所在的层数(深度)。
- 应对方法:在遍历时,记录每个节点的深度信息,并在结果中存储深度值。
两数之和
题目描述
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例:
|
|
核心思路
这道题是经典的数组查找问题,要求在数组中找到两个数,使得它们的和等于目标值。为了高效地解决这个问题,我们可以使用 哈希表(字典) 来存储已经遍历过的元素及其下标,通过查找哈希表来判断是否存在满足条件的数对。
实现步骤
- 初始化哈希表:创建一个空字典,用于存储数组元素及其下标。
- 遍历数组:
- 对于当前元素
nums[i]
,计算目标值与当前元素的差值target - nums[i]
。 - 如果差值在哈希表中存在,说明已经找到满足条件的数对,返回它们的下标。
- 如果差值不存在,将当前元素及其下标存储在哈希表中,继续遍历。
- 对于当前元素
- 返回结果:如果遍历结束后仍未找到满足条件的数对,返回
[-1, -1]
(根据题目描述,通常可以确保有解,因此这一步不会被执行)。
为什么使用哈希表?
哈希表的查找和插入操作的时间复杂度均为 O(1)
,因此可以极大地提高效率,避免双重循环导致的 O(n^2)
时间复杂度。
代码实现:哈希表
|
|
时空复杂度分析
- 时间复杂度:
O(n)
,其中n
是数组的长度。我们只需要遍历数组一次,每次查找哈希表的时间复杂度为O(1)
。 - 空间复杂度:
O(n)
,哈希表最多需要存储n
个元素。
易错点
- 哈希表的初始化:哈希表需要提前初始化为空字典,而不是在循环中动态创建。
- 差值的计算:在查找哈希表时,需要注意计算的是
target - nums[i]
,而不是反过来。 - 返回值的顺序:如果差值存在于哈希表中,返回的下标顺序应为
[哈希表中的下标, 当前下标]
。 - 边界条件:如果数组为空或只包含一个元素,需要额外处理(但根据题目描述,这种情况不会出现)。
变体与应对方法
-
多个解的情况:
- 问题描述:数组中存在多个满足条件的数对,要求返回所有的数对。
- 应对方法:将满足条件的数对存储在一个列表中,最终返回该列表。
-
包含重复元素的情况:
- 问题描述:数组中可能包含重复元素,要求返回唯一的数对。
- 应对方法:使用集合存储已经找到的数对,确保结果唯一。
-
和为
target
的子数组:- 问题描述:要求找到一个连续的子数组,其和等于
target
。 - 应对方法:使用前缀和和哈希表来解决,具体可以参考 560. 和为K的子数组。
- 问题描述:要求找到一个连续的子数组,其和等于
岛屿数量:DFS/BFS
题目描述
给你一个由 '1'
(陆地)和 '0'
(水)组成的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。
示例:
|
|
核心思路
这道题是经典的图遍历问题,要求统计二维网格中岛屿的数量。岛屿由相邻的陆地组成,相邻指的是水平方向(左右)和垂直方向(上下)的相邻。我们可以通过 深度优先搜索(DFS) 或 广度优先搜索(BFS) 来遍历每个岛屿,并在遍历过程中将访问过的陆地标记为已访问,以避免重复计数。
核心步骤:
- 遍历网格:从头到尾遍历二维网格中的每一个格子。
- 发现岛屿:如果当前格子是陆地(
'1'
),则开始遍历整个岛屿,并将岛屿的数量加1
。 - 标记陆地:在遍历过程中,将访问过的陆地标记为
'0'
,以避免重复访问。 - 返回结果:最终返回岛屿的总数。
为什么使用 DFS 或 BFS?
DFS 和 BFS 都可以用来遍历矩阵中的连通区域(岛屿),区别在于遍历的顺序:
- DFS 以递归的方式深入探索一个方向,直到无法继续为止,然后回溯到上一个节点继续探索。
- BFS 使用队列按层次扩展,逐层遍历相邻的节点。
方法1:深度优先搜索(DFS)
|
|
时间复杂度分析
- 时间复杂度:
O(m * n)
,其中m
是网格的行数,n
是网格的列数。每个格子最多被访问一次。 - 空间复杂度:
O(m * n)
,最坏情况下(整个网格都是陆地),递归栈的深度为m * n
。
方法2:广度优先搜索(BFS)
|
|
时间复杂度分析
- 时间复杂度:
O(m * n)
,每个格子最多被访问一次。 - 空间复杂度:
O(min(m, n))
,队列的大小取决于网格的形状,在最坏情况下(网格为长条形),队列中最多存储min(m, n)
个格子。
易错点
- 越界检查:在进行 DFS 或 BFS 时,必须检查当前格子是否在网格范围内,否则会导致越界错误。
- 标记访问:在访问陆地后,必须将其标记为
'0'
,否则会重复计数。 - 初始条件:如果网格为空,需要直接返回
0
。
变体与应对方法
-
统计每个岛屿的面积:
- 问题描述:要求统计每个岛屿的面积(陆地数量)。
- 应对方法:在 DFS 或 BFS 中增加一个计数器,记录当前岛屿的面积。
-
最大岛屿面积:
- 问题描述:要求找到所有岛屿中面积最大的一个。
- 应对方法:在 DFS 或 BFS 中记录每个岛屿的面积,并更新最大面积。
-
边界岛屿:
- 问题描述:要求只统计位于网格边界的岛屿。
- 应对方法:在 DFS 或 BFS 中检查当前岛屿是否包含边界陆地。
-
含湖泊的岛屿:
- 问题描述:岛屿内部可能包含湖泊(被
'0'
包围的区域)。 - 应对方法:在统计岛屿面积时,需要排除湖泊区域。
- 问题描述:岛屿内部可能包含湖泊(被
搜索旋转排序数组:二分查找
题目描述
给定一个旋转排序数组 nums
和一个目标值 target
,请你在数组中搜索目标值。如果存在,返回它的下标;否则,返回 -1
。你可以假设数组中不存在重复的元素。
示例:
|
|
核心思路
由于数组是部分有序的,我们可以先找到旋转点(即数组中的最大值,也是第二部分的最小值的左侧),然后根据目标值与旋转点的关系,将搜索范围缩小到其中一个有序部分,再进行二分查找。
- 找到旋转点:
- 使用二分查找找到旋转点(最大值的位置)。
- 旋转点将数组分为两个有序部分。
- 确定搜索范围:
- 如果目标值在第一部分(大于等于
nums[0]
),则在[0, 旋转点]
中搜索。 - 否则,在
[旋转点 + 1, len(nums) - 1]
中搜索。
- 如果目标值在第一部分(大于等于
- 二分查找:
- 在选定的范围内进行标准的二分查找。
袁师傅二分查找秘诀
- 性质决定前后
- 二分的意思是我们可以用一个性质把这个数组分成两份,做题的时候你要想一下,你是要用前面的性质还是后面的性质
- 选择前面的性质,找的就是满足前面性质的最后一个元素
- 选择后面的性质,找的就是满足后面性质的第一个元素
- 中点选择异侧点
- 在中点的选取上,要选择与性质侧不同的一边,防止死循环
- 选择前面的性质,
mid = (left + right + 1) // 2
- 选择后面的性质,
mid = (left + right) // 2
- 命中选择移动同侧边
- 中点一旦命中性质,则把性质所在一边的边界移动到中点处
- 未命中则选择移动异侧边
- 相反,没命中就移动另一侧的边界
- 另外,因为没命中,中点的可能性已经排除,要额外移动一格
- 如果性质在左侧,那就是
r = mid - 1
- 如果性质在右侧,那就是
l = mid + 1
代码实现:二分查找
|
|
时空复杂度分析
- 时间复杂度:
O(log n)
,其中n
是数组的长度。我们通过两次二分查找分别找到了旋转点和目标值。 - 空间复杂度:
O(1)
,只使用了常数级别的额外空间。
易错点
-
计算中点时的向上取整:
- 在第一个二分查找中,计算中点时需要向上取整(
mid = (left + right + 1) // 2
),否则会陷入死循环。
- 在第一个二分查找中,计算中点时需要向上取整(
-
旋转点的定义:
- 旋转点是数组中的最大值,而不是最小值。它位于第二部分最小值的左侧。
-
搜索范围的选择:
- 需要根据目标值与
nums[0]
和旋转点的值确定搜索范围。
- 需要根据目标值与
-
边界条件:
- 如果数组只有一个元素,需要单独处理。
全排列
题目描述
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
核心逻辑
-
回溯框架:
dfs(path, visit)
是递归函数,path
记录当前的路径(排列),visit
是一个布尔数组,记录哪些元素已经被选择。- 如果
path
的长度等于nums
的长度,说明path
是一个完整的排列,将其添加到result
中。
-
遍历选择:
- 遍历
nums
中的每一个元素nums[i]
。 - 如果
visit[i]
为True
,表示该元素已经被选择过,跳过。 - 如果
visit[i]
为False
,则选择该元素:- 将
visit[i]
标记为True
,表示该元素被选择。 - 将
nums[i]
加入到path
中。 - 递归调用
dfs
,继续搜索。 - 回溯:撤销选择,将
path
中的最后一个元素移除,并将visit[i]
标记为False
。
- 将
- 遍历
-
初始化调用:
- 调用
dfs([], [False]*len(nums))
,从空路径和未访问的状态开始搜索。
- 调用
代码实现:回溯
|
|
袁师傅回溯框架
|
|
复杂度分析
-
时间复杂度:
- 全排列的数量是
O(n!)
,其中n
是nums
的长度。 - 每次生成一个排列时,需要
O(n)
的时间复制path
。 - 总时间复杂度为
O(n! * n)
。
- 全排列的数量是
-
空间复杂度:
- 递归调用栈的深度为
O(n)
。 - 存储所有排列的空间复杂度为
O(n! * n)
。
- 递归调用栈的深度为
易错点
-
未使用副本:
- 如果不使用
path[:]
,直接将path
添加到result
中,最终所有排列都会指向同一个path
,导致结果错误。
- 如果不使用
-
未撤销选择:
- 在回溯过程中,必须通过
path.pop()
和visit[i] = False
撤销选择,否则会导致path
不断增长,visit
状态错误。
- 在回溯过程中,必须通过
-
重复选择:
- 通过
visit[i]
确保每个元素只被选择一次,避免重复选择。
- 通过
变体与应对方法
1. 包含重复元素的排列
如果 nums
中包含重复元素,直接使用上述代码会生成重复的排列。需要对其进行去重。
解决方法:
- 先对
nums
排序,然后在回溯时跳过重复元素。
|
|
2. 组合问题
如果要求组合而不是排列,可以在回溯过程中限制选择的范围,避免重复组合。
解决方法:
- 在递归调用时,通过
start
参数限制选择范围。
|
|
3. 子集问题
如果要求所有子集,可以在每次递归调用时将当前 path
添加到 result
中。
解决方法:
- 在递归调用前直接将
path
添加到result
中。
|
|
买卖股票的最佳时机
题目描述
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
方法:Kadane算法
|
|
时间复杂度分析
- 时间复杂度:
O(n)
,其中n
是prices
的长度。代码只遍历了一次价格数组。 - 空间复杂度:
O(1)
,只使用了常数级别的额外空间(min_price
和max_profit
)。
合并两个有序数组
题目描述
给你两个有序整数数组 nums1
和 nums2
,将 nums2
合并到 nums1
中,使 nums1
成为一个有序数组。nums1
的初始长度为 m + n
,其中 m
是 nums1
的有效元素数量,n
是 nums2
的长度。
代码实现:倒序遍历
|
|
代码解析
核心逻辑
-
初始化:
k
初始化为m + n - 1
,表示nums1
的最后一个位置。
-
从后向前遍历:
- 比较
nums1[m - 1]
和nums2[n - 1]
,将较大的元素放入nums1[k]
。 - 移动相应的指针(
k
、m
或n
)。 - 重复上述过程,直到
m
或n
为0
。
- 比较
-
处理剩余元素:
- 如果
nums2
还有剩余元素(n > 0
),将其直接复制到nums1
的前面。
- 如果
时空复杂度分析
- 时间复杂度:
O(m + n)
,其中m
是nums1
的有效元素数量,n
是nums2
的长度。代码只遍历了一次nums1
和nums2
。 - 空间复杂度:
O(1)
,没有使用额外的空间。
易错点
-
指针移动:
- 在比较
nums1[m - 1]
和nums2[n - 1]
后,必须正确移动相应的指针(k
、m
、n
)。
- 在比较
-
剩余元素处理:
- 如果
nums2
还有剩余元素,必须将其复制到nums1
的前面。
- 如果
-
边界条件:
- 如果
m = 0
,直接将nums2
复制到nums1
。 - 如果
n = 0
,nums1
已经是最终结果。
- 如果
变体与应对方法
1. 合并多个有序数组
合并 k
个有序数组。
解决方法:
- 使用堆(优先队列)来维护当前所有数组的最小值。
|
|
2. 合并两个无序数组
合并两个无序数组,并将结果排序。
解决方法:
- 先将两个数组合并,然后使用排序算法(如
sorted()
或sort()
)。
|
|
3. 原地合并两个有序链表
合并两个有序链表,并将结果存储在第一个链表中。
解决方法:
- 使用指针从头到尾合并链表。
|
|
括号匹配
题目描述
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
代码实现:栈
|
|
代码解析
- 初始化:
r2l
是一个字典,用于存储右括号到左括号的映射。stk
是一个栈,用于存储左括号。
- 遍历字符串:
- 对于每个字符
s[i]
:- 如果
s[i]
是右括号:- 检查栈顶是否是对应的左括号(
stk[-1] == r2l[s[i]]
)。 - 如果匹配成功,弹出栈顶元素(
stk.pop()
)。 - 如果匹配失败,直接返回
False
。
- 检查栈顶是否是对应的左括号(
- 如果
s[i]
是左括号,将其压入栈中(stk.append(s[i])
)。
- 如果
- 对于每个字符
- 最终检查:
- 如果栈为空,说明所有括号都匹配成功,返回
True
。 - 否则,返回
False
。
- 如果栈为空,说明所有括号都匹配成功,返回
时空复杂度分析
- 时间复杂度:
O(n)
,其中n
是字符串的长度。代码只遍历了一次字符串,并且栈操作的时间复杂度为O(1)
。 - 空间复杂度:
O(n)
,在最坏情况下(例如全是左括号),栈的大小为n
。
易错点
- 括号配对错误:
- 必须确保右括号和栈顶的左括号是同一类型,否则直接返回
False
。
- 必须确保右括号和栈顶的左括号是同一类型,否则直接返回
- 栈为空情况:
- 当遇到右括号时,如果栈为空,直接返回
False
,因为没有左括号与之匹配。
- 当遇到右括号时,如果栈为空,直接返回
- 最终栈为空:
- 遍历结束后,必须检查栈是否为空,如果栈不为空,说明有未匹配的左括号,返回
False
- 遍历结束后,必须检查栈是否为空,如果栈不为空,说明有未匹配的左括号,返回
二叉树的最近公共祖先
题目描述
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
代码实现:递归
|
|
代码解析
-
递归终止条件:
- 如果
root
为空(即not root
),或者root
等于p
或q
,直接返回root
。这里:- 如果
root
为空,表示当前分支没有找到p
或q
。 - 如果
root == p
或root == q
,表示已经找到了其中一个目标节点,返回root
。
- 如果
- 如果
-
递归左子树:
- 在左子树中递归查找
p
和q
,结果存储在left
中。
- 在左子树中递归查找
-
递归右子树:
- 在右子树中递归查找
p
和q
,结果存储在right
中。
- 在右子树中递归查找
-
判断 LCA:
- 如果
left
和right
都不为空,说明p
和q
分别位于当前节点的左右子树中,因此当前节点root
是 LCA。 - 如果只有
left
不为空,说明p
和q
都在左子树中,返回left
。 - 如果只有
right
不为空,说明p
和q
都在右子树中,返回right
。 - 如果
left
和right
都为空,说明当前子树中没有找到p
或q
,返回None
。
- 如果
时空复杂度分析
- 时间复杂度:
O(n)
,其中n
是二叉树的节点数。代码需要遍历所有节点。 - 空间复杂度:
O(h)
,其中h
是二叉树的高度。递归调用栈的深度取决于树的高度:- 对于平衡二叉树,
h = log n
。 - 对于最坏情况(例如树退化为链表),
h = n
。
- 对于平衡二叉树,
链表成环
题目描述
给你一个链表的头节点 head
,判断链表中是否有环。
实现:快慢指针
|
|