题目 |
简述、方案、死人坑 |
环形链表 II |
两次起始都设置成head ,快慢指针快慢走,遇到以后把一个放回到head ,然后快慢指针齐步走,相遇点就是入口 |
编辑距离 |
删除操作dp[i-1][j]+1 :删除 word1[i] ; 等价于在 word2[j] 后面添加 word1[i] 加入操作dp[i][j-1]+1 :在 word1[i] 后面插入word2[j] ;等价于删除word2[j] 替换操作dp[i][j] = dp[i-1][j-1] + 1 :将 word1 的第 i 个字符替换为 word2 的第 j 个字符 |
二叉树中的最大路径和 |
左右子树必须拒绝负数路径
pathSum 函数定义:以当前节点为根节点的子树的最大路径和 全局用 左 + 右 + 中 返回值用 左 / 右 + 中 |
复原 IP 地址 |
请严格按照以下顺序递归: idx == len(word) and step == 4 idx == len(word) or step == 4 word[idx] == "0"
for i in range(idx, len(word)) ,添加if sum > 255
|
最长公共子序列 |
dp[i][j] = dp[i-1][j-1] + 1
dp[i][j] = max(dp[i-1][j], dp[i][j-1]) |
删除链表的倒数第 N 个结点 |
初始在dummy ,拉开n ,循环条件fast.next |
删除排序链表中的重复元素 II |
slow 指向链表中 最后一个确认不重复的节点,用于更新链表。
fast 用于跳过所有与 slow.next 值相同的节点,停在下一个不重复的节点。
while fast and fast.val == slow.next.val |
寻找两个正序数组的中位数 |
ii = min(len(nums1), i + k // 2)
elif len(nums1) == i: return nums2[j + k - 1]
elif k == 1: return min(nums1[i], nums2[j])
if nums1[ii - 1] < nums2[jj - 1] 这里一定要注意下标的问题,k==1 的时候没有减一,其余时候基本都有 |
二叉树的右视图 |
层序遍历 |
二叉树的中序遍历 |
dfs |
环形链表 II
环形链表 II
题目描述
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
方法:快慢指针+齐步走
先快慢指针判断成环,如果成环则把一个指针立刻放回到起始指针,两个同步走便会在环人口相遇。注意,这里的起始指针可以是头指针也可以是哨兵指针,但是前后必须一致!!!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 初始化快慢指针,都指向链表头部
fast, slow = head, head
# 使用快慢指针遍历链表
while fast and fast.next:
fast = fast.next.next # 快指针每次走两步
slow = slow.next # 慢指针每次走一步
# 如果快慢指针相遇,说明链表中有环
if fast == slow:
# 将慢指针重新指向链表头部
slow = head
# 快慢指针每次各走一步,直到它们再次相遇
while fast != slow:
fast = fast.next # 快指针每次走一步
slow = slow.next # 慢指针每次走一步
# 相遇的节点就是环的入口节点
return slow
# 如果遍历结束都没有相遇,说明链表中没有环,返回None
return None
|
时间复杂度分析:
-
第一阶段:检测环是否存在
- 快指针每次走两步,慢指针每次走一步。
- 如果链表中有环,快慢指针会在环内相遇。
- 假设链表的非环部分长度为 𝑎,环的长度为 𝑏,则最多需要 𝑎+𝑏 步就可以找到相遇点。
- 时间复杂度为 𝑂(𝑎+𝑏)。
-
第二阶段:寻找环的入口节点
- 将慢指针重新指向链表头部,然后快慢指针每次各走一步。
- 它们会在环的入口节点相遇。
- 这一阶段的时间复杂度为 𝑂(𝑎)。
总时间复杂度:
𝑂(𝑎+𝑏),其中 𝑎 是非环部分的长度,𝑏 是环的长度。
编辑距离
72. 编辑距离
题目描述
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
编辑距离致命点
dp
定义:dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符所需的最小操作数。
- 删除操作
dp[i-1][j]+1
:删除 word1[i]
; 等价于在 word2[j]
后面添加 word1[i]
- 加入操作
dp[i][j-1]+1
:在 word1[i]
后面插入word2[j]
;等价于删除word2[j]
- 替换操作
dp[i][j] = dp[i-1][j-1] + 1
:将 word1
的第 i
个字符替换为 word2
的第 j
个字符
- 删除、插入、替换操作都是针对
word1
进行的,每种操作的目标是使 word1
逐渐与 word2
匹配。
- 初始化要设立边界,也就是要
m+1
和n+1
,在初始化和遍历的时候必须注意
- 初始化中的值复制和复制,这个很容易错误,内循环可以用
*
复制,因为是值复制,不会一动全动;外循环必须用for-range
,因为如果用了*
会变成引用复制,一动全动
- 初始化中的顺序先列后行:内循环是列,外循环是行
方法:动态规划
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
# 初始化 dp 表,dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最小操作数
dp = [[0] * (n + 1) for _ in range(m + 1)] # 正确的初始化方式
# 初始化第一列:将 word1 的前 i 个字符转换为空字符串需要 i 次删除操作
for i in range(m + 1):
dp[i][0] = i
# 初始化第一行:将空字符串转换为 word2 的前 j 个字符需要 j 次插入操作
for j in range(n + 1):
dp[0][j] = j
# 填充 dp 表
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
# 如果当前字符相等,则不需要操作,直接继承 dp[i-1][j-1]
dp[i][j] = dp[i - 1][j - 1]
else:
# 如果当前字符不相等,则取以下三种操作的最小值:
# 1. 删除:dp[i-1][j] + 1
# 2. 插入:dp[i][j-1] + 1
# 3. 替换:dp[i-1][j-1] + 1
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
# 返回 dp[m][n],即将 word1 转换为 word2 所需的最小操作数
return dp[m][n]
|
时间复杂度与空间复杂度
复杂度类型 |
未优化 |
优化后 |
时间 |
𝑂(𝑚×𝑛) |
同左 |
空间 |
𝑂(𝑚×𝑛) |
𝑂(𝑛) |
二叉树中的最大路径和
124. 二叉树中的最大路径和
题目描述
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
最大路径和致命点
pathSum
函数定义:以当前节点为根节点的子树的最大路径和
res
全局最大路径和需要初始化为最小值
- 全局最大值等于所有局部最大值的最大值
- 左右子树必须拒绝负数路径:
left = max(0, self.pathSum(root.left))
方法:递归遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
class Solution:
def __init__(self):
# 初始化结果变量,用于记录最大路径和
# 初始值设为负无穷,确保任何路径和都比它大
self.res = -float('inf')
def pathSum(self, root):
"""
递归计算以当前节点为根节点的子树的最大路径和
:param root: 当前节点
:return: 返回以当前节点为起点的最大路径和
"""
# 如果当前节点为空,返回0(空节点对路径和无贡献)
if not root:
return 0
# 递归计算左子树的最大路径和,如果为负则取0(舍弃负数路径)
left = max(0, self.pathSum(root.left))
# 递归计算右子树的最大路径和,如果为负则取0(舍弃负数路径)
right = max(0, self.pathSum(root.right))
# 更新全局最大路径和:当前节点值 + 左子树最大路径和 + 右子树最大路径和
self.res = max(self.res, left + right + root.val)
# 返回以当前节点为起点的最大路径和:当前节点值 + 左子树或右子树中的最大值
return max(left, right) + root.val
def maxPathSum(self, root: Optional[TreeNode]) -> int:
"""
计算二叉树中的最大路径和
:param root: 二叉树的根节点
:return: 返回最大路径和
"""
# 调用递归函数计算最大路径和
self.pathSum(root)
# 返回全局最大路径和
return self.res
|
时空复杂度
- 时间复杂度:𝑂(𝑁)O(N),其中 𝑁N 是节点数。
- 空间复杂度:𝑂(𝐻)O(H),其中 𝐻H 是树的高度。
复原IP地址
93. 复原 IP 地址
题目描述
有效 IP 地址 正好由四个整数(每个整数位于 0
到 255
之间组成,且不能含有前导 0
),整数之间用 '.'
分隔。
- 例如:
"0.1.2.201"
和 "192.168.1.1"
是 有效 IP 地址,但是 "0.011.255.245"
、"192.168.1.312"
和 "192.168@1.1"
是 无效 IP 地址。
给定一个只包含数字的字符串 s
,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s
中插入 '.'
来形成。你 不能 重新排序或删除 s
中的任何数字。你可以按 任何 顺序返回答案。
复原IP地址致命点
- 参数:
word, ip, idx, step
- 最好的遍历方案:
idx == len(word) and step == 4
idx == len(word) or step == 4
word[idx] == "0"
for i in range(idx, len(word))
,添加if sum > 255
- 去除掉最后的
.
就行了
方案:回溯
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
class Solution:
def __init__(self):
# 初始化结果列表,用于存储所有合法的IP地址
self.res = []
def backtrack(self, word, ip, idx, step):
"""
回溯函数,用于生成所有可能的IP地址
:param word: 原始字符串
:param ip: 当前生成的IP地址部分
:param idx: 当前处理的字符索引
:param step: 当前已经生成的IP段数(0到4)
"""
# 如果已经处理完所有字符,并且生成了4段IP地址
if idx == len(word) and step == 4:
self.res.append(ip[:-1]) # 去掉最后一个点号,添加到结果列表
return
# 如果已经处理完所有字符,但未生成4段IP地址,或者已经生成4段IP地址,但未处理完所有字符,直接返回
elif idx == len(word) or step == 4:
return
# 如果当前字符是 "0",则当前段只能是 "0",不能有前导零
elif word[idx] == "0":
self.backtrack(word, ip + "0.", idx + 1, step + 1) # 递归处理下一段
return
else:
# 初始化当前段的数值
sum = 0
# 尝试在当前段中取1到3个字符
for i in range(idx, len(word)):
sum = sum * 10 + int(word[i]) # 计算当前段的数值
# 如果当前段的数值超过255,直接退出循环
if sum > 255:
break
# 递归处理下一段
self.backtrack(word, ip + str(sum) + ".", i + 1, step + 1)
def restoreIpAddresses(self, s: str) -> List[str]:
"""
主函数,调用回溯函数生成所有合法的IP地址
:param s: 原始字符串
:return: 所有合法的IP地址列表
"""
self.backtrack(s, "", 0, 0) # 从第0个字符开始回溯
return self.res
|
时间复杂度总结
分析角度 |
时间复杂度 |
解释 |
组合数学 |
𝑂(𝐶(𝑛−1,3))O(C(n−1,3)) |
从 𝑛−1n−1 个间隔中挑选 3 个断点,组合数为 (𝑛−1)(𝑛−2)(𝑛−3)66(n−1)(n−2)(n−3)。 |
回溯算法 |
𝑂(34)=𝑂(81)O(34)=O(81) |
每段有最多 3 种选择,总共有 4 段。 |
实际运行 |
接近 𝑂(𝐶(𝑛−1,3))O(C(n−1,3)) |
由于剪枝优化,实际运行时间接近组合数,但不会达到理论最大值。 |
最长公共子序列
1143. 最长公共子序列
题目描述
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是 "abcde"
的子序列,但 "aec"
不是 "abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
方法:动态规划
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
# 初始化 (m+1) x (n+1) 的动态规划表,dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的最长公共子序列长度
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 遍历 text1 和 text2 的每个字符
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
# 如果当前字符相等,则 LCS 长度增加1
dp[i][j] = dp[i - 1][j - 1] + 1
else:
# 如果当前字符不等,则取上方或左方的最大值
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# 返回 text1 和 text2 的最长公共子序列长度
return dp[m][n]
|
删除链表的倒数第 N 个结点
19. 删除链表的倒数第 N 个结点
题目描述
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
方案:快慢指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class Solution:
def removeNthFromEnd(self, head, n) -> Optional[ListNode]:
# 创建一个虚拟头节点,简化边界情况处理(如删除的是第一个节点)
dummy = ListNode(next=head)
# 初始化两个指针 slow 和 fast,都指向虚拟头节点
slow, fast = dummy, dummy
# 让 fast 指针先向前移动 n 步
for i in range(n):
fast = fast.next
# 同时移动 slow 和 fast 指针,直到 fast 指针到达链表末尾
while fast.next:
slow = slow.next
fast = fast.next
# 此时 slow 指针指向待删除节点的前一个节点
# 将 slow.next 指向 slow.next.next,跳过待删除节点
slow.next = slow.next.next
# 返回链表的新头节点(即 dummy.next)
return dummy.next
|
易错点
我们要找到的是它的前一个节点,这样才能删除它,核心就是while fast.next
让fast
停在最后一个,这样slow
就会停在倒数第n+1
的地方
删除排序链表中的重复元素 II
82. 删除排序链表中的重复元素 II
题目描述
给定一个已排序的链表的头 head
, 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
方法:很微妙的一种双指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class Solution:
def deleteDuplicates(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 创建一个哑节点(dummy node),其 next 指向链表头节点
# 哑节点的作用是简化边界条件的处理(例如删除头节点的情况)
dummy = ListNode(next=head)
# 初始化两个指针:
# - slow:指向哑节点,用于构建新链表
# - fast:指向链表头节点,用于遍历链表
slow, fast = dummy, head
# 遍历链表
while fast:
# 如果当前 fast 节点与 slow 的下一个节点值相等,说明存在重复节点
# 继续移动 fast 指针,直到找到不重复的节点或链表末尾
while fast and fast.val == slow.next.val:
fast = fast.next
# 如果 fast 指针移动后,fast == slow.next.next
# 说明 slow.next 节点没有重复,直接将 slow 移动到下一个节点
if fast == slow.next.next:
slow = slow.next
else:
# 否则,说明 slow.next 节点有重复,直接跳过所有重复节点
slow.next = fast
# 返回新链表的头节点
return dummy.next
|
slow
和 fast
的含义
指针 |
含义 |
slow |
指向链表中 最后一个确认不重复的节点,用于更新链表。 |
fast |
用于跳过所有与 slow.next 值相同的节点,停在下一个不重复的节点。 |
寻找两个正序数组的中位数
4. 寻找两个正序数组的中位数
题目描述
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
方法:递归,O(log(m+n))
看了就会,不看就不会
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
def findKthNumber(self, nums1, nums2, i, j, k):
"""
用来找到两个数组的第k个元素(从1开始)
:param nums1: 第一个有序数组
:param nums2: 第二个有序数组
:param i: nums1 尚未遍历的坐标起始,包含i
:param j: nums2 尚未遍历的坐标起始,包含j
:param k: 目标找的第k个元素,从1开始
"""
# 保证 nums1 的剩余长度最小,这样可以减少递归的次数
if len(nums1) - i > len(nums2) - j:
return self.findKthNumber(nums2, nums1, j, i, k)
# 如果 nums1 的剩余长度为 0,直接在 nums2 中找第k个元素
elif len(nums1) == i:
return nums2[j + k - 1]
# 如果 k == 1,直接返回两个数组当前起始元素的最小值
elif k == 1:
return min(nums1[i], nums2[j])
# 计算 nums1 和 nums2 中要比较的位置
# ii 和 jj 分别是 nums1 和 nums2 中可能的第 k//2 个元素
ii = min(len(nums1), i + k // 2) # 保证 ii 不超过 nums1 的长度
jj = min(len(nums2), j + k // 2) # 保证 jj 不超过 nums2 的长度
# 比较 nums1[ii-1] 和 nums2[jj-1]
if nums1[ii - 1] < nums2[jj - 1]:
# 如果 nums1[ii-1] 比 nums2[jj-1] 小
# 说明 nums1 的前半部分不可能是第k个元素
# 因此,我们可以在 nums1 的剩余部分 (从 ii 开始)
# 和 nums2 中找第 k - (ii - i) 个元素
return self.findKthNumber(nums1, nums2, ii, j, k - (ii - i))
else:
# 如果 nums2[jj-1] 比 nums1[ii-1] 小
# 说明 nums2 的前半部分不可能是第k个元素
# 因此,我们可以在 nums2 的剩余部分 (从 jj 开始)
# 和 nums1 中找第 k - (jj - j) 个元素
return self.findKthNumber(nums1, nums2, i, jj, k - (jj - j))
|
二叉树的右视图
199. 二叉树的右视图
题目描述
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
方法:二叉树层序遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
"""
返回二叉树的右视图
:param root: 二叉树的根节点
:return: 右视图的节点值列表
"""
# 如果根节点为空,直接返回空列表
if not root:
return []
# 结果列表:存储右视图的节点值
res = []
# 使用队列进行层次遍历(BFS)
q = deque([root])
# 进行层次遍历
while q:
# 当前层的节点数量
size = len(q)
# 遍历当前层的所有节点
for i in range(size):
# 从队列左侧取出节点
node = q.popleft()
# 如果当前节点是当前层的最后一个节点,将其值加入结果列表
if i + 1 == size:
res.append(node.val)
# 将当前节点的左孩子加入队列
if node.left:
q.append(node.left)
# 将当前节点的右孩子加入队列
if node.right:
q.append(node.right)
# 返回结果列表
return res
|
复杂度分析
-
时间复杂度:
- 每个节点只会被访问一次,因此时间复杂度为 𝑂(𝑛),其中
n
是二叉树的节点数。
-
空间复杂度:
- 使用队列存储每一层的节点,最坏情况下队列的大小为二叉树的最大宽度。
- 对于完全二叉树,最大宽度约为 𝑛/2,因此空间复杂度为 𝑂(𝑛)。
- 对于最不平衡的二叉树(例如链表结构),最大宽度为 11,因此空间复杂度为 𝑂(1)。
- 平均空间复杂度为 𝑂(𝑛)。
二叉树中序遍历
[94. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/
题目描述
给定一个二叉树的根节点 root
,返回 它的 中序 遍历 。
方案:中序遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
"""
返回二叉树的中序遍历结果
:param root: 二叉树的根节点
:return: 中序遍历的节点值列表
"""
# 初始化结果列表
self.res = []
# 定义递归函数进行中序遍历
def dfs(root):
# 如果当前节点为空,直接返回
if not root:
return
# 递归遍历左子树
dfs(root.left)
# 将当前节点的值加入结果列表
self.res.append(root.val)
# 递归遍历右子树
dfs(root.right)
# 从根节点开始递归遍历
dfs(root)
# 返回结果列表
return self.res
|