返回
Featured image of post CodeTop - Top 40

CodeTop - Top 40

最硬核的10道题

题目 简述、方案、死人坑
环形链表 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

时间复杂度分析

  1. 第一阶段:检测环是否存在

    • 快指针每次走两步,慢指针每次走一步。
    • 如果链表中有环,快慢指针会在环内相遇。
    • 假设链表的非环部分长度为 𝑎,环的长度为 𝑏,则最多需要 𝑎+𝑏 步就可以找到相遇点。
    • 时间复杂度为 𝑂(𝑎+𝑏)。
  2. 第二阶段:寻找环的入口节点

    • 将慢指针重新指向链表头部,然后快慢指针每次各走一步。
    • 它们会在环的入口节点相遇。
    • 这一阶段的时间复杂度为 𝑂(𝑎)。

总时间复杂度:
𝑂(𝑎+𝑏),其中 𝑎 是非环部分的长度,𝑏 是环的长度。


编辑距离

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+1n+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.nextfast停在最后一个,这样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

复杂度分析

  1. 时间复杂度

    • 每个节点只会被访问一次,因此时间复杂度为 𝑂(𝑛),其中 n 是二叉树的节点数。
  2. 空间复杂度

    • 使用队列存储每一层的节点,最坏情况下队列的大小为二叉树的最大宽度。
    • 对于完全二叉树,最大宽度约为 𝑛/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
Licensed under CC BY-NC-SA 4.0
© 2023 - 2025 壹壹贰捌· 0Days
共书写了265.7k字·共 93篇文章 京ICP备2023035941号-1