本文系统性地梳理了算法题的核心类型与解题思路,重点聚焦LeetCode Hot 100与牛客Top 101中的高频题目
一、算法学习的基础认知
1.1 复杂度分析基础
在深入学习具体算法之前,我们必须理解算法效率的衡量标准------时间复杂度和空间复杂度。这两个概念是评估算法优劣的核心指标。
时间复杂度的本质
时间复杂度并非指代码的实际运行时间,而是描述算法执行时间随数据规模增长的变化趋势,也称为渐进时间复杂度。用大O表示法记为T(n) = O(f(n)),其中n表示数据规模,f(n)表示代码执行次数与n的关系函数。
在计算时间复杂度时,我们遵循以下原则:
- 只保留最高阶项
- 忽略常数项和系数
- 从内向外分析,从最深层开始
常见时间复杂度从优到劣排序:
| 复杂度 | 名称 | 说明 | 典型算法 |
|---|---|---|---|
| O(1) | 常数阶 | 执行时间不随n变化 | 数组按索引访问、哈希表查找 |
| O(log n) | 对数阶 | 每次减半问题规模 | 二分查找、平衡二叉树操作 |
| O(n) | 线性阶 | 执行次数与n成正比 | 线性查找、单层循环 |
| O(n log n) | 线性对数阶 | 高效排序的标准 | 快速排序、归并排序、堆排序 |
| O(n²) | 平方阶 | 双层嵌套循环 | 冒泡排序、插入排序、选择排序 |
| O(n³) | 立方阶 | 三层嵌套循环 | 某些动态规划问题 |
| O(2ⁿ) | 指数阶 | 问题规模指数增长 | 递归求解斐波那契数列、回溯法 |
| O(n!) | 阶乘阶 | 全排列问题 | 旅行商问题的暴力解法 |
空间复杂度分析
空间复杂度是算法在运行过程中临时占用存储空间大小的量度,同样使用大O表示法。我们主要关注算法需要分配的额外空间,不包括输入数据本身占用的空间。
递归算法的空间复杂度需要特别注意递归调用栈的深度。例如,递归计算斐波那契数列的空间复杂度为O(n),因为最多有n层递归调用栈。
1.2 LeetCode Hot 100与牛客Top 101题目分布概览
为了更有针对性地学习,我们首先了解这两个高频题库的题型分布特点:
LeetCode Hot 100题型统计
25% 12% 18% 15% 8% 7% 6% 5% 4% LeetCode Hot 100 题型分布 数组与字符串 链表 二叉树 动态规划 回溯与递归 图论与搜索 栈与队列 哈希表 其他
牛客Top 101题型统计
| 题型 | 题目数量 | 占比 | 难度分布 |
|---|---|---|---|
| 链表 | 15 | 14.9% | 简单-中等 |
| 二叉树 | 17 | 16.8% | 简单-困难 |
| 动态规划 | 16 | 15.8% | 中等-困难 |
| 递归/回溯 | 10 | 9.9% | 中等-困难 |
| 排序 | 9 | 8.9% | 简单-中等 |
| 双指针 | 8 | 7.9% | 简单-中等 |
| 二分查找 | 7 | 6.9% | 中等 |
| 哈希 | 6 | 5.9% | 简单-中等 |
| 堆/栈/队列 | 7 | 6.9% | 中等 |
| 其他 | 6 | 5.9% | 不定 |
从分布可以看出,数据结构类题目 (链表、树、栈/队列)和核心算法思想(动态规划、回溯、贪心)是高频考点的主体,这也将是本文重点讲解的内容。
二、基础数据结构类题目
2.1 数组与矩阵
核心特点
数组是最基础的数据结构,在内存中连续存储,支持O(1)时间的随机访问。
数组类题目通常考查对索引的灵活运用、边界条件处理、以及各种遍历技巧。
常见题型与解题思路
2.1.1 双指针技巧
双指针是数组题目中最常用的技巧之一,根据指针移动方式可分为:
对撞指针(两端向中间)
- 适用场景: 有序数组查找、回文判断、容器问题
- 经典题目:
- 两数之和II(LeetCode 167)
- 盛最多水的容器(LeetCode 11)
- 三数之和(LeetCode 15)
python
# 对撞指针模板
def two_pointers_converge(arr):
left, right = 0, len(arr) - 1
while left < right:
# 根据条件移动指针
if 满足某条件:
left += 1
else:
right -= 1
return result
快慢指针(同向移动)
- 适用场景: 数组去重、移动元素、滑动窗口
- 经典题目:
- 删除有序数组中的重复项(LeetCode 26)
- 移动零(LeetCode 283)
- 颜色分类(LeetCode 75)
python
# 快慢指针模板
def two_pointers_same_direction(arr):
slow = 0
for fast in range(len(arr)):
if 满足保留条件:
arr[slow] = arr[fast]
slow += 1
return slow # slow即为新数组长度
2.1.2 前缀和技巧
前缀和是一种预处理技巧,通过构建前缀和数组,可以在O(1)时间内计算任意区间的和。
原理
plain
原数组: [a₀, a₁, a₂, ..., aₙ]
前缀和: prefix[i] = a₀ + a₁ + ... + aᵢ₋₁
区间和: sum(i, j) = prefix[j+1] - prefix[i]
经典题目
- 和为K的子数组(LeetCode 560): 前缀和 + 哈希表
- 矩阵区域和检索(LeetCode 304): 二维前缀和
python
# 一维前缀和模板
def prefix_sum(nums):
n = len(nums)
prefix = [0] * (n + 1)
for i in range(n):
prefix[i + 1] = prefix[i] + nums[i]
return prefix
# 查询区间[i, j]的和
def range_sum(prefix, i, j):
return prefix[j + 1] - prefix[i]
2.1.3 滑动窗口
滑动窗口是处理连续子数组/子串问题的利器,本质上是双指针的一种特殊应用。
适用条件
- 问题涉及连续子数组/子串
- 需要找满足某条件的最长/最短子数组
- 具有单调性(扩大窗口使条件变差,缩小窗口使条件变好)
核心框架
python
def sliding_window(s):
left = 0
window = {} # 窗口内的数据
result = 0
for right in range(len(s)):
# 扩大窗口
c = s[right]
window[c] = window.get(c, 0) + 1
# 收缩窗口
while 窗口需要收缩:
d = s[left]
window[d] -= 1
left += 1
# 更新结果
result = max(result, right - left + 1)
return result
经典题目
- 无重复字符的最长子串(LeetCode 3)
- 最小覆盖子串(LeetCode 76)
- 找到字符串中所有字母异位词(LeetCode 438)
- 长度最小的子数组(LeetCode 209)
时间复杂度分析
虽然有嵌套循环,但left和right都只会移动n次,因此时间复杂度是O(n)。
2.1.4 矩阵遍历
矩阵题目主要考查二维数组的遍历技巧和空间优化。
常见模式
- 螺旋遍历 : 按顺时针螺旋顺序访问矩阵元素
- 螺旋矩阵(LeetCode 54)
- 螺旋矩阵II(LeetCode 59)
- 对角线遍历 : 沿对角线方向访问
- 对角线遍历(LeetCode 498)
- 原地修改 : 利用矩阵本身空间进行标记
- 矩阵置零(LeetCode 73)
- 旋转图像(LeetCode 48)
python
# 螺旋遍历模板
def spiral_order(matrix):
if not matrix: return []
result = []
top, bottom = 0, len(matrix) - 1
left, right = 0, len(matrix[0]) - 1
while top <= bottom and left <= right:
# 从左到右
for j in range(left, right + 1):
result.append(matrix[top][j])
top += 1
# 从上到下
for i in range(top, bottom + 1):
result.append(matrix[i][right])
right -= 1
if top <= bottom:
# 从右到左
for j in range(right, left - 1, -1):
result.append(matrix[bottom][j])
bottom -= 1
if left <= right:
# 从下到上
for i in range(bottom, top - 1, -1):
result.append(matrix[i][left])
left += 1
return result
2.2 链表
核心特点
链表是一种线性数据结构,元素在内存中不连续存储,通过指针连接。
与数组相比,链表的优势在于插入和删除操作的O(1)时间复杂度(给定节点位置),劣势是不支持随机访问,查找需要O(n)时间。
链表题目的通用技巧
2.2.1 虚拟头节点(哨兵节点)
虚拟头节点是链表题目中最重要的技巧之一,可以统一处理头节点和其他节点,避免大量边界判断。
python
# 标准模板
def process_linked_list(head):
dummy = ListNode(0) # 虚拟头节点
dummy.next = head
# 进行链表操作
# ...
return dummy.next # 返回真正的头节点
应用场景
- 删除节点(包括可能删除头节点)
- 合并链表
- 反转链表的部分或全部
2.2.2 快慢指针
快慢指针是链表题目中的另一大核心技巧,通过两个移动速度不同的指针解决多种问题。
典型应用
- 找中点(快指针走2步,慢指针走1步)
python
def find_middle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow # slow指向中点
- 检测环(Floyd判圈算法)
python
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
- 找环的入口
python
def detect_cycle(head):
slow = fast = head
# 第一阶段:判断是否有环
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None
# 第二阶段:找入口
slow = head
while slow != fast:
slow = slow.next
fast = fast.next
return slow
数学原理:设链表头到环入口距离为a,环入口到相遇点距离为b,环长为c,则:
- 快指针走过: a + b + n*c
- 慢指针走过: a + b
- 由于快指针速度是慢指针2倍: a + b + n*c = 2(a + b)
- 化简得: a = (n-1)*c + (c-b)
- 含义: 从头节点到入口的距离 = 从相遇点继续走到入口的距离
- 删除倒数第N个节点
python
def remove_nth_from_end(head, n):
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# fast先走n+1步
for _ in range(n + 1):
fast = fast.next
# fast和slow一起走,直到fast到达末尾
while fast:
fast = fast.next
slow = slow.next
# 删除slow.next
slow.next = slow.next.next
return dummy.next
2.2.3 链表反转
链表反转是面试中的经典题目,有多种变体。
1. 反转整个链表(迭代法)
python
def reverse_list(head):
prev = None
curr = head
while curr:
next_temp = curr.next # 保存下一个节点
curr.next = prev # 反转指针
prev = curr # prev前进
curr = next_temp # curr前进
return prev
2. 反转整个链表(递归法)
python
def reverse_list_recursive(head):
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
3. 反转链表的一部分(LeetCode 92)
python
def reverse_between(head, left, right):
dummy = ListNode(0)
dummy.next = head
pre = dummy
# 找到left前一个节点
for _ in range(left - 1):
pre = pre.next
# 反转left到right部分
curr = pre.next
for _ in range(right - left):
next_node = curr.next
curr.next = next_node.next
next_node.next = pre.next
pre.next = next_node
return dummy.next
4. K个一组翻转链表 (LeetCode 25,困难题)
这是链表反转的最难变体,需要结合多种技巧:
- 计算链表长度,判断是否需要反转
- 分组反转
- 连接各个反转后的组
python
def reverse_k_group(head, k):
dummy = ListNode(0)
dummy.next = head
pre = dummy
while True:
# 检查剩余节点是否够k个
tail = pre
for _ in range(k):
tail = tail.next
if not tail:
return dummy.next
# 记录下一组的前驱节点
next_group = tail.next
# 反转当前组
head, tail = reverse_one_group(pre.next, tail)
# 连接
pre.next = head
tail.next = next_group
pre = tail
def reverse_one_group(head, tail):
prev = tail.next
curr = head
while prev != tail:
next_temp = curr.next
curr.next = prev
prev = curr
curr = next_temp
return tail, head
2.2.4 合并链表
1. 合并两个有序链表(LeetCode 21)
python
def merge_two_lists(l1, l2):
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
2. 合并K个有序链表 (LeetCode 23,困难题)
使用最小堆(优先队列)维护K个链表的当前最小节点。
python
import heapq
def merge_k_lists(lists):
dummy = ListNode(0)
curr = dummy
heap = []
# 初始化堆
for i, l in enumerate(lists):
if l:
heapq.heappush(heap, (l.val, i, l))
# 不断取出最小值
while heap:
val, idx, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, idx, node.next))
return dummy.next
时间复杂度: O(N log K),其中N是所有节点总数,K是链表个数。每次堆操作的时间复杂度为O(log K),需要进行N次操作。
2.2.5 链表排序
归并排序 (LeetCode 148)
链表排序的最佳选择是归并排序,因为:
- 不需要随机访问
- 空间复杂度可以做到O(1)
- 时间复杂度稳定O(n log n)
python
def sort_list(head):
if not head or not head.next:
return head
# 快慢指针找中点
slow, fast = head, head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 分割链表
mid = slow.next
slow.next = None
# 递归排序
left = sort_list(head)
right = sort_list(mid)
# 合并
return merge(left, right)
def merge(l1, l2):
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
2.3 栈与队列
核心特点
- 栈: 后进先出(LIFO),主要操作为push和pop
- 队列: 先进先出(FIFO),主要操作为enqueue和dequeue
- 单调栈: 栈内元素保持单调性,用于解决下一个更大/更小元素问题
- 单调队列: 队列内元素保持单调性,常用于滑动窗口最值问题
2.3.1 栈的经典应用
1. 括号匹配 (LeetCode 20)
这是栈最直接的应用,利用栈的LIFO特性匹配成对括号。
python
def is_valid(s):
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
if not stack or stack[-1] != mapping[char]:
return False
stack.pop()
else:
stack.append(char)
return len(stack) == 0
2. 最小栈 (LeetCode 155)
要求实现一个栈,支持常数时间获取最小值。核心思路是用辅助栈记录每个状态的最小值。
python
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val):
self.stack.append(val)
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self):
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop()
def top(self):
return self.stack[-1]
def get_min(self):
return self.min_stack[-1]
3. 逆波兰表达式求值 (LeetCode 150)
栈天然适合处理后缀表达式。
python
def eval_rpn(tokens):
stack = []
operators = {'+', '-', '*', '/'}
for token in tokens:
if token in operators:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b)
elif token == '*':
stack.append(a * b)
else:
stack.append(int(a / b)) # 向零截断
else:
stack.append(int(token))
return stack[0]
2.3.2 单调栈
单调栈是一种特殊的栈,栈内元素保持单调递增或递减。主要用于解决"下一个更大元素"或"下一个更小元素"类问题。
核心思想
- 单调递减栈: 从栈底到栈顶递减,用于找下一个更大元素
- 单调递增栈: 从栈底到栈顶递增,用于找下一个更小元素
1. 下一个更大元素(LeetCode 496, 503, 739)
python
# 每日温度(LeetCode 739)
def daily_temperatures(temperatures):
n = len(temperatures)
result = [0] * n
stack = [] # 存储索引
for i in range(n):
while stack and temperatures[i] > temperatures[stack[-1]]:
prev_index = stack.pop()
result[prev_index] = i - prev_index
stack.append(i)
return result
工作原理
- 遍历数组,维护一个单调递减栈(存储索引)
- 当前元素大于栈顶元素时,说明找到了栈顶元素的"下一个更大元素"
- 不断弹出栈顶并记录答案,直到当前元素不再大于栈顶
- 将当前元素索引入栈
2. 接雨水 (LeetCode 42,困难题)
这是单调栈的经典应用,也可以用双指针解决。
python
def trap(height):
n = len(height)
if n < 3:
return 0
result = 0
stack = [] # 单调递减栈,存储索引
for i in range(n):
while stack and height[i] > height[stack[-1]]:
top = stack.pop()
if not stack:
break
left = stack[-1]
width = i - left - 1
h = min(height[left], height[i]) - height[top]
result += width * h
stack.append(i)
return result
原理解析
单调栈解法按层计算雨水:
- 栈维护可能形成积水的位置
- 当遇到更高的柱子时,计算之前凹陷处的积水
- 积水高度 = min(左柱子, 右柱子) - 凹陷处高度
- 积水宽度 = 右柱子位置 - 左柱子位置 - 1
3. 柱状图中最大的矩形(LeetCode 84,困难题)
python
def largest_rectangle_area(heights):
stack = []
max_area = 0
heights = [0] + heights + [0] # 前后加0,简化边界处理
for i in range(len(heights)):
while stack and heights[i] < heights[stack[-1]]:
h = heights[stack.pop()]
w = i - stack[-1] - 1
max_area = max(max_area, h * w)
stack.append(i)
return max_area
原理
- 维护单调递增栈
- 当前柱子矮于栈顶时,说明栈顶柱子的"右边界"找到了
- 栈顶下一个元素是"左边界"
- 矩形宽度 = 右边界 - 左边界 - 1
2.3.3 队列的应用
1. 用栈实现队列 (LeetCode 232)
用两个栈实现队列的所有操作。
python
class MyQueue:
def __init__(self):
self.stack_in = [] # 负责入队
self.stack_out = [] # 负责出队
def push(self, x):
self.stack_in.append(x)
def pop(self):
if not self.stack_out:
while self.stack_in:
self.stack_out.append(self.stack_in.pop())
return self.stack_out.pop()
def peek(self):
if not self.stack_out:
while self.stack_in:
self.stack_out.append(self.stack_in.pop())
return self.stack_out[-1]
def empty(self):
return not self.stack_in and not self.stack_out
均摊时间复杂度分析
- push: O(1)
- pop: 看似O(n),但每个元素最多被移动两次,因此均摊O(1)
2. 滑动窗口最大值 (LeetCode 239,困难题)
使用单调队列(双端队列)维护窗口内的最大值。
python
from collections import deque
def max_sliding_window(nums, k):
dq = deque() # 存储索引
result = []
for i in range(len(nums)):
# 移除窗口外的元素
if dq and dq[0] < i - k + 1:
dq.popleft()
# 维护单调递减队列
while dq and nums[dq[-1]] < nums[i]:
dq.pop()
dq.append(i)
# 窗口形成后记录最大值
if i >= k - 1:
result.append(nums[dq[0]])
return result
核心思想
- 队列保持单调递减(队首最大)
- 队首元素就是窗口的最大值
- 新元素入队时,从队尾移除所有比它小的元素(因为它们不可能是答案)
- 时间复杂度: O(n),每个元素最多入队出队各一次
2.4 哈希表
核心特点
哈希表通过哈希函数将键映射到数组索引,实现O(1)平均时间的查找、插入和删除。
是用空间换时间的典型数据结构。
常见应用场景
- 快速查找: 判断元素是否存在
- 计数统计: 统计元素出现次数
- 去重: 利用哈希表key的唯一性
- 建立映射关系: 存储key-value对应关系
2.4.1 经典哈希表题目
1. 两数之和 (LeetCode 1)
这是LeetCode的第一题,也是哈希表应用的经典例子。
python
def two_sum(nums, target):
hash_map = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hash_map:
return [hash_map[complement], i]
hash_map[num] = i
return []
优化思路
- 暴力解法: O(n²),两层循环
- 哈希表: O(n),用空间换时间,只需遍历一次
2. 字母异位词分组 (LeetCode 49)
核心是找到一个标准化的key来表示异位词。
python
from collections import defaultdict
def group_anagrams(strs):
hash_map = defaultdict(list)
for s in strs:
# 排序后的字符串作为key
key = ''.join(sorted(s))
hash_map[key].append(s)
return list(hash_map.values())
另一种key的构造方法(更高效)
python
def group_anagrams(strs):
hash_map = defaultdict(list)
for s in strs:
# 用字符计数作为key
count = [0] * 26
for c in s:
count[ord(c) - ord('a')] += 1
hash_map[tuple(count)].append(s)
return list(hash_map.values())
3. 最长连续序列 (LeetCode 128)
要求O(n)时间复杂度,因此不能排序。
python
def longest_consecutive(nums):
num_set = set(nums)
max_length = 0
for num in num_set:
# 只从序列的起点开始计数
if num - 1 not in num_set:
current_num = num
current_length = 1
while current_num + 1 in num_set:
current_num += 1
current_length += 1
max_length = max(max_length, current_length)
return max_length
关键优化
- 只从序列起点开始计数(num-1不在集合中)
- 避免重复计算
- 每个数字最多被访问两次,时间复杂度O(n)
4. LRU缓存 (LeetCode 146,中等偏难)
要求实现一个固定容量的缓存,支持O(1)时间的get和put操作,并在容量满时删除最久未使用的项。
python
class Node:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity):
self.cache = {} # key -> Node
self.capacity = capacity
# 虚拟头尾节点
self.head = Node()
self.tail = Node()
self.head.next = self.tail
self.tail.prev = self.head
def get(self, key):
if key not in self.cache:
return -1
node = self.cache[key]
self._move_to_head(node)
return node.value
def put(self, key, value):
if key in self.cache:
node = self.cache[key]
node.value = value
self._move_to_head(node)
else:
node = Node(key, value)
self.cache[key] = node
self._add_to_head(node)
if len(self.cache) > self.capacity:
removed = self._remove_tail()
del self.cache[removed.key]
def _add_to_head(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
def _remove_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _move_to_head(self, node):
self._remove_node(node)
self._add_to_head(node)
def _remove_tail(self):
node = self.tail.prev
self._remove_node(node)
return node
数据结构选择
- 哈希表: O(1)查找
- 双向链表: O(1)插入和删除
- 两者结合: 实现所有操作O(1)
三、树相关算法
3.1 二叉树基础
二叉树是最重要的非线性数据结构之一,每个节点最多有两个子节点。二叉树题目通常考查递归、遍历、路径问题等。
二叉树节点定义
python
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
3.1.1 二叉树遍历
1. 前序遍历(根-左-右)
递归实现:
python
def preorder_traversal(root):
result = []
def dfs(node):
if not node:
return
result.append(node.val) # 根
dfs(node.left) # 左
dfs(node.right) # 右
dfs(root)
return result
迭代实现(用栈):
python
def preorder_traversal_iterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
# 先右后左,因为栈是LIFO
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
2. 中序遍历(左-根-右)
递归实现:
python
def inorder_traversal(root):
result = []
def dfs(node):
if not node:
return
dfs(node.left) # 左
result.append(node.val) # 根
dfs(node.right) # 右
dfs(root)
return result
迭代实现:
python
def inorder_traversal_iterative(root):
result = []
stack = []
curr = root
while curr or stack:
# 一直向左走到底
while curr:
stack.append(curr)
curr = curr.left
# 处理栈顶节点
curr = stack.pop()
result.append(curr.val)
# 转向右子树
curr = curr.right
return result
重要性质
- 二叉搜索树的中序遍历结果是有序的
- 这个性质是BST相关题目的关键
3. 后序遍历(左-右-根)
递归实现:
python
def postorder_traversal(root):
result = []
def dfs(node):
if not node:
return
dfs(node.left) # 左
dfs(node.right) # 右
result.append(node.val) # 根
dfs(root)
return result
迭代实现(技巧:前序遍历的变体):
python
def postorder_traversal_iterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
# 先左后右(与前序相反)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return result[::-1] # 反转得到后序
4. 层序遍历(BFS)
层序遍历是按层访问节点,需要用队列实现。
python
from collections import deque
def level_order(root):
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
level_nodes = []
for _ in range(level_size):
node = queue.popleft()
level_nodes.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level_nodes)
return result
变体题目
- 二叉树的右视图(LeetCode 199): 每层最右边的节点
- 二叉树的锯齿形层序遍历(LeetCode 103): 奇数层正序,偶数层逆序
- 二叉树的层平均值(LeetCode 637): 计算每层平均值
3.1.2 二叉树的深度和高度
1. 最大深度(LeetCode 104)
递归解法:
python
def max_depth(root):
if not root:
return 0
return 1 + max(max_depth(root.left), max_depth(root.right))
迭代解法(BFS):
python
def max_depth_iterative(root):
if not root:
return 0
depth = 0
queue = deque([root])
while queue:
depth += 1
for _ in range(len(queue)):
node = queue.popleft()
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return depth
2. 最小深度 (LeetCode 111)
注意最小深度的定义:从根节点到最近叶子节点的路径长度。
python
def min_depth(root):
if not root:
return 0
# 如果只有一个子树,返回该子树的最小深度
if not root.left:
return 1 + min_depth(root.right)
if not root.right:
return 1 + min_depth(root.left)
# 两个子树都存在
return 1 + min(min_depth(root.left), min_depth(root.right))
3. 平衡二叉树 (LeetCode 110)
判断一棵树是否是平衡二叉树(任意节点的左右子树高度差不超过1)。
python
def is_balanced(root):
def height(node):
if not node:
return 0
left_height = height(node.left)
if left_height == -1:
return -1
right_height = height(node.right)
if right_height == -1:
return -1
if abs(left_height - right_height) > 1:
return -1
return 1 + max(left_height, right_height)
return height(root) != -1
优化技巧: 用-1表示不平衡,避免重复计算高度。
3.1.3 二叉树路径问题
1. 路径总和 (LeetCode 112)
判断是否存在根到叶子的路径,使得路径上所有节点值之和等于目标值。
python
def has_path_sum(root, target_sum):
if not root:
return False
if not root.left and not root.right:
return root.val == target_sum
return (has_path_sum(root.left, target_sum - root.val) or
has_path_sum(root.right, target_sum - root.val))
2. 路径总和II (LeetCode 113)
找出所有满足条件的路径。
python
def path_sum(root, target_sum):
result = []
def dfs(node, path, remain):
if not node:
return
path.append(node.val)
if not node.left and not node.right and remain == node.val:
result.append(path[:]) # 复制路径
dfs(node.left, path, remain - node.val)
dfs(node.right, path, remain - node.val)
path.pop() # 回溯
dfs(root, [], target_sum)
return result
3. 路径总和III (LeetCode 437,中等偏难)
路径不一定从根开始,也不一定到叶子结束。
python
def path_sum_iii(root, target_sum):
def count_paths(node, current_sum):
if not node:
return 0
current_sum += node.val
count = 0
# 以当前节点结束的路径数
if current_sum == target_sum:
count += 1
# 继续向下搜索
count += count_paths(node.left, current_sum)
count += count_paths(node.right, current_sum)
return count
if not root:
return 0
# 从当前节点开始的路径数
paths_from_root = count_paths(root, 0)
# 从左右子树开始的路径数
paths_from_left = path_sum_iii(root.left, target_sum)
paths_from_right = path_sum_iii(root.right, target_sum)
return paths_from_root + paths_from_left + paths_from_right
优化: 使用前缀和+哈希表,时间复杂度从O(n²)降到O(n)。
python
def path_sum_iii_optimized(root, target_sum):
prefix_sum_count = {0: 1} # 前缀和 -> 出现次数
def dfs(node, current_sum):
if not node:
return 0
current_sum += node.val
count = prefix_sum_count.get(current_sum - target_sum, 0)
prefix_sum_count[current_sum] = prefix_sum_count.get(current_sum, 0) + 1
count += dfs(node.left, current_sum)
count += dfs(node.right, current_sum)
prefix_sum_count[current_sum] -= 1 # 回溯
return count
return dfs(root, 0)
4. 二叉树的最大路径和 (LeetCode 124,困难题)
路径可以从任意节点开始到任意节点结束。
python
def max_path_sum(root):
max_sum = float('-inf')
def max_gain(node):
nonlocal max_sum
if not node:
return 0
# 只有正收益才选择该子树
left_gain = max(max_gain(node.left), 0)
right_gain = max(max_gain(node.right), 0)
# 当前节点作为路径顶点的最大路径和
path_sum = node.val + left_gain + right_gain
max_sum = max(max_sum, path_sum)
# 返回当前节点对父节点的贡献
return node.val + max(left_gain, right_gain)
max_gain(root)
return max_sum
关键点
- 递归函数返回的是单侧路径的最大值
- 在递归过程中更新全局最大值(考虑经过当前节点的完整路径)
3.2 二叉搜索树(BST)
定义: 二叉搜索树是一种特殊的二叉树,满足:
- 左子树所有节点值 < 根节点值
- 右子树所有节点值 > 根节点值
- 左右子树也分别是二叉搜索树
重要性质
- BST的中序遍历结果是升序的
- 这个性质是解决BST问题的关键
3.2.1 BST的验证和搜索
1. 验证二叉搜索树(LeetCode 98)
错误的做法:
python
# 错误! 只比较了父子节点
def is_valid_bst_wrong(root):
if not root:
return True
if root.left and root.left.val >= root.val:
return False
if root.right and root.right.val <= root.val:
return False
return is_valid_bst_wrong(root.left) and is_valid_bst_wrong(root.right)
正确的做法(维护取值范围):
python
def is_valid_bst(root):
def validate(node, min_val, max_val):
if not node:
return True
if node.val <= min_val or node.val >= max_val:
return False
return (validate(node.left, min_val, node.val) and
validate(node.right, node.val, max_val))
return validate(root, float('-inf'), float('inf'))
利用中序遍历:
python
def is_valid_bst_inorder(root):
prev = float('-inf')
stack = []
curr = root
while curr or stack:
while curr:
stack.append(curr)
curr = curr.left
curr = stack.pop()
if curr.val <= prev:
return False
prev = curr.val
curr = curr.right
return True
2. 二叉搜索树中的搜索(LeetCode 700)
python
def search_bst(root, val):
if not root or root.val == val:
return root
if val < root.val:
return search_bst(root.left, val)
else:
return search_bst(root.right, val)
迭代版本(空间复杂度O(1)):
python
def search_bst_iterative(root, val):
while root:
if val == root.val:
return root
elif val < root.val:
root = root.left
else:
root = root.right
return None
3. 二叉搜索树中第K小的元素(LeetCode 230)
利用BST中序遍历的性质:
python
def kth_smallest(root, k):
stack = []
curr = root
count = 0
while curr or stack:
while curr:
stack.append(curr)
curr = curr.left
curr = stack.pop()
count += 1
if count == k:
return curr.val
curr = curr.right
return -1
3.2.2 BST的修改操作
1. 插入节点(LeetCode 701)
python
def insert_into_bst(root, val):
if not root:
return TreeNode(val)
if val < root.val:
root.left = insert_into_bst(root.left, val)
else:
root.right = insert_into_bst(root.right, val)
return root
2. 删除节点(LeetCode 450,中等偏难)
删除节点是BST操作中最复杂的,需要分三种情况:
- 删除叶子节点: 直接删除
- 删除只有一个子节点的节点: 用子节点替代
- 删除有两个子节点的节点: 用后继节点(右子树最小值)或前驱节点(左子树最大值)替代
python
def delete_node(root, key):
if not root:
return None
if key < root.val:
root.left = delete_node(root.left, key)
elif key > root.val:
root.right = delete_node(root.right, key)
else:
# 找到要删除的节点
if not root.left:
return root.right
if not root.right:
return root.left
# 有两个子节点:用后继节点替代
successor = find_min(root.right)
root.val = successor.val
root.right = delete_node(root.right, successor.val)
return root
def find_min(node):
while node.left:
node = node.left
return node
3.2.3 BST的构造
1. 有序数组转BST (LeetCode 108)
要构造高度平衡的BST,选择中间元素作为根。
python
def sorted_array_to_bst(nums):
if not nums:
return None
mid = len(nums) // 2
root = TreeNode(nums[mid])
root.left = sorted_array_to_bst(nums[:mid])
root.right = sorted_array_to_bst(nums[mid+1:])
return root
2. 前序和中序遍历构造二叉树(LeetCode 105,中等偏难)
python
def build_tree(preorder, inorder):
if not preorder:
return None
root_val = preorder[0]
root = TreeNode(root_val)
# 在中序遍历中找到根节点位置
root_idx = inorder.index(root_val)
# 递归构造左右子树
root.left = build_tree(preorder[1:root_idx+1], inorder[:root_idx])
root.right = build_tree(preorder[root_idx+1:], inorder[root_idx+1:])
return root
优化: 使用哈希表存储中序遍历的索引,避免重复查找。
python
def build_tree_optimized(preorder, inorder):
inorder_map = {val: idx for idx, val in enumerate(inorder)}
def helper(pre_start, pre_end, in_start, in_end):
if pre_start > pre_end:
return None
root_val = preorder[pre_start]
root = TreeNode(root_val)
root_idx = inorder_map[root_val]
left_size = root_idx - in_start
root.left = helper(pre_start + 1, pre_start + left_size,
in_start, root_idx - 1)
root.right = helper(pre_start + left_size + 1, pre_end,
root_idx + 1, in_end)
return root
return helper(0, len(preorder) - 1, 0, len(inorder) - 1)
3.3 二叉树的高级操作
3.3.1 二叉树的序列化与反序列化
序列化 (LeetCode 297,困难题)
将树结构转换为字符串,并能从字符串恢复树结构。
前序遍历方式:
python
class Codec:
def serialize(self, root):
"""将树序列化为字符串"""
if not root:
return "null"
left = self.serialize(root.left)
right = self.serialize(root.right)
return f"{root.val},{left},{right}"
def deserialize(self, data):
"""从字符串反序列化树"""
def helper(values):
val = next(values)
if val == "null":
return None
node = TreeNode(int(val))
node.left = helper(values)
node.right = helper(values)
return node
values = iter(data.split(','))
return helper(values)
层序遍历方式:
python
class Codec:
def serialize(self, root):
if not root:
return ""
result = []
queue = deque([root])
while queue:
node = queue.popleft()
if node:
result.append(str(node.val))
queue.append(node.left)
queue.append(node.right)
else:
result.append("null")
return ",".join(result)
def deserialize(self, data):
if not data:
return None
values = data.split(',')
root = TreeNode(int(values[0]))
queue = deque([root])
i = 1
while queue:
node = queue.popleft()
if values[i] != "null":
node.left = TreeNode(int(values[i]))
queue.append(node.left)
i += 1
if values[i] != "null":
node.right = TreeNode(int(values[i]))
queue.append(node.right)
i += 1
return root
3.3.2 最近公共祖先
1. 二叉树的最近公共祖先(LeetCode 236,中等偏难)
python
def lowest_common_ancestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowest_common_ancestor(root.left, p, q)
right = lowest_common_ancestor(root.right, p, q)
# p和q分别在左右子树
if left and right:
return root
# p和q都在左子树或都在右子树
return left if left else right
核心思想
- 如果p和q分别在root的左右子树,root就是LCA
- 如果都在左子树,LCA也在左子树
- 如果都在右子树,LCA也在右子树
2. 二叉搜索树的最近公共祖先 (LeetCode 235)
利用BST的性质可以简化算法。
python
def lowest_common_ancestor_bst(root, p, q):
while root:
# p和q都在左子树
if p.val < root.val and q.val < root.val:
root = root.left
# p和q都在右子树
elif p.val > root.val and q.val > root.val:
root = root.right
# p和q分别在两侧,或其中一个就是root
else:
return root
return None
四、搜索与图论
4.1 深度优先搜索(DFS)
核心思想
DFS采用"一条路走到黑"的策略,沿着一条路径尽可能深地搜索,直到无法继续,然后回溯到上一个节点,尝试其他路径。
实现方式
- 递归(隐式栈)
- 显式栈
经典应用场景
- 路径搜索
- 连通性问题
- 拓扑排序
- 岛屿问题
4.1.1 岛屿类问题
岛屿问题是DFS/BFS的经典应用,也是LeetCode和牛客的高频题型。
1. 岛屿数量(LeetCode 200)
python
def num_islands(grid):
if not grid:
return 0
m, n = len(grid), len(grid[0])
count = 0
def dfs(i, j):
# 边界检查和访问检查
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
return
# 标记为已访问
grid[i][j] = '0'
# 向四个方向DFS
dfs(i + 1, j)
dfs(i - 1, j)
dfs(i, j + 1)
dfs(i, j - 1)
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
count += 1
dfs(i, j)
return count
2. 岛屿的最大面积(LeetCode 695)
python
def max_area_of_island(grid):
m, n = len(grid), len(grid[0])
max_area = 0
def dfs(i, j):
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != 1:
return 0
grid[i][j] = 0
area = 1
area += dfs(i + 1, j)
area += dfs(i - 1, j)
area += dfs(i, j + 1)
area += dfs(i, j - 1)
return area
for i in range(m):
for j in range(n):
if grid[i][j] == 1:
max_area = max(max_area, dfs(i, j))
return max_area
3. 被围绕的区域 (LeetCode 130,中等偏难)
核心思路:从边界的'O'开始DFS,标记所有不会被包围的'O'。
python
def solve(board):
if not board:
return
m, n = len(board), len(board[0])
def dfs(i, j):
if i < 0 or i >= m or j < 0 or j >= n or 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)
# 从边界DFS
for i in range(m):
dfs(i, 0)
dfs(i, n - 1)
for j in range(n):
dfs(0, j)
dfs(m - 1, j)
# 更新棋盘
for i in range(m):
for j in range(n):
if board[i][j] == 'T':
board[i][j] = 'O'
elif board[i][j] == 'O':
board[i][j] = 'X'
4.1.2 单词搜索
单词搜索 (LeetCode 79)
在二维网格中搜索单词,可以上下左右移动,但不能重复使用同一个格子。
python
def exist(board, word):
m, n = len(board), len(board[0])
def dfs(i, j, k):
# k是word中当前要匹配的字符索引
if k == len(word):
return True
if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]:
return False
# 标记当前格子已访问
temp = board[i][j]
board[i][j] = '#'
# 向四个方向搜索
found = (dfs(i + 1, j, k + 1) or
dfs(i - 1, j, k + 1) or
dfs(i, j + 1, k + 1) or
dfs(i, j - 1, k + 1))
# 回溯
board[i][j] = temp
return found
for i in range(m):
for j in range(n):
if dfs(i, j, 0):
return True
return False
关键点
- 原地修改board来标记访问状态,避免额外空间
- 回溯时恢复现场
- 剪枝:如果当前字符不匹配就立即返回
4.2 广度优先搜索(BFS)
核心思想
BFS采用"一层一层"的搜索策略,先访问距离起点近的节点,再访问远的节点。
实现方式
使用队列
适用场景
- 最短路径问题(无权图)
- 层次遍历
- 连通性判断
4.2.1 BFS求最短路径
1. 二进制矩阵中的最短路径(LeetCode 1091)
python
from collections import deque
def shortest_path_binary_matrix(grid):
n = len(grid)
if grid[0][0] == 1 or grid[n-1][n-1] == 1:
return -1
if n == 1:
return 1
queue = deque([(0, 0, 1)]) # (row, col, distance)
grid[0][0] = 1 # 标记为已访问
directions = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
while queue:
row, col, dist = queue.popleft()
for dr, dc in directions:
r, c = row + dr, col + dc
if r == n-1 and c == n-1:
return dist + 1
if 0 <= r < n and 0 <= c < n and grid[r][c] == 0:
grid[r][c] = 1
queue.append((r, c, dist + 1))
return -1
2. 单词接龙 (LeetCode 127,困难题)
从beginWord变换到endWord,每次只能改变一个字母,且中间词必须在字典中。
python
def ladder_length(begin_word, end_word, word_list):
if end_word not in word_list:
return 0
word_set = set(word_list)
queue = deque([(begin_word, 1)])
while queue:
word, length = queue.popleft()
if word == end_word:
return length
# 尝试改变每个位置的字母
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in word_set:
word_set.remove(next_word)
queue.append((next_word, length + 1))
return 0
优化:双向BFS
从起点和终点同时BFS,可以大幅减少搜索空间。
python
def ladder_length_bidirectional(begin_word, end_word, word_list):
if end_word not in word_list:
return 0
word_set = set(word_list)
begin_set = {begin_word}
end_set = {end_word}
length = 1
while begin_set and end_set:
# 总是从较小的集合开始扩展
if len(begin_set) > len(end_set):
begin_set, end_set = end_set, begin_set
next_set = set()
for word in begin_set:
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
next_word = word[:i] + c + word[i+1:]
if next_word in end_set:
return length + 1
if next_word in word_set:
word_set.remove(next_word)
next_set.add(next_word)
begin_set = next_set
length += 1
return 0
4.3 拓扑排序
定义: 对有向无环图(DAG)的所有顶点进行线性排序,使得对于任何有向边u->v,u在排序中都在v之前。
应用场景
- 课程安排
- 任务调度
- 编译顺序
算法思路(Kahn算法)
- 统计每个节点的入度
- 将入度为0的节点加入队列
- 从队列中取出节点,将其指向的节点入度-1
- 重复步骤2-3,直到队列为空
课程表(LeetCode 207)
python
from collections import deque, defaultdict
def can_finish(num_courses, prerequisites):
# 构建图和入度表
graph = defaultdict(list)
in_degree = [0] * num_courses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
# BFS
queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
count = 0
while queue:
course = queue.popleft()
count += 1
for next_course in graph[course]:
in_degree[next_course] -= 1
if in_degree[next_course] == 0:
queue.append(next_course)
return count == num_courses
课程表II (LeetCode 210)
在判断能否完成的基础上,返回一个可行的课程顺序。
python
def find_order(num_courses, prerequisites):
graph = defaultdict(list)
in_degree = [0] * num_courses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
queue = deque([i for i in range(num_courses) if in_degree[i] == 0])
result = []
while queue:
course = queue.popleft()
result.append(course)
for next_course in graph[course]:
in_degree[next_course] -= 1
if in_degree[next_course] == 0:
queue.append(next_course)
return result if len(result) == num_courses else []
五、回溯算法
5.1 回溯算法核心思想
定义: 回溯算法是一种通过探索所有可能的候选解来找出所有解的算法。如果候选解不可行,则放弃该候选解,回溯到上一步,尝试其他可能的解。
本质: 暴力搜索 + 剪枝
适用场景
- 求所有解(组合、排列、子集)
- 求可行解(N皇后、数独)
- 求最优解(结合剪枝)
回溯算法模板
python
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择 # 回溯
5.2 组合问题
1. 组合 (LeetCode 77)
从1到n中选k个数的所有组合。
python
def combine(n, k):
result = []
def backtrack(start, path):
# 剪枝:剩余元素不足
if len(path) + (n - start + 1) < k:
return
if len(path) == k:
result.append(path[:])
return
for i in range(start, n + 1):
path.append(i)
backtrack(i + 1, path)
path.pop()
backtrack(1, [])
return result
关键点
- start参数避免重复组合
- 剪枝提高效率
2. 组合总和 (LeetCode 39)
给定数组(无重复元素),找出和为target的所有组合,同一个数可以重复使用。
python
def combination_sum(candidates, target):
result = []
candidates.sort() # 排序便于剪枝
def backtrack(start, path, remain):
if remain == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remain:
break # 剪枝
path.append(candidates[i])
backtrack(i, path, remain - candidates[i]) # 注意是i不是i+1
path.pop()
backtrack(0, [], target)
return result
3. 组合总和II (LeetCode 40)
数组有重复元素,每个元素只能使用一次,但结果不能有重复组合。
python
def combination_sum2(candidates, target):
result = []
candidates.sort()
def backtrack(start, path, remain):
if remain == 0:
result.append(path[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remain:
break
# 去重:同一层不能使用相同的元素
if i > start and candidates[i] == candidates[i-1]:
continue
path.append(candidates[i])
backtrack(i + 1, path, remain - candidates[i]) # i+1表示不重复使用
path.pop()
backtrack(0, [], target)
return result
去重技巧
i > start and candidates[i] == candidates[i-1]: 同一层跳过重复元素- 需要先排序
5.3 排列问题
1. 全排列(LeetCode 46)
python
def permute(nums):
result = []
n = len(nums)
used = [False] * n
def backtrack(path):
if len(path) == n:
result.append(path[:])
return
for i in range(n):
if used[i]:
continue
used[i] = True
path.append(nums[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return result
2. 全排列II (LeetCode 47)
数组包含重复元素,但结果不能有重复排列。
python
def permute_unique(nums):
result = []
nums.sort()
n = len(nums)
used = [False] * n
def backtrack(path):
if len(path) == n:
result.append(path[:])
return
for i in range(n):
if used[i]:
continue
# 去重:如果当前元素与前一个元素相同,且前一个元素未被使用,则跳过
if i > 0 and nums[i] == nums[i-1] and not used[i-1]:
continue
used[i] = True
path.append(nums[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return result
去重原理
- 排序后,相同元素相邻
not used[i-1]保证相同元素按顺序使用- 避免[1a, 2, 1b]和[1b, 2, 1a]都出现
5.4 子集问题
1. 子集(LeetCode 78)
python
def subsets(nums):
result = []
def backtrack(start, path):
result.append(path[:]) # 每个状态都是一个子集
for i in range(start, len(nums)):
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
2. 子集II (LeetCode 90)
数组包含重复元素。
python
def subsets_with_dup(nums):
result = []
nums.sort()
def backtrack(start, path):
result.append(path[:])
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i-1]:
continue
path.append(nums[i])
backtrack(i + 1, path)
path.pop()
backtrack(0, [])
return result
5.5 其他经典回溯问题
1. 括号生成 (LeetCode 22)
生成n对括号的所有合法组合。
python
def generate_parenthesis(n):
result = []
def backtrack(path, left, right):
if len(path) == 2 * n:
result.append(''.join(path))
return
# 左括号:只要没用完就可以加
if left < n:
path.append('(')
backtrack(path, left + 1, right)
path.pop()
# 右括号:只有当右括号少于左括号时才能加
if right < left:
path.append(')')
backtrack(path, left, right + 1)
path.pop()
backtrack([], 0, 0)
return result
关键点
- 不是盲目枚举,而是根据括号配对规则剪枝
right < left保证括号合法性
2. N皇后(LeetCode 51,困难题)
python
def solve_n_queens(n):
result = []
board = [['.'] * n for _ in range(n)]
cols = set()
diag1 = set() # 左上到右下对角线
diag2 = set() # 右上到左下对角线
def backtrack(row):
if row == n:
result.append([''.join(row) for row in board])
return
for col in range(n):
# 检查是否可以放置皇后
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue
# 放置皇后
board[row][col] = 'Q'
cols.add(col)
diag1.add(row - col)
diag2.add(row + col)
backtrack(row + 1)
# 回溯
board[row][col] = '.'
cols.remove(col)
diag1.remove(row - col)
diag2.remove(row + col)
backtrack(0)
return result
对角线判断技巧
- 左上到右下:
row - col相同 - 右上到左下:
row + col相同
3. 解数独(LeetCode 37,困难题)
python
def solve_sudoku(board):
rows = [set() for _ in range(9)]
cols = [set() for _ in range(9)]
boxes = [set() for _ in range(9)]
empty = []
# 初始化
for i in range(9):
for j in range(9):
if board[i][j] == '.':
empty.append((i, j))
else:
digit = board[i][j]
rows[i].add(digit)
cols[j].add(digit)
boxes[(i // 3) * 3 + j // 3].add(digit)
def backtrack(idx):
if idx == len(empty):
return True
i, j = empty[idx]
box_idx = (i // 3) * 3 + j // 3
for digit in '123456789':
if digit in rows[i] or digit in cols[j] or digit in boxes[box_idx]:
continue
# 放置数字
board[i][j] = digit
rows[i].add(digit)
cols[j].add(digit)
boxes[box_idx].add(digit)
if backtrack(idx + 1):
return True
# 回溯
board[i][j] = '.'
rows[i].remove(digit)
cols[j].remove(digit)
boxes[box_idx].remove(digit)
return False
backtrack(0)
六、动态规划
6.1 动态规划核心思想
定义: 动态规划是一种将复杂问题分解为子问题,通过求解子问题并存储其结果,避免重复计算,从而高效求解原问题的算法思想。
与递归、分治的区别
- 递归: 自顶向下,可能有大量重复计算
- 分治: 子问题相互独立
- 动态规划: 子问题重叠,通过记忆化避免重复计算
动态规划的三要素
- 最优子结构: 问题的最优解包含子问题的最优解
- 重叠子问题: 子问题会被多次求解
- 无后效性: 子问题的解一旦确定,就不会再改变
动态规划的解题步骤
- 定义状态: dp[i]或dp[i][j]表示什么
- 找状态转移方程: 如何从小问题推导到大问题
- 初始化: 确定边界条件
- 确定计算顺序: 保证计算dp[i]时,所需的子问题已经计算完成
- 优化空间复杂度(可选): 滚动数组等技巧
6.2 一维DP
6.2.1 爬楼梯类问题
1. 爬楼梯 (LeetCode 70)
每次可以爬1或2个台阶,爬到第n阶有多少种方法?
状态定义 : dp[i]表示爬到第i阶的方法数
转移方程 : dp[i] = dp[i-1] + dp[i-2]
初始条件: dp[0] = 1, dp[1] = 1
python
def climb_stairs(n):
if n <= 1:
return 1
dp = [0] * (n + 1)
dp[0], dp[1] = 1, 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
空间优化: 只需保存前两个状态
python
def climb_stairs_optimized(n):
if n <= 1:
return 1
prev2, prev1 = 1, 1
for i in range(2, n + 1):
curr = prev1 + prev2
prev2 = prev1
prev1 = curr
return prev1
变体: 如果每次可以爬1、2或3个台阶呢?
python
# 转移方程变为: dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
# 需要保存前三个状态
6.2.2 打家劫舍系列
1. 打家劫舍 (LeetCode 198)
不能抢劫相邻的房子,求最大金额。
状态定义 : dp[i]表示抢劫前i个房子能获得的最大金额
转移方程 : dp[i] = max(dp[i-1], dp[i-2] + nums[i])
含义:
- dp[i-1]: 不抢第i个房子
- dp[i-2] + nums[i]: 抢第i个房子
python
def rob(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]
n = len(nums)
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
return dp[n-1]
空间优化:
python
def rob_optimized(nums):
prev2, prev1 = 0, 0
for num in nums:
curr = max(prev1, prev2 + num)
prev2 = prev1
prev1 = curr
return prev1
2. 打家劫舍II (LeetCode 213)
房子围成一圈,首尾房子相邻,不能同时抢。
核心思路: 分两种情况
- 抢第一个房子,不能抢最后一个: [0, n-2]
- 不抢第一个房子,可以抢最后一个: [1, n-1]
python
def rob_ii(nums):
n = len(nums)
if n == 1:
return nums[0]
if n == 2:
return max(nums[0], nums[1])
def rob_range(start, end):
prev2, prev1 = 0, 0
for i in range(start, end + 1):
curr = max(prev1, prev2 + nums[i])
prev2 = prev1
prev1 = curr
return prev1
return max(rob_range(0, n-2), rob_range(1, n-1))
3. 打家劫舍III (LeetCode 337)
房子分布在二叉树上,相邻节点不能同时抢。
状态定义: 返回(不抢当前节点的最大值, 抢当前节点的最大值)
python
def rob_iii(root):
def dfs(node):
if not node:
return (0, 0)
left = dfs(node.left)
right = dfs(node.right)
# 不抢当前节点:子节点可抢可不抢,取最大值
not_rob = max(left) + max(right)
# 抢当前节点:子节点一定不抢
rob = node.val + left[0] + right[0]
return (not_rob, rob)
return max(dfs(root))
6.2.3 最大子数组和
最大子数组和(LeetCode 53)
状态定义 : dp[i]表示以nums[i]结尾的最大子数组和
转移方程 : dp[i] = max(nums[i], dp[i-1] + nums[i])
含义:
- nums[i]: 从当前元素重新开始
- dp[i-1] + nums[i]: 延续之前的子数组
python
def max_sub_array(nums):
if not nums:
return 0
dp = [0] * len(nums)
dp[0] = nums[0]
max_sum = dp[0]
for i in range(1, len(nums)):
dp[i] = max(nums[i], dp[i-1] + nums[i])
max_sum = max(max_sum, dp[i])
return max_sum
空间优化(Kadane算法):
python
def max_sub_array_kadane(nums):
max_sum = current_sum = nums[0]
for num in nums[1:]:
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
变体 : 最大子数组乘积(LeetCode 152)
由于有负数,需要同时维护最大值和最小值。
python
def max_product(nums):
max_prod = min_prod = result = nums[0]
for num in nums[1:]:
if num < 0:
max_prod, min_prod = min_prod, max_prod
max_prod = max(num, max_prod * num)
min_prod = min(num, min_prod * num)
result = max(result, max_prod)
return result
6.3 二维DP
6.3.1 路径问题
1. 不同路径 (LeetCode 62)
从左上角到右下角有多少条不同路径(只能向右或向下)。
状态定义 : dp[i][j]表示到达(i,j)的路径数
转移方程 : dp[i][j] = dp[i-1][j] + dp[i][j-1]
初始条件: 第一行和第一列都是1
python
def unique_paths(m, n):
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
空间优化: 只需要一行
python
def unique_paths_optimized(m, n):
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[n-1]
2. 不同路径II (LeetCode 63)
网格中有障碍物。
python
def unique_paths_with_obstacles(obstacle_grid):
m, n = len(obstacle_grid), len(obstacle_grid[0])
if obstacle_grid[0][0] == 1:
return 0
dp = [[0] * n for _ in range(m)]
dp[0][0] = 1
# 初始化第一列
for i in range(1, m):
dp[i][0] = 0 if obstacle_grid[i][0] == 1 else dp[i-1][0]
# 初始化第一行
for j in range(1, n):
dp[0][j] = 0 if obstacle_grid[0][j] == 1 else dp[0][j-1]
# 填表
for i in range(1, m):
for j in range(1, n):
if obstacle_grid[i][j] == 1:
dp[i][j] = 0
else:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
3. 最小路径和 (LeetCode 64)
找出路径和最小的路径。
python
def min_path_sum(grid):
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]
dp[0][0] = grid[0][0]
# 初始化第一行
for j in range(1, n):
dp[0][j] = dp[0][j-1] + grid[0][j]
# 初始化第一列
for i in range(1, m):
dp[i][0] = dp[i-1][0] + grid[i][0]
# 填表
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
return dp[m-1][n-1]
6.3.2 编辑距离
编辑距离 (LeetCode 72,困难题)
将word1转换为word2的最少操作数(插入、删除、替换)。
状态定义: dp[i][j]表示word1的前i个字符转换为word2的前j个字符的最少操作数
转移方程:
plain
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # 删除word1[i-1]
dp[i][j-1], # 插入word2[j-1]
dp[i-1][j-1] # 替换word1[i-1]为word2[j-1]
)
python
def min_distance(word1, word2):
m, n = len(word1), len(word2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
# 填表
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = 1 + min(
dp[i-1][j], # 删除
dp[i][j-1], # 插入
dp[i-1][j-1] # 替换
)
return dp[m][n]
6.3.3 最长公共子序列
最长公共子序列(LeetCode 1143)
状态定义: dp[i][j]表示text1的前i个字符和text2的前j个字符的LCS长度
转移方程:
plain
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
python
def longest_common_subsequence(text1, text2):
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
相关题目
- 最长公共子串: 要求连续,转移方程不同
- 最长递增子序列(LeetCode 300)
6.4 背包问题
背包问题是动态规划的经典应用,也是面试的高频考点。
6.4.1 0-1背包
问题描述: 有n个物品和容量为W的背包,每个物品有重量w[i]和价值v[i],每个物品只能选一次,求最大价值。
状态定义: dp[i][j]表示前i个物品,背包容量为j时的最大价值
转移方程:
plain
dp[i][j] = max(
dp[i-1][j], # 不选第i个物品
dp[i-1][j-w[i]] + v[i] # 选第i个物品
)
python
def knapsack_01(weights, values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, capacity + 1):
# 不选第i个物品
dp[i][j] = dp[i-1][j]
# 选第i个物品(如果放得下)
if j >= weights[i-1]:
dp[i][j] = max(dp[i][j],
dp[i-1][j-weights[i-1]] + values[i-1])
return dp[n][capacity]
空间优化: 一维数组,逆序遍历
python
def knapsack_01_optimized(weights, values, capacity):
dp = [0] * (capacity + 1)
for i in range(len(weights)):
# 逆序遍历,避免重复使用
for j in range(capacity, weights[i] - 1, -1):
dp[j] = max(dp[j], dp[j-weights[i]] + values[i])
return dp[capacity]
相关题目
- 分割等和子集(LeetCode 416): 判断能否分割为两个和相等的子集
- 目标和(LeetCode 494): 给数组元素添加+/-使和为target
6.4.2 完全背包
问题描述: 每个物品可以选无限次。
转移方程:
plain
dp[i][j] = max(
dp[i-1][j],
dp[i][j-w[i]] + v[i] # 注意是dp[i]而不是dp[i-1]
)
python
def knapsack_complete(weights, values, capacity):
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, capacity + 1):
dp[i][j] = dp[i-1][j]
if j >= weights[i-1]:
dp[i][j] = max(dp[i][j],
dp[i][j-weights[i-1]] + values[i-1])
return dp[n][capacity]
空间优化: 正序遍历(允许重复使用)
python
def knapsack_complete_optimized(weights, values, capacity):
dp = [0] * (capacity + 1)
for i in range(len(weights)):
# 正序遍历,允许重复使用
for j in range(weights[i], capacity + 1):
dp[j] = max(dp[j], dp[j-weights[i]] + values[i])
return dp[capacity]
相关题目
- 零钱兑换(LeetCode 322): 最少硬币数
- 零钱兑换II(LeetCode 518): 组合数
七、贪心算法
7.1 贪心算法核心思想
定义: 贪心算法在每一步选择中都采取当前状态下的最优选择,期望通过局部最优达到全局最优。
与动态规划的区别
- 贪心: 只看当前最优,不考虑后续影响
- 动态规划: 考虑所有可能,保存子问题的解
适用条件
- 贪心选择性质: 局部最优能导致全局最优
- 最优子结构: 问题的最优解包含子问题的最优解
- 无后效性: 当前选择不影响之前的选择
注意: 贪心算法不一定能得到全局最优解,需要通过数学证明或反例验证。
7.2 区间问题
区间问题是贪心算法的典型应用场景。
7.2.1 区间调度
无重叠区间 (LeetCode 435)
给定一组区间,找出需要移除的最少区间数,使剩余区间不重叠。
贪心策略: 按结束时间排序,优先选择结束早的区间
python
def erase_overlap_intervals(intervals):
if not intervals:
return 0
# 按结束时间排序
intervals.sort(key=lambda x: x[1])
count = 1 # 第一个区间一定选
end = intervals[0][1]
for i in range(1, len(intervals)):
# 如果不重叠,选择该区间
if intervals[i][0] >= end:
count += 1
end = intervals[i][1]
# 需要移除的区间数
return len(intervals) - count
正确性证明: 选择结束最早的区间,为后续区间留出更多空间。
相关题目
- 用最少数量的箭引爆气球(LeetCode 452)
- 会议室II(LeetCode 253): 需要的最少会议室数
7.2.2 区间合并
合并区间(LeetCode 56)
python
def merge(intervals):
if not intervals:
return []
# 按起始时间排序
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for i in range(1, len(intervals)):
# 如果当前区间与上一个区间重叠
if intervals[i][0] <= merged[-1][1]:
# 合并区间
merged[-1][1] = max(merged[-1][1], intervals[i][1])
else:
# 不重叠,添加新区间
merged.append(intervals[i])
return merged
7.3 贪心+排序
很多贪心问题需要先排序,然后根据某种策略选择。
分发饼干(LeetCode 455)
python
def find_content_children(g, s):
g.sort() # 孩子的胃口
s.sort() # 饼干尺寸
child = cookie = 0
while child < len(g) and cookie < len(s):
if s[cookie] >= g[child]:
child += 1
cookie += 1
return child
贪心策略: 用最小的饼干满足最小胃口的孩子
7.4 其他贪心问题
跳跃游戏 (LeetCode 55)
判断能否跳到最后一个位置。
python
def can_jump(nums):
max_reach = 0
for i in range(len(nums)):
# 如果当前位置不可达
if i > max_reach:
return False
# 更新最远可达位置
max_reach = max(max_reach, i + nums[i])
# 如果已经可以到达最后
if max_reach >= len(nums) - 1:
return True
return True
跳跃游戏II (LeetCode 45,中等偏难)
求到达最后位置的最少跳跃次数。
python
def jump(nums):
if len(nums) <= 1:
return 0
jumps = 0
current_end = 0
farthest = 0
for i in range(len(nums) - 1):
farthest = max(farthest, i + nums[i])
# 到达当前跳跃的边界
if i == current_end:
jumps += 1
current_end = farthest
# 如果已经可以到达最后
if current_end >= len(nums) - 1:
break
return jumps
八、其他重要算法
8.1 二分查找
核心思想: 在有序数组中,每次比较中间元素,将搜索范围缩小一半,时间复杂度O(log n)。
标准模板
python
def binary_search(arr, target):
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # 防止溢出
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
变体
1. 寻找左边界
python
def left_bound(arr, target):
left, right = 0, len(arr)
while left < right:
mid = left + (right - left) // 2
if arr[mid] < target:
left = mid + 1
else:
right = mid
return left
2. 寻找右边界
python
def right_bound(arr, target):
left, right = 0, len(arr)
while left < right:
mid = left + (right - left) // 2
if arr[mid <= target:
left = mid + 1
else:
right = mid
return left - 1
经典题目
- 搜索旋转排序数组(LeetCode 33)
- 寻找旋转排序数组中的最小值(LeetCode 153)
- 在排序数组中查找元素的第一个和最后一个位置(LeetCode 34)
8.2 排序算法
虽然实际编程中常用库函数,但理解排序算法原理对算法思维训练很有帮助。
常见排序算法性能对比
| 排序算法 | 平均时间 | 最坏时间 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
快速排序实现
python
def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2]
left = [x for x in arr if x < pivot]
middle = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
return quick_sort(left) + middle + quick_sort(right)
归并排序实现
python
def merge_sort(arr):
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
8.3 位运算
位运算在某些场景下可以大幅提升性能。
常用技巧
| 操作 | 实现 | 说明 |
|---|---|---|
| 判断奇偶 | n & 1 | 奇数为1,偶数为0 |
| 除以2 | n >> 1 | 右移一位 |
| 乘以2 | n << 1 | 左移一位 |
| 取反 | ~n | 按位取反 |
| 消除最后的1 | n & (n-1) | 常用于计数1的个数 |
| 获取最后的1 | n & (-n) | 提取最低位的1 |
经典题目
1. 只出现一次的数字(LeetCode 136)
python
def single_number(nums):
result = 0
for num in nums:
result ^= num # 异或
return result
原理: a ^ a = 0, a ^ 0 = a
2. 位1的个数(LeetCode 191)
python
def hamming_weight(n):
count = 0
while n:
n &= n - 1 # 消除最后一个1
count += 1
return count
3. 颠倒二进制位(LeetCode 190)
python
def reverse_bits(n):
result = 0
for _ in range(32):
result = (result << 1) | (n & 1)
n >>= 1
return result
九、算法学习路线与技巧总结
9.1 刷题顺序建议
基于LeetCode Hot 100和牛客Top 101的学习路线:
第一阶段:基础数据结构(2-3周)
- 数组与字符串(双指针、滑动窗口)
- 链表(虚拟头节点、快慢指针、反转)
- 栈和队列(单调栈、单调队列)
- 哈希表(两数之和系列)
第二阶段:树与图(2-3周)
- 二叉树遍历(递归与迭代)
- 二叉树路径问题
- 二叉搜索树
- DFS/BFS
- 岛屿问题
第三阶段:核心算法(3-4周)
- 回溯(组合、排列、子集、N皇后)
- 动态规划(一维DP → 二维DP → 背包问题)
- 贪心(区间问题、跳跃游戏)
- 二分查找
第四阶段:进阶与复习(2周)
- 困难题攻克
- 专题总结
- 模拟面试
9.2 解题技巧总结
通用思路
- 理解题意: 仔细读题,明确输入输出,考虑边界条件
- 暴力求解: 先想出暴力解法,分析时间复杂度
- 寻找优化 :
- 能否用哈希表空间换时间?
- 能否先排序?
- 能否用双指针优化?
- 是否有单调性可以二分?
- 分类讨论: 复杂问题分情况讨论
- 画图辅助: 画出数据结构的图形表示
- 写伪代码: 先用伪代码理清思路
- 编码实现: 注意边界条件和特殊情况
- 测试用例 :
- 正常用例
- 边界用例(空、单元素、极值)
- 特殊用例
Debug技巧
- 添加打印语句查看中间状态
- 使用调试器单步执行
- 用小数据手工模拟
- 检查数组越界
- 检查整数溢出
- 检查空指针
9.3 时间管理
刷题时间分配
- 简单题: 15-30分钟
- 中等题: 30-60分钟
- 困难题: 60-120分钟
如果卡住
- 15分钟无思路: 看提示
- 30分钟无进展: 看题解
- 看完题解:
- 理解思路
- 自己实现
- 第二天再做一遍
9.4 常见错误
思维错误
- 没有考虑边界条件
- 忘记回溯(修改后没有恢复)
- 混淆索引和长度
- 死循环(循环条件错误)
代码错误
- 数组越界
- 整数溢出
- 浮点数比较
- 深拷贝vs浅拷贝
- 引用传递问题
9.5 刷题心态
正确心态
- 刷题是练习思维,不是背答案
- 遇到困难很正常,不要气馁
- 重复刷题是必要的,第一遍不会很正常
- 重视理解原理,而非死记代码
刷题节奏
- 每天1-3题
- 周末复习一周的题目
- 每月总结专题
- 面试前集中复习
十、总结
算法学习是一个循序渐进的过程,需要理解原理、掌握模板、大量练习。
本文系统地梳理了LeetCode Hot 100和牛客Top 101中的核心题型:
数据结构类
- 数组: 双指针、滑动窗口、前缀和
- 链表: 快慢指针、反转、合并
- 栈队列: 单调栈、单调队列
- 哈希表: 快速查找、计数统计
- 树: 遍历、路径、BST、序列化
算法思想类
- 搜索: DFS(回溯)、BFS、拓扑排序
- 动态规划: 一维DP、二维DP、背包问题
- 贪心: 区间问题、跳跃游戏
- 分治: 归并排序、快速排序
- 二分: 标准二分、变体
核心建议
- 掌握基本数据结构的操作
- 理解算法思想的本质
- 熟记常用模板
- 大量练习形成肌肉记忆
- 定期复习巩固
参考文献
- LeetCode官方网站 - 提供了海量算法题目和社区讨论
https://leetcode.cn/ - 牛客网算法题库 - 国内优秀的算法练习平台
https://www.nowcoder.com/ - 《算法导论》(Introduction to Algorithms) - 算法领域的经典教材,全面系统地介绍了算法设计与分析
作者: Thomas H. Cormen等 - 《数据结构与算法之美》专栏 - 极客时间 - 通俗易懂的算法学习专栏
https://time.geekbang.org/column/intro/126 - LeetCode Hot 100题解汇总 - GitHub - 开源的题解集合
https://github.com/tonngw/LeetCode021 - OI Wiki - 算法竞赛知识整合站点 - 提供算法竞赛相关的知识和技巧
https://oi-wiki.org/ - Big O Cheat Sheet - 常用数据结构和算法的复杂度速查表
http://bigocheatsheet.com/ - 《算法》第4版 (Algorithms, 4th Edition) - 由Robert Sedgewick编写的经典算法教材
- LeetCode中国区题解精选 - CSDN博客 - 包含大量优质题解和学习心得
https://blog.csdn.net/ - 代码随想录 - 系统的算法学习路线和详细题解
https://programmercarl.com/ - 算法模板 - 知乎专栏 - 总结了各类算法的通用模板
https://www.zhihu.com/ - 《剑指Offer》 - 面试算法题的经典参考书
作者: 何海涛