LeetCode 100+ 题详解 - 完整刷题指南¶
重要性:⭐⭐⭐⭐⭐ 学习时间:2-3个月 目标:大厂面试准备 题目数量:100+道
📚 目录¶
数组(20题)¶
题目1:两数之和(Two Sum)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 1
难度:简单
问题描述: 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
示例:
解法1:暴力法¶
思路:双重循环,枚举所有可能的两数组合
def two_sum_brute(nums, target):
"""
两数之和(暴力法)
时间:O(n²)
空间:O(1)
"""
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
if nums[i] + nums[j] == target:
return [i, j]
return []
复杂度分析: - 时间:O(n²),双重循环 - 空间:O(1),不需要额外空间
解法2:哈希表(推荐)⭐¶
思路:用哈希表存储已访问的数字及其索引
def two_sum_hash(nums, target):
"""
两数之和(哈希表)
时间:O(n)
空间:O(n)
"""
# 创建哈希表:数字 → 索引
num_to_index = {}
for i, num in enumerate(nums):
# 计算需要的补数
complement = target - num
# 检查补数是否已在哈希表中
if complement in num_to_index:
return [num_to_index[complement], i]
# 将当前数字存入哈希表
num_to_index[num] = i
return []
执行过程演示:
输入:nums = [2,7,11,15], target = 9
第1次迭代 (i=0, num=2):
- complement = 9 - 2 = 7
- 7 不在哈希表中
- 存储:{2: 0}
第2次迭代 (i=1, num=7):
- complement = 9 - 7 = 2
- 2 在哈希表中!索引为 0
- 返回 [0, 1] ✓
结果:[0, 1]
复杂度分析: - 时间:O(n),单次遍历 - 空间:O(n),哈希表最多存储n个元素
解法3:双指针(需排序)¶
思路:先排序,再用双指针从两端向中间查找
def two_sum_two_pointers(nums, target):
"""
两数之和(双指针,需排序)
时间:O(n log n)
空间:O(n)
"""
# 创建带索引的数组
nums_with_index = [(num, i) for i, num in enumerate(nums)] # enumerate同时获取索引和值
# 排序
nums_with_index.sort()
# 双指针
left, right = 0, len(nums) - 1
while left < right:
current_sum = nums_with_index[left][0] + nums_with_index[right][0]
if current_sum == target:
return [nums_with_index[left][1], nums_with_index[right][1]]
elif current_sum < target:
left += 1
else:
right -= 1
return []
复杂度分析: - 时间:O(n log n),主要是排序 - 空间:O(n),存储带索引的数组
题目2:买卖股票的最佳时机(Best Time to Buy and Sell Stock)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 121
难度:简单
问题描述: 给定一个数组 prices,它的第 i 个元素 prices[i] 是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一只股票),设计一个算法来计算你所能获取的最大利润。
示例:
解法1:暴力法¶
def max_profit_brute(prices):
"""
买卖股票(暴力法)
时间:O(n²)
空间:O(1)
"""
max_profit = 0
n = len(prices)
for i in range(n):
for j in range(i + 1, n):
profit = prices[j] - prices[i]
max_profit = max(max_profit, profit)
return max_profit
解法2:一次遍历(推荐)⭐¶
思路:记录最低价格,每次计算当前价格卖出能获得的利润
def max_profit(prices):
"""
买卖股票(一次遍历)
时间:O(n)
空间:O(1)
"""
if not prices:
return 0
min_price = prices[0] # 最低价格
max_profit = 0 # 最大利润
for price in prices[1:]:
# 更新最低价格
min_price = min(min_price, price)
# 更新最大利润
profit = price - min_price
max_profit = max(max_profit, profit)
return max_profit
执行过程演示:
输入:prices = [7,1,5,3,6,4]
第1天 (price=7):
- min_price = 7
- profit = 7 - 7 = 0
- max_profit = 0
第2天 (price=1):
- min_price = min(7, 1) = 1
- profit = 1 - 1 = 0
- max_profit = 0
第3天 (price=5):
- min_price = 1
- profit = 5 - 1 = 4
- max_profit = 4
第4天 (price=3):
- min_price = 1
- profit = 3 - 1 = 2
- max_profit = 4
第5天 (price=6):
- min_price = 1
- profit = 6 - 1 = 5
- max_profit = 5 ← 最大利润
第6天 (price=4):
- min_price = 1
- profit = 4 - 1 = 3
- max_profit = 5
结果:5
核心思想: - 保持"在最低点买入"的策略 - 每天都计算"如果今天卖出"能赚多少 - 取最大值
题目3:最大子数组和(Maximum Subarray)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 53
难度:中等
问题描述: 给定一个整数数组 nums,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
解法1:Kadane算法(推荐)⭐¶
核心思想:动态规划,dp[i] = max(nums[i], dp[i-1] + nums[i])
def max_sub_array(nums):
"""
最大子数组和(Kadane算法)
时间:O(n)
空间:O(1)
"""
if not nums:
return 0
# 当前子数组和
current_sum = nums[0]
# 最大子数组和
max_sum = nums[0]
for num in nums[1:]:
# 选择:扩展当前子数组 vs 从当前数字重新开始
current_sum = max(num, current_sum + num)
# 更新最大值
max_sum = max(max_sum, current_sum)
return max_sum
执行过程演示:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
初始化:
- current_sum = -2
- max_sum = -2
第2个元素 (num=1):
- current_sum = max(1, -2+1) = max(1, -1) = 1
- max_sum = max(-2, 1) = 1
第3个元素 (num=-3):
- current_sum = max(-3, 1-3) = max(-3, -2) = -2
- max_sum = max(1, -2) = 1
第4个元素 (num=4):
- current_sum = max(4, -2+4) = max(4, 2) = 4 ← 重新开始
- max_sum = max(1, 4) = 4
第5个元素 (num=-1):
- current_sum = max(-1, 4-1) = max(-1, 3) = 3
- max_sum = max(4, 3) = 4
第6个元素 (num=2):
- current_sum = max(2, 3+2) = max(2, 5) = 5
- max_sum = max(4, 5) = 5
第7个元素 (num=1):
- current_sum = max(1, 5+1) = max(1, 6) = 6
- max_sum = max(5, 6) = 6 ← 最大值
第8个元素 (num=-5):
- current_sum = max(-5, 6-5) = max(-5, 1) = 1
- max_sum = max(6, 1) = 6
第9个元素 (num=4):
- current_sum = max(4, 1+4) = max(4, 5) = 5
- max_sum = max(6, 5) = 6
结果:6
子数组:[4, -1, 2, 1]
核心思想: - 遍历数组,维护两个变量 - current_sum:以当前元素结尾的最大子数组和 - max_sum:全局最大子数组和 - 对于每个元素,决定是"继续前面的子数组"还是"从这里重新开始"
解法2:分治法¶
def max_sub_array_divide(nums):
"""
最大子数组和(分治法)
时间:O(n log n)
空间:O(log n)
"""
def divide_conquer(left, right):
if left == right:
return nums[left]
mid = (left + right) // 2
# 左半部分的最大子数组和
left_max = divide_conquer(left, mid)
# 右半部分的最大子数组和
right_max = divide_conquer(mid + 1, right)
# 跨越中点的最大子数组和
# 从中点向左扩展
left_sum = float('-inf')
current_sum = 0
for i in range(mid, left - 1, -1):
current_sum += nums[i]
left_sum = max(left_sum, current_sum)
# 从中点+1向右扩展
right_sum = float('-inf')
current_sum = 0
for i in range(mid + 1, right + 1):
current_sum += nums[i]
right_sum = max(right_sum, current_sum)
cross_max = left_sum + right_sum
# 返回三者中的最大值
return max(left_max, right_max, cross_max)
if not nums:
return 0
return divide_conquer(0, len(nums) - 1)
题目4:删除有序数组中的重复项(Remove Duplicates from Sorted Array)⭐⭐⭐¶
题目链接:LeetCode 26
难度:简单
问题描述: 给你一个有序数组 nums,请你原地删除重复的元素,使每个元素只出现一次,返回删除后数组的新长度。
示例:
解法:双指针(推荐)⭐¶
思路:慢指针指向不重复的位置,快指针遍历数组
def remove_duplicates(nums):
"""
删除有序数组中的重复项(双指针)
时间:O(n)
空间:O(1)
"""
if not nums:
return 0
# 慢指针:指向不重复的最后位置
slow = 0
# 快指针:遍历数组
for fast in range(1, len(nums)):
# 当快指针指向的元素与慢指针不同时
if nums[fast] != nums[slow]:
# 慢指针前进
slow += 1
# 复制新元素
nums[slow] = nums[fast]
# 返回长度(索引+1)
return slow + 1
执行过程演示:
输入:nums = [1,1,2]
初始状态:
- slow = 0, fast = 1
- nums = [1,1,2]
第1次迭代 (fast=1):
- nums[1]=1, nums[0]=1
- 相等,不操作
第2次迭代 (fast=2):
- nums[2]=2, nums[0]=1
- 不相等!
- slow += 1 → slow = 1
- nums[1] = nums[2] → nums = [1,2,2]
结果:
- 返回 2 (slow+1)
- nums = [1,2,2]
- 前2个元素:[1,2] ✓
题目5:轮转数组(Rotate Array)⭐⭐⭐⭐¶
题目链接:LeetCode 189
难度:中等
问题描述: 给定一个数组,将数组中的元素向右移动 k 个位置。
示例:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
解法1:三次翻转(推荐)⭐¶
思路: 1. 翻转整个数组 2. 翻转前k个元素 3. 翻转后n-k个元素
def rotate(nums, k):
"""
轮转数组(三次翻转)
时间:O(n)
空间:O(1)
"""
n = len(nums)
k %= n # 处理 k >= n 的情况
def reverse(left, right):
"""翻转 nums[left:right+1]"""
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
# 三次翻转
reverse(0, n - 1) # 翻转整个数组
reverse(0, k - 1) # 翻转前k个元素
reverse(k, n - 1) # 翻转后n-k个元素
执行过程演示:
输入:nums = [1,2,3,4,5,6,7], k = 3
初始数组:[1,2,3,4,5,6,7]
第1步:翻转整个数组 [0,6]
[1,2,3,4,5,6,7] → [7,6,5,4,3,2,1]
第2步:翻转前3个元素 [0,2]
[7,6,5,4,3,2,1] → [5,6,7,4,3,2,1]
第3步:翻转后4个元素 [3,6]
[5,6,7,4,3,2,1] → [5,6,7,1,2,3,4]
结果:[5,6,7,1,2,3,4] ✓
解法2:使用额外数组¶
def rotate_extra_array(nums, k):
"""
轮转数组(额外数组)
时间:O(n)
空间:O(n)
"""
n = len(nums)
k %= n
# 创建新数组
result = [0] * n
for i in range(n):
result[(i + k) % n] = nums[i]
# 复制回原数组
for i in range(n):
nums[i] = result[i]
题目6:除自身以外数组的乘积(Product of Array Except Self)⭐⭐⭐⭐¶
题目链接:LeetCode 238
难度:中等
问题描述: 给定一个整数数组 nums,返回数组 answer,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
示例:
输入:nums = [1,2,3,4]
输出:[24,12,8,6]
解释:
- answer[0] = 2*3*4 = 24
- answer[1] = 1*3*4 = 12
- answer[2] = 1*2*4 = 8
- answer[3] = 1*2*3 = 6
解法:左右乘积列表(推荐)⭐¶
思路: - 左侧乘积:当前元素左边所有元素的乘积 - 右侧乘积:当前元素右边所有元素的乘积 - answer[i] = left[i] * right[i]
def product_except_self(nums):
"""
除自身以外数组的乘积
时间:O(n)
空间:O(1)(不包括输出数组)
"""
n = len(nums)
answer = [1] * n
# 计算左侧乘积
left_product = 1
for i in range(n):
answer[i] = left_product
left_product *= nums[i]
# 计算右侧乘积并更新answer
right_product = 1
for i in range(n - 1, -1, -1):
answer[i] *= right_product
right_product *= nums[i]
return answer
执行过程演示:
输入:nums = [1,2,3,4]
初始化:answer = [1,1,1,1]
第1步:计算左侧乘积(从左到右)
i=0: answer[0]=1, left_product=1*1=1
i=1: answer[1]=1, left_product=1*2=2
i=2: answer[2]=2, left_product=2*3=6
i=3: answer[3]=6, left_product=6*4=24
answer = [1,1,2,6](左侧乘积)
第2步:计算右侧乘积并更新(从右到左)
i=3: answer[3]=6*1=6, right_product=1*4=4
i=2: answer[2]=2*4=8, right_product=4*3=12
i=1: answer[1]=1*12=12, right_product=12*2=24
i=0: answer[0]=1*24=24, right_product=24*1=24
answer = [24,12,8,6] ✓
验证:
- answer[0] = 24 = 2*3*4 ✓
- answer[1] = 12 = 1*3*4 ✓
- answer[2] = 8 = 1*2*4 ✓
- answer[3] = 6 = 1*2*3 ✓
题目7:三数之和(3Sum)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 15
难度:中等
问题描述: 给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c,使得 a + b + c = 0?找出所有和为 0 且不重复的三元组。
示例:
解法:排序 + 双指针(推荐)⭐¶
思路: 1. 先排序数组 2. 固定第一个元素,用双指针找另外两个元素 3. 跳过重复元素
def three_sum(nums):
"""
三数之和
时间:O(n²)
空间:O(1)
"""
nums.sort()
result = []
n = len(nums)
for i in range(n - 2):
# 跳过重复的第一个元素
if i > 0 and nums[i] == nums[i - 1]:
continue
# 双指针
left, right = i + 1, n - 1
while left < right:
current_sum = nums[i] + nums[left] + nums[right]
if current_sum == 0:
result.append([nums[i], nums[left], nums[right]])
# 跳过重复元素
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif current_sum < 0:
left += 1
else:
right -= 1
return result
执行过程演示:
输入:nums = [-1,0,1,2,-1,-4]
第1步:排序
[-4,-1,-1,0,1,2]
第2步:遍历
i=0, nums[i]=-4:
- left=1, right=5
- -4 + (-1) + 2 = -3 < 0, left++
- -4 + (-1) + 2 = -3 < 0, left++
- ... (left继续增加,但无法找到和为0的组合)
i=1, nums[i]=-1:
- left=2, right=5
- -1 + (-1) + 2 = 0 ✓ 找到 [-1,-1,2]
- left=3, right=4
- -1 + 0 + 1 = 0 ✓ 找到 [-1,0,1]
i=2, nums[i]=-1:
- 跳过(与i=1重复)
i=3, nums[i]=0:
- left=4, right=5
- 0 + 1 + 2 = 3 > 0, right--
- left >= right, 结束
结果:[[-1,-1,2], [-1,0,1]] ✓
题目8:搜索二维矩阵II(Search a 2D Matrix II)⭐⭐⭐⭐¶
题目链接:LeetCode 240
难度:中等
问题描述: 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性: - 每行的元素从左到右升序排列 - 每列的元素从上到下升序排列
示例:
输入:matrix = [
[1,4,7,11,15],
[2,5,8,12,19],
[3,6,9,16,22],
[10,13,14,17,24],
[18,21,23,26,30]
], target = 5
输出:true
解法:从右上角开始搜索(推荐)⭐¶
思路: - 从右上角开始 - 如果当前元素 > target,向左移动(列减小) - 如果当前元素 < target,向下移动(行增加)
def search_matrix(matrix, target):
"""
搜索二维矩阵II
时间:O(m + n)
空间:O(1)
"""
if not matrix or not matrix[0]:
return False
m, n = len(matrix), len(matrix[0])
# 从右上角开始
row, col = 0, n - 1
while row < m and col >= 0:
current = matrix[row][col]
if current == target:
return True
elif current > target:
# 当前值太大,向左移动
col -= 1
else:
# 当前值太小,向下移动
row += 1
return False
执行过程演示:
矩阵:
[ 1, 4, 7,11,15]
[ 2, 5, 8,12,19]
[ 3, 6, 9,16,22]
[10,13,14,17,24]
[18,21,23,26,30]
target = 5
从右上角(0,4)=15开始:
1. 15 > 5, 向左移动到 (0,3)=11
2. 11 > 5, 向左移动到 (0,2)=7
3. 7 > 5, 向左移动到 (0,1)=4
4. 4 < 5, 向下移动到 (1,1)=5
5. 5 == 5 ✓ 找到!
返回:True
题目9:合并区间(Merge Intervals)⭐⭐⭐⭐¶
题目链接:LeetCode 56
难度:中等
问题描述: 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]。请你合并所有重叠的区间,并返回一个不重叠的区间数组。
示例:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠,将它们合并为 [1,6]。
解法:排序 + 遍历(推荐)⭐¶
def merge(intervals):
"""
合并区间
时间:O(n log n)
空间:O(1)
"""
if not intervals:
return []
# 按区间起点排序
intervals.sort(key=lambda x: x[0]) # lambda匿名函数:简洁的单行函数
result = []
current = intervals[0]
for interval in intervals[1:]: # 切片操作:[start:end:step]提取子序列
# 如果有重叠(当前区间的起点 <= 上一个区间的终点)
if interval[0] <= current[1]:
# 合并:更新终点为两者中较大的
current[1] = max(current[1], interval[1])
else:
# 无重叠,保存当前区间,开始新区间
result.append(current)
current = interval
# 添加最后一个区间
result.append(current)
return result
执行过程演示:
输入:[[1,3],[2,6],[8,10],[15,18]]
第1步:排序(已排序)
[[1,3],[2,6],[8,10],[15,18]]
第2步:遍历合并
初始:current = [1,3]
[2,6]:
- 2 <= 3,有重叠
- 合并:current = [1, max(3,6)] = [1,6]
[8,10]:
- 8 > 6,无重叠
- 保存 [1,6]
- current = [8,10]
[15,18]:
- 15 > 10,无重叠
- 保存 [8,10]
- current = [15,18]
最后:添加 [15,18]
结果:[[1,6],[8,10],[15,18]] ✓
题目10:矩阵置零(Set Matrix Zeroes)⭐⭐⭐¶
题目链接:LeetCode 73
难度:中等
问题描述: 给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例:
解法:使用首行首列标记(推荐)⭐¶
思路: - 用第一行和第一列作为标记数组 - 用两个变量记录第一行和第一列本身是否有0
def set_zeroes(matrix):
"""
矩阵置零
时间:O(m*n)
空间:O(1)
"""
if not matrix or not matrix[0]:
return
m, n = len(matrix), len(matrix[0])
# 标记第一行和第一列是否有0
first_row_has_zero = any(matrix[0][j] == 0 for j in range(n))
first_col_has_zero = any(matrix[i][0] == 0 for i in range(m))
# 用第一行和第一列标记0的位置
for i in range(1, m):
for j in range(1, n):
if matrix[i][j] == 0:
matrix[i][0] = 0
matrix[0][j] = 0
# 根据标记置零
for i in range(1, m):
for j in range(1, n):
if matrix[i][0] == 0 or matrix[0][j] == 0:
matrix[i][j] = 0
# 处理第一行和第一列
if first_row_has_zero:
for j in range(n):
matrix[0][j] = 0
if first_col_has_zero:
for i in range(m):
matrix[i][0] = 0
链表(15题)¶
题目11:反转链表(Reverse Linked List)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 206
难度:简单
问题描述: 给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
示例:
解法1:迭代法(推荐)⭐¶
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def reverse_list(head):
"""
反转链表(迭代)
时间:O(n)
空间:O(1)
"""
prev = None
current = head
while current:
# 保存下一个节点
next_node = current.next
# 反转指针
current.next = prev
# 移动指针
prev = current
current = next_node
return prev
执行过程演示:
初始:1 -> 2 -> 3 -> 4 -> 5
第1次迭代:
- current = 1
- next_node = 2
- 1.next = None
- prev = 1
- 状态:None <- 1 2 -> 3 -> 4 -> 5
第2次迭代:
- current = 2
- next_node = 3
- 2.next = 1
- prev = 2
- 状态:None <- 1 <- 2 3 -> 4 -> 5
第3次迭代:
- current = 3
- next_node = 4
- 3.next = 2
- prev = 3
- 状态:None <- 1 <- 2 <- 3 4 -> 5
第4次迭代:
- current = 4
- next_node = 5
- 4.next = 3
- prev = 4
- 状态:None <- 1 <- 2 <- 3 <- 4 5
第5次迭代:
- current = 5
- next_node = None
- 5.next = 4
- prev = 5
- 状态:None <- 1 <- 2 <- 3 <- 4 <- 5
返回:prev = 5(新头节点)
解法2:递归法¶
def reverse_list_recursive(head):
"""
反转链表(递归)
时间:O(n)
空间:O(n)(递归栈)
"""
if not head or not head.next:
return head
# 递归反转后面的链表
new_head = reverse_list_recursive(head.next)
# 反转当前节点和下一个节点的指针
head.next.next = head
head.next = None
return new_head
题目12:合并两个有序链表(Merge Two Sorted Lists)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 21
难度:简单
问题描述: 将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
解法:迭代(推荐)⭐¶
def merge_two_lists(l1, l2):
"""
合并两个有序链表
时间:O(m + n)
空间:O(1)
"""
# 创建虚拟头节点
dummy = ListNode(0)
current = dummy
# 比较并合并
while l1 and l2:
if l1.val <= l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
# 连接剩余节点
current.next = l1 if l1 else l2
return dummy.next
题目13:环形链表(Linked List Cycle)⭐⭐⭐⭐¶
题目链接:LeetCode 141
难度:简单
问题描述: 给你一个链表的头节点 head,判断链表中是否有环。
解法:快慢指针(推荐)⭐¶
def has_cycle(head):
"""
环形链表(快慢指针)
时间:O(n)
空间:O(1)
"""
if not head or not head.next:
return False
slow = head
fast = head
while fast and fast.next:
slow = slow.next # 慢指针走1步
fast = fast.next.next # 快指针走2步
# 如果相遇,说明有环
if slow == fast:
return True
return False
题目14:环形链表II(Linked List Cycle II)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 142
难度:中等
问题描述: 给定一个链表,返回链表开始入环的第一个节点。如果链表无环,则返回 null。
解法:快慢指针 + 数学(推荐)⭐¶
def detect_cycle(head):
"""
环形链表II
时间:O(n)
空间:O(1)
"""
if not head or not head.next:
return None
# 第1步:判断是否有环
slow = head
fast = head
has_cycle = False
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
has_cycle = True
break
if not has_cycle:
return None
# 第2步:找到环的入口
# 一个指针从head开始,一个从相遇点开始
# 相同速度前进,相遇点就是环入口
ptr1 = head
ptr2 = slow
while ptr1 != ptr2:
ptr1 = ptr1.next
ptr2 = ptr2.next
return ptr1
数学原理:
设:
- 头节点到环入口距离为 a
- 环入口到相遇点距离为 b
- 相遇点到环入口距离为 c
- 环长度为 b + c
快指针走的距离:a + b + c + b = a + 2b + c
慢指针走的距离:a + b
快指针速度是慢指针的2倍:
a + 2b + c = 2(a + b)
=> a + c = 0
因此:从head出发走a步,从相遇点出发走c步,会在环入口相遇
题目15:回文链表(Palindrome Linked List)⭐⭐⭐⭐¶
题目链接:LeetCode 234
难度:简单
问题描述: 给你一个单链表的头节点 head,请你判断该链表是否为回文链表。
解法:反转后半部分(推荐)⭐¶
def is_palindrome(head):
"""
回文链表
时间:O(n)
空间:O(1)
"""
if not head or not head.next:
return True
# 第1步:找到中点
slow = head
fast = head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# 第2步:反转后半部分
second_half = reverse_list(slow.next)
# 第3步:比较前后两部分
first_half = head
result = True
while second_half:
if first_half.val != second_half.val:
result = False
break
first_half = first_half.next
second_half = second_half.next
# 第4步:恢复链表(可选)
slow.next = reverse_list(second_half)
return result
树(20题)¶
题目16:二叉树的最大深度(Maximum Depth of Binary Tree)⭐⭐⭐⭐¶
题目链接:LeetCode 104
难度:简单
问题描述: 给定一个二叉树,找出其最大深度。
解法:递归(推荐)⭐¶
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def max_depth(root):
"""
二叉树的最大深度
时间:O(n)
空间:O(h)
"""
if not root:
return 0
left_depth = max_depth(root.left)
right_depth = max_depth(root.right)
return max(left_depth, right_depth) + 1
题目17:验证二叉搜索树(Validate BST)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 98
难度:中等
问题描述: 给定一个二叉树,判断其是否是一个有效的二叉搜索树(BST)。
解法:递归 + 上下界(推荐)⭐¶
def is_valid_bst(root):
"""
验证二叉搜索树
时间:O(n)
空间:O(h)
"""
def validate(node, low, high):
if not node:
return True
# 检查当前节点值是否在范围内
if not (low < node.val < high):
return False
# 递归检查左右子树
return (validate(node.left, low, node.val) and
validate(node.right, node.val, high))
return validate(root, float('-inf'), float('inf'))
题目18:对称二叉树(Symmetric Tree)⭐⭐⭐⭐¶
题目链接:LeetCode 101
难度:简单
问题描述: 给定一个二叉树,检查它是否轴对称。
解法:递归(推荐)⭐¶
def is_symmetric(root):
"""
对称二叉树
时间:O(n)
空间:O(h)
"""
if not root:
return True
def is_mirror(left, right):
if not left and not right:
return True
if not left or not right:
return False
return (left.val == right.val and
is_mirror(left.left, right.right) and
is_mirror(left.right, right.left))
return is_mirror(root.left, root.right)
题目19:层序遍历(Binary Tree Level Order Traversal)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 102
难度:中等
问题描述: 给你二叉树的根节点,返回其节点值的层序遍历。(即逐层地,从左到右访问所有节点)。
解法:BFS(推荐)⭐¶
from collections import deque
def level_order(root):
"""
层序遍历
时间:O(n)
空间:O(n)
"""
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return result
题目20:二叉树展开为链表(Flatten Binary Tree to Linked List)⭐⭐⭐⭐¶
题目链接:LeetCode 114
难度:中等
问题描述: 给你二叉树的根节点,请你将它展开为一个单链表。 - 展开后的单链表应该同样使用 TreeNode,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null。 - 展开后的单链表应该与二叉树先序遍历顺序相同。
解法:迭代(推荐)⭐¶
def flatten(root):
"""
二叉树展开为链表
时间:O(n)
空间:O(1)
"""
if not root:
return
current = root
while current:
if current.left:
# 找到左子树的最右节点
rightmost = current.left
while rightmost.right:
rightmost = rightmost.right
# 将右子树接到左子树的最右节点
rightmost.right = current.right
# 将左子树移到右子树
current.right = current.left
current.left = None
# 移动到下一个节点
current = current.right
动态规划(20题)¶
题目21:爬楼梯(Climbing Stairs)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 70
难度:简单
问题描述: 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
解法1:动态规划(推荐)⭐¶
def climb_stairs(n):
"""
爬楼梯
时间:O(n)
空间:O(1)
"""
if n <= 2:
return n
# dp[i] = dp[i-1] + dp[i-2]
prev, curr = 1, 2
for _ in range(3, n + 1):
prev, curr = curr, prev + curr
return curr
解法2:矩阵快速幂(进阶)¶
def climb_stairs_fast(n):
"""
爬楼梯(矩阵快速幂)
时间:O(log n)
空间:O(1)
"""
def matrix_pow(mat, power):
result = [[1, 0], [0, 1]] # 单位矩阵
while power:
if power % 2 == 1:
result = matrix_multiply(result, mat)
mat = matrix_multiply(mat, mat)
power //= 2
return result
def matrix_multiply(a, b):
return [
[a[0][0] * b[0][0] + a[0][1] * b[1][0],
a[0][0] * b[0][1] + a[0][1] * b[1][1]],
[a[1][0] * b[0][0] + a[1][1] * b[1][0],
a[1][0] * b[0][1] + a[1][1] * b[1][1]]
]
if n <= 2:
return n
mat = [[1, 1], [1, 0]]
result = matrix_pow(mat, n - 1)
return result[0][0]
题目22:打家劫舍(House Robber)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 198
难度:中等
问题描述: 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统。如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,今晚能够偷窃到的最高金额。
解法:动态规划(推荐)⭐¶
def rob(nums):
"""
打家劫舍
时间:O(n)
空间:O(1)
"""
if not nums:
return 0
if len(nums) <= 2:
return max(nums)
# dp[i] = max(dp[i-1], dp[i-2] + nums[i])
prev2 = nums[0] # dp[i-2]
prev1 = max(nums[0], nums[1]) # dp[i-1]
for i in range(2, len(nums)):
current = max(prev1, prev2 + nums[i])
prev2, prev1 = prev1, current
return prev1
状态转移方程:
对于第i个房子,有两种选择:
1. 不偷第i个房子:最大金额 = dp[i-1]
2. 偷第i个房子:最大金额 = dp[i-2] + nums[i]
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
题目23:零钱兑换(Coin Change)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 322
难度:中等
问题描述: 给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。计算并返回可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
解法:动态规划(推荐)⭐¶
def coin_change(coins, amount):
"""
零钱兑换
时间:O(amount * len(coins))
空间:O(amount)
"""
# dp[i] = 凑成金额i所需的最少硬币数
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # 凑成金额0需要0个硬币
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
执行过程演示:
输入:coins = [1,2,5], amount = 11
初始化:dp = [0,inf,inf,inf,inf,inf,inf,inf,inf,inf,inf,inf]
使用coin=1更新:
dp = [0,1,2,3,4,5,6,7,8,9,10,11]
使用coin=2更新:
dp = [0,1,1,2,2,3,3,4,4,5,5,6]
使用coin=5更新:
dp = [0,1,1,2,2,1,2,2,3,3,2,3]
结果:3(5+5+1)
题目24:最长递增子序列(Longest Increasing Subsequence)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 300
难度:中等
问题描述: 给你一个整数数组 nums,找到其中最长严格递增子序列的长度。
解法1:动态规划(基础)⭐¶
def length_of_lis(nums):
"""
最长递增子序列(DP)
时间:O(n²)
空间:O(n)
"""
if not nums:
return 0
n = len(nums)
# dp[i] = 以nums[i]结尾的最长递增子序列长度
dp = [1] * n
for i in range(n):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
解法2:二分查找(进阶)⭐⭐¶
import bisect
def length_of_lis_binary(nums):
"""
最长递增子序列(二分查找)
时间:O(n log n)
空间:O(n)
"""
if not nums:
return 0
# tails[i] = 长度为i+1的递增子序列的最小末尾
tails = []
for num in nums:
# 二分查找第一个 >= num 的位置
idx = bisect.bisect_left(tails, num)
if idx == len(tails):
tails.append(num)
else:
tails[idx] = num
return len(tails)
执行过程演示:
输入:nums = [10,9,2,5,3,7,101,18]
初始化:tails = []
num=10: tails = [10]
num=9: tails = [9] (替换10)
num=2: tails = [2] (替换9)
num=5: tails = [2,5]
num=3: tails = [2,3] (替换5)
num=7: tails = [2,3,7]
num=101: tails = [2,3,7,101]
num=18: tails = [2,3,7,18] (替换101)
结果:4
子序列:[2,3,7,101] 或 [2,3,7,18]
题目25:完全平方数(Perfect Squares)⭐⭐⭐⭐¶
题目链接:LeetCode 279
难度:中等
问题描述: 给你一个整数 n,返回和为 n 的完全平方数的最少数量。
解法:动态规划(推荐)⭐¶
def num_squares(n):
"""
完全平方数
时间:O(n * sqrt(n))
空间:O(n)
"""
# dp[i] = 和为i的完全平方数的最少数量
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, n + 1):
j = 1
while j * j <= i:
dp[i] = min(dp[i], dp[i - j * j] + 1)
j += 1
return dp[n]
图(10题)¶
题目26:课程表(Course Schedule)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 207
难度:中等
问题描述: 你必须修读 numCourses 门课程,记为 0 到 numCourses-1。在选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi],表示如果要学习课程 ai 则必须先学习课程 bi。判断是否可能完成所有课程的学习?
解法:拓扑排序(推荐)⭐¶
from collections import deque
def can_finish(num_courses, prerequisites):
"""
课程表(拓扑排序)
时间:O(V + E)
空间:O(V + E)
"""
# 构建邻接表和入度数组
adj = [[] for _ in range(num_courses)]
in_degree = [0] * num_courses
for course, prereq in prerequisites:
adj[prereq].append(course)
in_degree[course] += 1
# 将入度为0的节点加入队列
queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
# BFS拓扑排序
count = 0
while queue:
node = queue.popleft()
count += 1
for neighbor in adj[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# 如果所有节点都被访问,说明没有环
return count == num_courses
题目27:课程表II(Course Schedule II)⭐⭐⭐⭐⭐¶
题目链接:LeetCode 210
难度:中等
问题描述: 现在你总共有 numCourses 门课需要选,记为 0 到 numCourses-1。给定一个数组 prerequisites,其中 prerequisites[i] = [ai, bi],表示在选修课程 ai 前必须先选修 bi。返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回任意一种就可以了。如果不可能完成所有课程,返回一个空数组。
解法:拓扑排序(推荐)⭐¶
def find_order(num_courses, prerequisites):
"""
课程表II
时间:O(V + E)
空间:O(V + E)
"""
# 构建邻接表和入度数组
adj = [[] for _ in range(num_courses)]
in_degree = [0] * num_courses
for course, prereq in prerequisites:
adj[prereq].append(course)
in_degree[course] += 1
# BFS拓扑排序
queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in adj[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# 判断是否有环
return result if len(result) == num_courses else []
题目28:被围绕的区域(Surrounded Regions)⭐⭐⭐⭐¶
题目链接:LeetCode 130
难度:中等
问题描述: 给你一个 m x n 的矩阵 board,由若干字符 'X' 和 'O',找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
解法:从边界开始DFS(推荐)⭐¶
def solve(board):
"""
被围绕的区域
时间:O(m * n)
空间:O(m * n)
"""
if not board or not board[0]:
return
m, n = len(board), len(board[0])
def dfs(i, j):
"""标记与边界相连的O"""
if i < 0 or i >= m or j < 0 or j >= n:
return
if board[i][j] != 'O':
return
# 标记为临时字符
board[i][j] = 'T'
# 递归标记四个方向
dfs(i + 1, j)
dfs(i - 1, j)
dfs(i, j + 1)
dfs(i, j - 1)
# 第1步:从边界开始,标记所有与边界相连的O
for i in range(m):
for j in range(n):
if i == 0 or i == m - 1 or j == 0 or j == n - 1:
if board[i][j] == 'O':
dfs(i, j)
# 第2步:遍历整个矩阵
for i in range(m):
for j in range(n):
if board[i][j] == 'O':
# 被包围的O,改为X
board[i][j] = 'X'
elif board[i][j] == 'T':
# 与边界相连的O,恢复为O
board[i][j] = 'O'
排序/搜索(15题)¶
题目29:合并两个有序数组(Merge Sorted Array)⭐⭐⭐¶
题目链接:LeetCode 88
难度:简单
问题描述: 给你两个按非递减顺序排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。
示例:
解法:从后向前合并(推荐)⭐¶
def merge(nums1, m, nums2, n):
"""
合并两个有序数组
时间:O(m + n)
空间:O(1)
"""
# 三个指针
p1 = m - 1 # nums1的最后一个元素
p2 = n - 1 # nums2的最后一个元素
p = m + n - 1 # 合并后的最后一个位置
# 从后向前合并
while p1 >= 0 and p2 >= 0:
if nums1[p1] > nums2[p2]:
nums1[p] = nums1[p1]
p1 -= 1
else:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
# 复制nums2剩余元素
while p2 >= 0:
nums1[p] = nums2[p2]
p2 -= 1
p -= 1
执行过程演示:
输入:nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
初始:p1=2, p2=2, p=5
nums1 = [1,2,3,0,0,0]
nums2 = [2,5,6]
第1次比较:
nums1[2]=3 vs nums2[2]=6
3 < 6,nums1[5]=6
p2=1, p=4
nums1 = [1,2,3,0,0,6]
第2次比较:
nums1[2]=3 vs nums2[1]=5
3 < 5,nums1[4]=5
p2=0, p=3
nums1 = [1,2,3,0,5,6]
第3次比较:
nums1[2]=3 vs nums2[0]=2
3 > 2,nums1[3]=3
p1=1, p=2
nums1 = [1,2,3,3,5,6]
第4次比较:
nums1[1]=2 vs nums2[0]=2
2 = 2,nums1[2]=2
p2=-1, p=1
nums1 = [1,2,2,3,5,6]
p2 < 0,结束
结果:[1,2,2,3,5,6] ✓
题目30:第一个错误的版本(First Bad Version)⭐⭐⭐¶
题目链接:LeetCode 278
难度:简单
问题描述: 你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
解法:二分查找(推荐)⭐¶
def first_bad_version(n):
"""
第一个错误的版本
时间:O(log n)
空间:O(1)
"""
left, right = 1, n
while left < right:
mid = left + (right - left) // 2
if is_bad_version(mid):
# mid是错误版本,第一个错误版本在[left, mid]
right = mid
else:
# mid是正确版本,第一个错误版本在[mid+1, right]
left = mid + 1
return left
题目31:搜索插入位置(Search Insert Position)⭐⭐⭐¶
题目链接:LeetCode 35
难度:简单
问题描述: 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
解法:二分查找(推荐)⭐¶
def search_insert(nums, target):
"""
搜索插入位置
时间:O(log n)
空间:O(1)
"""
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
题目32:搜索二维矩阵(Search a 2D Matrix)⭐⭐⭐⭐¶
题目链接:LeetCode 74
难度:中等
问题描述: 编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性: - 每行中的整数从左到右按升序排列 - 每行的第一个整数大于前一行的最后一个整数
示例:
解法:二分查找(推荐)⭐¶
def search_matrix(matrix, target):
"""
搜索二维矩阵
时间:O(log(m * n))
空间:O(1)
"""
if not matrix or not matrix[0]:
return False
m, n = len(matrix), len(matrix[0])
left, right = 0, m * n - 1
while left <= right:
mid = left + (right - left) // 2
# 将一维索引转换为二维坐标
row = mid // n
col = mid % n
if matrix[row][col] == target:
return True
elif matrix[row][col] < target:
left = mid + 1
else:
right = mid - 1
return False
题目33:在排序数组中查找元素的第一个和最后一个位置⭐⭐⭐⭐⭐¶
题目链接:LeetCode 34
难度:中等
问题描述: 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值 target,返回 [-1, -1]。
解法:二分查找(推荐)⭐¶
def search_range(nums, target):
"""
查找元素的第一个和最后一个位置
时间:O(log n)
空间:O(1)
"""
def find_left():
"""找第一个位置"""
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
def find_right():
"""找最后一个位置"""
left, right = 0, len(nums)
while left < right:
mid = left + (right - left) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid
return left - 1
if not nums:
return [-1, -1]
left_idx = find_left()
right_idx = find_right()
if left_idx <= right_idx and right_idx < len(nums) and nums[left_idx] == target:
return [left_idx, right_idx]
return [-1, -1]
题目34:寻找峰值(Find Peak Element)⭐⭐⭐⭐¶
题目链接:LeetCode 162
难度:中等
问题描述: 峰值元素是指其值严格大于左右相邻值的元素。给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。你可以假设 nums[-1] = nums[n] = -∞。
解法:二分查找(推荐)⭐¶
def find_peak_element(nums):
"""
寻找峰值
时间:O(log n)
空间:O(1)
"""
if not nums:
return -1
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[mid + 1]:
# 峰值在[left, mid]
right = mid
else:
# 峰值在[mid+1, right]
left = mid + 1
return left
📝 总结¶
刷题策略¶
1. 按难度刷题¶
第1阶段(简单题):
- 两数之和、买卖股票、最大子数组和
- 反转链表、合并两个有序链表
- 爬楼梯、打家劫舍
第2阶段(中等题):
- 三数之和、合并区间
- 验证BST、层序遍历
- 零钱兑换、最长递增子序列
第3阶段(困难题):
- (根据自己需求选择)
2. 按标签刷题¶
3. 重复刷题¶
面试高频题Top 20¶
- 两数之和 ⭐⭐⭐⭐⭐
- 买卖股票的最佳时机 ⭐⭐⭐⭐⭐
- 最大子数组和 ⭐⭐⭐⭐⭐
- 三数之和 ⭐⭐⭐⭐⭐
- 反转链表 ⭐⭐⭐⭐⭐
- 合并两个有序链表 ⭐⭐⭐⭐⭐
- 环形链表II ⭐⭐⭐⭐⭐
- 二叉树的最大深度 ⭐⭐⭐⭐
- 验证二叉搜索树 ⭐⭐⭐⭐⭐
- 层序遍历 ⭐⭐⭐⭐⭐
- 爬楼梯 ⭐⭐⭐⭐⭐
- 打家劫舍 ⭐⭐⭐⭐⭐
- 零钱兑换 ⭐⭐⭐⭐⭐
- 最长递增子序列 ⭐⭐⭐⭐⭐
- 课程表 ⭐⭐⭐⭐⭐
- 合并区间 ⭐⭐⭐⭐
- 搜索插入位置 ⭐⭐⭐⭐
- 在排序数组中查找元素的第一个和最后一个位置 ⭐⭐⭐⭐⭐
- 除自身以外数组的乘积 ⭐⭐⭐⭐
- 搜索二维矩阵 ⭐⭐⭐⭐
继续努力!每天刷2-3题,2-3个月后就能轻松应对大厂面试!💪