返回
Featured image of post CodeTop - Top 20

CodeTop - Top 20

这十道题反而没有那么难

题目 简述、方案、死人坑
二叉树的层序遍历 BFS比较好,这里我们要用python里面的dequepopleft,另外记得不能用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)
合并两个有序数组 这里逆序即可,其他的和合并两个有序链表一样
有效的括号 栈,用栈来实现就行了
环形链表 快慢指针即可,如果找找环入口,就相遇之后齐步走,就能遇到

二叉树层序遍历

102. 二叉树的层序遍历

题目描述

给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。

1
2
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

方法:双端队列 deque

  1. 初始化队列:将根节点放入队列中。
  2. 逐层遍历
    • 在每一层开始时,记录当前层的节点数量(即队列的长度)。
    • 遍历当前层的所有节点,将其值加入结果列表,并将其子节点(左子节点和右子节点)加入队列。
    • 重复上述步骤,直到队列为空。
  3. 返回结果:最终返回按层存储的结果列表。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from collections import deque

class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        result = []  # 用于存储最终的结果
        if root is None:  # 如果根节点为空,直接返回空列表
            return []
        else:
            queue = deque([root])  # 初始化队列,将根节点加入队列

        while len(queue) > 0:  # 当队列不为空时,继续遍历
            level = []  # 用于存储当前层的节点值
            level_len = len(queue)  # 记录当前层的节点数量
            for _ in range(level_len):  # 遍历当前层的所有节点
                node = queue.popleft()  # 从队列中取出一个节点
                if node.left:  # 如果该节点有左子节点,将左子节点加入队列
                    queue.append(node.left)
                if node.right:  # 如果该节点有右子节点,将右子节点加入队列
                    queue.append(node.right)
                level.append(node.val)  # 将当前节点的值加入当前层的列表
            result.append(level)  # 将当前层的列表加入结果列表
      
        return result

时间复杂度分析

  • 时间复杂度O(n),其中 n 是二叉树的节点总数。每个节点会被访问一次,并且每个节点的入队和出队操作的时间复杂度都是 O(1)
  • 空间复杂度O(n),队列中最多会存储一层的节点数。在最坏情况下(完全二叉树),最后一层的节点数为 O(n/2),因此空间复杂度为 O(n)

易错点

  1. 根节点为空:如果根节点为空,需要直接返回空列表,而不是继续遍历。
  2. 队列的初始化:队列的初始化应该以列表形式传入根节点,而不是直接传入根节点。
  3. 当前层的节点数量:需要在每一层遍历之前记录当前层的节点数量,否则队列的长度会动态变化,导致无法正确划分层次。
  4. 结果列表的存储:每一层的节点值需要单独存储为一个列表,然后加入结果列表中

变体与应对方法

  1. 自底向上的层序遍历

    • 问题描述:要求从最后一层开始,逐层向上遍历。
    • 应对方法:在遍历过程中,将每一层的结果插入到结果列表的头部(例如使用 result.insert(0, level)),或者最后将结果列表反转(例如使用 result.reverse())。
  2. 锯齿形层序遍历

    • 问题描述:要求一层从左到右,下一层从右到左,交替进行。
    • 应对方法:在遍历每一层时,判断当前层的奇偶性(例如通过 len(result) % 2),如果为奇数,则反转当前层的节点值。
  3. 层序遍历的节点深度

    • 问题描述:要求返回每个节点所在的层数(深度)。
    • 应对方法:在遍历时,记录每个节点的深度信息,并在结果中存储深度值。

两数之和

1. 两数之和

题目描述

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例:

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

核心思路

这道题是经典的数组查找问题,要求在数组中找到两个数,使得它们的和等于目标值。为了高效地解决这个问题,我们可以使用 哈希表(字典) 来存储已经遍历过的元素及其下标,通过查找哈希表来判断是否存在满足条件的数对。

实现步骤

  1. 初始化哈希表:创建一个空字典,用于存储数组元素及其下标。
  2. 遍历数组
    • 对于当前元素 nums[i],计算目标值与当前元素的差值 target - nums[i]
    • 如果差值在哈希表中存在,说明已经找到满足条件的数对,返回它们的下标。
    • 如果差值不存在,将当前元素及其下标存储在哈希表中,继续遍历。
  3. 返回结果:如果遍历结束后仍未找到满足条件的数对,返回 [-1, -1](根据题目描述,通常可以确保有解,因此这一步不会被执行)。

为什么使用哈希表?

哈希表的查找和插入操作的时间复杂度均为 O(1),因此可以极大地提高效率,避免双重循环导致的 O(n^2) 时间复杂度。

代码实现:哈希表

1
2
3
4
5
6
7
8
class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        dict = {}  # 初始化哈希表,用于存储元素及其下标
        for i in range(len(nums)):  # 遍历数组
            if target - nums[i] in dict:  # 检查差值是否存在于哈希表中
                return [dict[target - nums[i]], i]  # 如果存在,返回两个下标
            dict[nums[i]] = i  # 如果不存在,将当前元素及其下标存入哈希表
        return [-1, -1]  # 如果没有找到满足条件的数对,返回 [-1, -1]

时空复杂度分析

  • 时间复杂度O(n),其中 n 是数组的长度。我们只需要遍历数组一次,每次查找哈希表的时间复杂度为 O(1)
  • 空间复杂度O(n),哈希表最多需要存储 n 个元素。

易错点

  1. 哈希表的初始化:哈希表需要提前初始化为空字典,而不是在循环中动态创建。
  2. 差值的计算:在查找哈希表时,需要注意计算的是 target - nums[i],而不是反过来。
  3. 返回值的顺序:如果差值存在于哈希表中,返回的下标顺序应为 [哈希表中的下标, 当前下标]
  4. 边界条件:如果数组为空或只包含一个元素,需要额外处理(但根据题目描述,这种情况不会出现)。

变体与应对方法

  1. 多个解的情况

    • 问题描述:数组中存在多个满足条件的数对,要求返回所有的数对。
    • 应对方法:将满足条件的数对存储在一个列表中,最终返回该列表。
  2. 包含重复元素的情况

    • 问题描述:数组中可能包含重复元素,要求返回唯一的数对。
    • 应对方法:使用集合存储已经找到的数对,确保结果唯一。
  3. 和为 target 的子数组

    • 问题描述:要求找到一个连续的子数组,其和等于 target
    • 应对方法:使用前缀和和哈希表来解决,具体可以参考 560. 和为K的子数组

岛屿数量:DFS/BFS

200. 岛屿数量

题目描述

给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。

示例:

1
2
3
4
5
6
7
输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

核心思路

这道题是经典的图遍历问题,要求统计二维网格中岛屿的数量。岛屿由相邻的陆地组成,相邻指的是水平方向(左右)和垂直方向(上下)的相邻。我们可以通过 深度优先搜索(DFS) 或 广度优先搜索(BFS) 来遍历每个岛屿,并在遍历过程中将访问过的陆地标记为已访问,以避免重复计数。

核心步骤:

  1. 遍历网格:从头到尾遍历二维网格中的每一个格子。
  2. 发现岛屿:如果当前格子是陆地('1'),则开始遍历整个岛屿,并将岛屿的数量加 1
  3. 标记陆地:在遍历过程中,将访问过的陆地标记为 '0',以避免重复访问。
  4. 返回结果:最终返回岛屿的总数。

为什么使用 DFS 或 BFS?

DFS 和 BFS 都可以用来遍历矩阵中的连通区域(岛屿),区别在于遍历的顺序:

  • DFS 以递归的方式深入探索一个方向,直到无法继续为止,然后回溯到上一个节点继续探索。
  • BFS 使用队列按层次扩展,逐层遍历相邻的节点。

方法1:深度优先搜索(DFS)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        def dfs(i, j):
            # 如果当前格子超出边界或者不是陆地,则返回
            if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]) or grid[i][j] == '0':
                return
            # 将当前陆地标记为已访问
            grid[i][j] = '0'
            # 递归遍历上下左右四个方向的格子
            dfs(i + 1, j)
            dfs(i - 1, j)
            dfs(i, j + 1)
            dfs(i, j - 1)

        # 初始化岛屿数量
        count = 0
        # 遍历整个网格
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                # 如果发现陆地,则开始 DFS,并将岛屿数量加 1
                if grid[i][j] == '1':
                    dfs(i, j)
                    count += 1
        return count

时间复杂度分析

  • 时间复杂度O(m * n),其中 m 是网格的行数,n 是网格的列数。每个格子最多被访问一次。
  • 空间复杂度O(m * n),最坏情况下(整个网格都是陆地),递归栈的深度为 m * n

方法2:广度优先搜索(BFS)

 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
from collections import deque

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        def bfs(i, j):
            # 初始化队列
            queue = deque([(i, j)])
            # 标记当前陆地为已访问
            grid[i][j] = '0'
            # 遍历队列中的每一个格子
            while queue:
                x, y = queue.popleft()
                # 检查上下左右四个方向的格子
                for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
                    nx, ny = x + dx, y + dy
                    # 如果新格子是陆地,则加入队列并标记为已访问
                    if 0 <= nx < len(grid) and 0 <= ny < len(grid[0]) and grid[nx][ny] == '1':
                        grid[nx][ny] = '0'
                        queue.append((nx, ny))

        # 初始化岛屿数量
        count = 0
        # 遍历整个网格
        for i in range(len(grid)):
            for j in range(len(grid[0])):
                # 如果发现陆地,则开始 BFS,并将岛屿数量加 1
                if grid[i][j] == '1':
                    bfs(i, j)
                    count += 1
        return count

时间复杂度分析

  • 时间复杂度O(m * n),每个格子最多被访问一次。
  • 空间复杂度O(min(m, n)),队列的大小取决于网格的形状,在最坏情况下(网格为长条形),队列中最多存储 min(m, n) 个格子。

易错点

  1. 越界检查:在进行 DFS 或 BFS 时,必须检查当前格子是否在网格范围内,否则会导致越界错误。
  2. 标记访问:在访问陆地后,必须将其标记为 '0',否则会重复计数。
  3. 初始条件:如果网格为空,需要直接返回 0

变体与应对方法

  1. 统计每个岛屿的面积

    • 问题描述:要求统计每个岛屿的面积(陆地数量)。
    • 应对方法:在 DFS 或 BFS 中增加一个计数器,记录当前岛屿的面积。
  2. 最大岛屿面积

    • 问题描述:要求找到所有岛屿中面积最大的一个。
    • 应对方法:在 DFS 或 BFS 中记录每个岛屿的面积,并更新最大面积。
  3. 边界岛屿

    • 问题描述:要求只统计位于网格边界的岛屿。
    • 应对方法:在 DFS 或 BFS 中检查当前岛屿是否包含边界陆地。
  4. 含湖泊的岛屿

    • 问题描述:岛屿内部可能包含湖泊(被 '0' 包围的区域)。
    • 应对方法:在统计岛屿面积时,需要排除湖泊区域。

搜索旋转排序数组:二分查找

33. 搜索旋转排序数组

题目描述

给定一个旋转排序数组 nums 和一个目标值 target,请你在数组中搜索目标值。如果存在,返回它的下标;否则,返回 -1。你可以假设数组中不存在重复的元素。

示例:

1
2
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

核心思路

由于数组是部分有序的,我们可以先找到旋转点(即数组中的最大值,也是第二部分的最小值的左侧),然后根据目标值与旋转点的关系,将搜索范围缩小到其中一个有序部分,再进行二分查找。

  1. 找到旋转点
    • 使用二分查找找到旋转点(最大值的位置)。
    • 旋转点将数组分为两个有序部分。
  2. 确定搜索范围
    • 如果目标值在第一部分(大于等于 nums[0]),则在 [0, 旋转点] 中搜索。
    • 否则,在 [旋转点 + 1, len(nums) - 1] 中搜索。
  3. 二分查找
    • 在选定的范围内进行标准的二分查找。

袁师傅二分查找秘诀

  • 性质决定前后
    • 二分的意思是我们可以用一个性质把这个数组分成两份,做题的时候你要想一下,你是要用前面的性质还是后面的性质
    • 选择前面的性质,找的就是满足前面性质的最后一个元素
    • 选择后面的性质,找的就是满足后面性质的第一个元素
  • 中点选择异侧点
    • 在中点的选取上,要选择与性质侧不同的一边,防止死循环
    • 选择前面的性质,mid = (left + right + 1) // 2
    • 选择后面的性质,mid = (left + right) // 2
  • 命中选择移动同侧边
    • 中点一旦命中性质,则把性质所在一边的边界移动到中点处
  • 未命中则选择移动异侧边
    • 相反,没命中就移动另一侧的边界
    • 另外,因为没命中,中点的可能性已经排除,要额外移动一格
    • 如果性质在左侧,那就是r = mid - 1
    • 如果性质在右侧,那就是l = mid + 1

代码实现:二分查找

 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
class Solution:
    def search(self, nums: List[int], target: int) -> int:
        # 处理空数组的情况
        if not nums:
            return -1
      
        # 找到旋转点(最大值的位置)
        left, right = 0, len(nums) - 1
        while left < right:
            mid = (left + right + 1) // 2  # 向上取整,避免死循环
            if nums[mid] >= nums[0]:  # 旋转点在右侧
                left = mid
            else:
                right = mid - 1
      
        # 根据旋转点确定搜索范围
        if target >= nums[0] :  # 目标值在左侧有序部分
            left, right = 0, left
        elif l + 1 == len(nums): # 必须排除
	        return -1
        else:  # 目标值在右侧有序部分
            left, right = left + 1, len(nums) - 1
      
        # 在搜索范围内进行二分查找
        while left < right:
            mid = (left + right + 1) // 2
            if nums[mid] <= target:  # 目标值在右侧
                left = mid
            else:
                right = mid - 1
      
        # 检查是否找到目标值
        if nums[left] == target:
            return left
        else:
            return -1

时空复杂度分析

  • 时间复杂度O(log n),其中 n 是数组的长度。我们通过两次二分查找分别找到了旋转点和目标值。
  • 空间复杂度O(1),只使用了常数级别的额外空间。

易错点

  1. 计算中点时的向上取整

    • 在第一个二分查找中,计算中点时需要向上取整(mid = (left + right + 1) // 2),否则会陷入死循环。
  2. 旋转点的定义

    • 旋转点是数组中的最大值,而不是最小值。它位于第二部分最小值的左侧。
  3. 搜索范围的选择

    • 需要根据目标值与 nums[0] 和旋转点的值确定搜索范围。
  4. 边界条件

    • 如果数组只有一个元素,需要单独处理。

全排列

46. 全排列

题目描述

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

核心逻辑

  1. 回溯框架

    • dfs(path, visit) 是递归函数,path 记录当前的路径(排列),visit 是一个布尔数组,记录哪些元素已经被选择。
    • 如果 path 的长度等于 nums 的长度,说明 path 是一个完整的排列,将其添加到 result 中。
  2. 遍历选择

    • 遍历 nums 中的每一个元素 nums[i]
    • 如果 visit[i]True,表示该元素已经被选择过,跳过。
    • 如果 visit[i]False,则选择该元素:
      • visit[i] 标记为 True,表示该元素被选择。
      • nums[i] 加入到 path 中。
      • 递归调用 dfs,继续搜索。
      • 回溯:撤销选择,将 path 中的最后一个元素移除,并将 visit[i] 标记为 False
  3. 初始化调用

    • 调用 dfs([], [False]*len(nums)),从空路径和未访问的状态开始搜索。

代码实现:回溯

 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 permute(self, nums: List[int]) -> List[List[int]]:
        result = []

        def dfs(path, visit):
            # 如果当前路径是一个排列,添加到结果集
            if len(path) == len(nums):
                result.append(path[:])  # 添加 path 的副本
                return

            # 遍历所有可能的选项
            for i in range(len(nums)):
                if visit[i]:  # 如果该元素已经被选择过,跳过
                    continue
                visit[i] = True  # 标记该元素已被选择
                path.append(nums[i])  # 选择该元素
                dfs(path, visit)  # 递归搜索
                path.pop()  # 撤销选择
                visit[i] = False  # 标记该元素未被选择

        dfs([], [False] * len(nums))  # 从空路径开始搜索
        return result

袁师傅回溯框架

Leetcode 回溯问题

 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
45
46
47
48
49
50
51
52
from typing import List

def solveProblem(nums: List[int]) -> List[List[int]]:
    # 1. 预处理(排序/初始化)
    nums.sort()  # 如果需要去重,先排序
    res = []

    # 2. 定义回溯函数(使用闭包捕获外部变量)
    def backtrack(
        path: List[int],         # 当前路径:已做出的选择序列
        selection: List[int],    # 选择列表:当前可选的元素集合
        used: List[bool],        # 辅助参数:标记已使用的元素(根据问题调整)
        start: int               # 辅助参数:组合问题中的起始位置(根据问题调整)
    ):
        # 终止条件(根据具体问题修改)
        if len(path) == len(nums):  # 示例:路径长度等于输入数组长度
            res.append(path[:])     # 添加当前路径的副本到结果集
            return
        
        # 横向遍历选择列表(根据问题调整遍历方式)
        for i in range(start, len(selection)):  # 组合问题从start开始,排列问题从0开始
            # 剪枝判断(根据问题需要添加条件)
            if used[i]:
                continue  # 示例:跳过已使用的元素
            if i > start and selection[i] == selection[i-1] and not used[i-1]:
                continue  # 示例:横向去重
            
            # 做选择(更新路径和状态)
            path.append(selection[i])  # 将当前选择加入路径
            used[i] = True             # 标记当前选择为已使用
            
            # 进入下一层决策树(更新选择列表和参数)
            backtrack(
                path,         # 更新后的路径
                selection,    # 新的选择列表(可能保持不变)
                used,         # 更新后的使用状态
                i+1 if start is not None else 0  # 新的起始位置(组合问题用)
            )
            
            # 撤销选择(回溯操作)
            path.pop()       # 移除当前选择
            used[i] = False  # 标记当前选择为未使用

    # 3. 初始调用(根据问题设置初始参数)
    backtrack(
        [],           # 初始路径:空路径
        nums,         # 初始选择列表:完整数组
        [False] * len(nums),  # 初始辅助参数:未使用的标记数组
        0 if start is not None else None  # 初始起始位置:组合问题从0开始,排列问题为None
    )

    return res

复杂度分析

  1. 时间复杂度

    • 全排列的数量是 O(n!),其中 nnums 的长度。
    • 每次生成一个排列时,需要 O(n) 的时间复制 path
    • 总时间复杂度为 O(n! * n)
  2. 空间复杂度

    • 递归调用栈的深度为 O(n)
    • 存储所有排列的空间复杂度为 O(n! * n)

易错点

  1. 未使用副本

    • 如果不使用 path[:],直接将 path 添加到 result 中,最终所有排列都会指向同一个 path,导致结果错误。
  2. 未撤销选择

    • 在回溯过程中,必须通过 path.pop()visit[i] = False 撤销选择,否则会导致 path 不断增长,visit 状态错误。
  3. 重复选择

    • 通过 visit[i] 确保每个元素只被选择一次,避免重复选择。

变体与应对方法

1. 包含重复元素的排列

如果 nums 中包含重复元素,直接使用上述代码会生成重复的排列。需要对其进行去重。

解决方法

  • 先对 nums 排序,然后在回溯时跳过重复元素。
1
2
if i > 0 and nums[i] == nums[i - 1] and not visit[i - 1]:
    continue

2. 组合问题

如果要求组合而不是排列,可以在回溯过程中限制选择的范围,避免重复组合。

解决方法

  • 在递归调用时,通过 start 参数限制选择范围。
1
2
3
4
5
def dfs(start, path):
    for i in range(start, len(nums)):
        path.append(nums[i])
        dfs(i + 1, path)
        path.pop()

3. 子集问题

如果要求所有子集,可以在每次递归调用时将当前 path 添加到 result 中。

解决方法

  • 在递归调用前直接将 path 添加到 result 中。
1
2
3
4
5
6
def dfs(start, path):
    result.append(path[:])
    for i in range(start, len(nums)):
        path.append(nums[i])
        dfs(i + 1, path)
        path.pop()

买卖股票的最佳时机

121. 买卖股票的最佳时机 Leetcode股票问题

题目描述

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

方法:Kadane算法

1
2
3
4
5
6
7
8
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_price = prices[0]  # 初始化最低价格
        max_profit = 0         # 初始化最大利润
        for i in range(1, len(prices)):  # 遍历价格数组
            min_price = min(min_price, prices[i])  # 更新最低价格
            max_profit = max(max_profit, prices[i] - min_price)  # 更新最大利润
        return max_profit

时间复杂度分析

  • 时间复杂度O(n),其中 n 是 prices 的长度。代码只遍历了一次价格数组。
  • 空间复杂度O(1),只使用了常数级别的额外空间(min_price 和 max_profit)。

合并两个有序数组

88. 合并两个有序数组

题目描述

给你两个有序整数数组 nums1nums2,将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。nums1 的初始长度为 m + n,其中 mnums1 的有效元素数量,nnums2 的长度。

代码实现:倒序遍历

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        """
        Do not return anything, modify nums1 in-place instead.
        """
        k = m + n - 1  # 指向 nums1 的最后一个位置
        while n > 0 and m > 0:  # 从后向前遍历
            if nums1[m - 1] > nums2[n - 1]:  # 比较 nums1 和 nums2 的末尾元素
                nums1[k] = nums1[m - 1]  # 将较大的元素放入 nums1 的末尾
                k, m = k - 1, m - 1  # 移动指针
            else:
                nums1[k] = nums2[n - 1]  # 将较大的元素放入 nums1 的末尾
                k, n = k - 1, n - 1  # 移动指针
        if n > 0:  # 如果 nums2 还有剩余元素
            nums1[:n] = nums2[:n]  # 将剩余元素直接复制到 nums1 的前面

代码解析

核心逻辑

  1. 初始化

    • k 初始化为 m + n - 1,表示 nums1 的最后一个位置。
  2. 从后向前遍历

    • 比较 nums1[m - 1]nums2[n - 1],将较大的元素放入 nums1[k]
    • 移动相应的指针(kmn)。
    • 重复上述过程,直到 mn0
  3. 处理剩余元素

    • 如果 nums2 还有剩余元素(n > 0),将其直接复制到 nums1 的前面。

时空复杂度分析

  • 时间复杂度O(m + n),其中 mnums1 的有效元素数量,nnums2 的长度。代码只遍历了一次 nums1nums2
  • 空间复杂度O(1),没有使用额外的空间。

易错点

  1. 指针移动

    • 在比较 nums1[m - 1]nums2[n - 1] 后,必须正确移动相应的指针(kmn)。
  2. 剩余元素处理

    • 如果 nums2 还有剩余元素,必须将其复制到 nums1 的前面。
  3. 边界条件

    • 如果 m = 0,直接将 nums2 复制到 nums1
    • 如果 n = 0nums1 已经是最终结果。

变体与应对方法

1. 合并多个有序数组

合并 k 个有序数组。

解决方法

  • 使用(优先队列)来维护当前所有数组的最小值。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import heapq
def mergeKLists(lists):
    heap = []
    for i, lst in enumerate(lists):
        if lst:
            heapq.heappush(heap, (lst[0], i, 0))  # (value, list_idx, element_idx)
    res = []
    while heap:
        val, lst_idx, ele_idx = heapq.heappop(heap)
        res.append(val)
        if ele_idx + 1 < len(lists[lst_idx]):
            heapq.heappush(heap, (lists[lst_idx][ele_idx + 1], lst_idx, ele_idx + 1))
    return res

2. 合并两个无序数组

合并两个无序数组,并将结果排序。

解决方法

  • 先将两个数组合并,然后使用排序算法(如 sorted()sort())。
1
2
nums1[m:] = nums2
nums1.sort()

3. 原地合并两个有序链表

合并两个有序链表,并将结果存储在第一个链表中。

解决方法

  • 使用指针从头到尾合并链表。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
    dummy = ListNode(0)
    curr = dummy
    while l1 and l2:
        if l1.val <= l2.val:
            curr.next = l1
            l1 = l1.next
        else:
            curr.next = l2
            l2 = l2.next
        curr = curr.next
    curr.next = l1 if l1 else l2
    return dummy.next

括号匹配

20. 有效的括号

题目描述

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

代码实现:栈

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution:
    def isValid(self, s: str) -> bool:
        r2l = {  # 右括号到左括号的映射
            ')': '(',
            ']': '[',
            '}': '{',
        }
        stk = []  # 栈
        for i in range(len(s)):  # 遍历字符串
            if s[i] in r2l:  # 如果当前字符是右括号
                # 检查栈顶是否是对应的左括号
                if len(stk) > 0 and stk[-1] == r2l[s[i]]:
                    stk.pop()  # 匹配成功,弹出栈顶元素
                else:
                    return False  # 匹配失败,直接返回 False
            else:  # 如果当前字符是左括号
                stk.append(s[i])  # 将其压入栈中
        return len(stk) == 0  # 最后检查栈是否为空

代码解析

  1. 初始化
    • r2l 是一个字典,用于存储右括号到左括号的映射。
    • stk 是一个栈,用于存储左括号。
  2. 遍历字符串
    • 对于每个字符 s[i]
      • 如果 s[i] 是右括号:
        • 检查栈顶是否是对应的左括号(stk[-1] == r2l[s[i]])。
        • 如果匹配成功,弹出栈顶元素(stk.pop())。
        • 如果匹配失败,直接返回 False
      • 如果 s[i] 是左括号,将其压入栈中(stk.append(s[i]))。
  3. 最终检查
    • 如果栈为空,说明所有括号都匹配成功,返回 True
    • 否则,返回 False

时空复杂度分析

  • 时间复杂度O(n),其中 n 是字符串的长度。代码只遍历了一次字符串,并且栈操作的时间复杂度为 O(1)
  • 空间复杂度O(n),在最坏情况下(例如全是左括号),栈的大小为 n

易错点

  1. 括号配对错误
    • 必须确保右括号和栈顶的左括号是同一类型,否则直接返回 False
  2. 栈为空情况
    • 当遇到右括号时,如果栈为空,直接返回 False,因为没有左括号与之匹配。
  3. 最终栈为空
    • 遍历结束后,必须检查栈是否为空,如果栈不为空,说明有未匹配的左括号,返回 False

二叉树的最近公共祖先

236. 二叉树的最近公共祖先

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

代码实现:递归

 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
class Solution:
    def lowestCommonAncestor(self, root, p, q):
        # 递归终止条件:
        # 1. 如果当前节点为空,说明当前分支没有找到 p 或 q,返回 None。
        # 2. 如果当前节点是 p 或 q,说明找到了目标节点,返回当前节点。
        if not root or root == p or root == q:
            return root
        
        # 递归左子树,在左子树中查找 p 和 q。
        # left 表示在左子树中找到的 p 或 q 的最近公共祖先。
        left = self.lowestCommonAncestor(root.left, p, q)
        
        # 递归右子树,在右子树中查找 p 和 q。
        # right 表示在右子树中找到的 p 或 q 的最近公共祖先。
        right = self.lowestCommonAncestor(root.right, p, q)
        
        # 判断逻辑:
        # 1. 如果 left 和 right 都不为空,说明 p 和 q 分别位于当前节点的左右子树中,
        #    因此当前节点是 p 和 q 的最近公共祖先,返回 root。
        if left and right:
            return root
        
        # 2. 如果只有 left 不为空,说明 p 和 q 都在左子树中,返回 left。
        if left:
            return left
        
        # 3. 如果只有 right 不为空,说明 p 和 q 都在右子树中,返回 right。
        if right:
            return right
        
        # 4. 如果 left 和 right 都为空,说明当前子树中没有找到 p 或 q,返回 None。
        return None

代码解析

  1. 递归终止条件

    • 如果 root 为空(即 not root),或者 root 等于 p 或 q,直接返回 root。这里:
      • 如果 root 为空,表示当前分支没有找到 p 或 q
      • 如果 root == p 或 root == q,表示已经找到了其中一个目标节点,返回 root
  2. 递归左子树

    • 在左子树中递归查找 p 和 q,结果存储在 left 中。
  3. 递归右子树

    • 在右子树中递归查找 p 和 q,结果存储在 right 中。
  4. 判断 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

链表成环

141. 环形链表

题目描述

给你一个链表的头节点 head ,判断链表中是否有环。

实现:快慢指针

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Solution:
    def hasCycle(self, head: Optional[ListNode]) -> bool:
        if not head:
            return False 
        slow, fast = head, head 
        while fast and fast.next:
            slow = slow.next 
            fast = fast.next.next 
            if fast == slow:
                return True
        return False
Licensed under CC BY-NC-SA 4.0
© 2023 - 2025 壹壹贰捌· 0Days
共书写了265.7k字·共 93篇文章 京ICP备2023035941号-1