第一章:LeetCode高频题型总览
在准备技术面试的过程中,LeetCode 已成为全球开发者提升算法能力的核心平台。通过对近年企业面试题目的统计分析,可以发现部分题型出现频率显著高于其他题目,掌握这些高频题型有助于高效备考。常见高频题型分类
- 数组与字符串操作:如两数之和、最长无重复子串
- 链表处理:反转链表、环形检测、合并有序链表
- 树的遍历与递归:二叉树的最大深度、路径总和、验证BST
- 动态规划:爬楼梯、背包问题、最长递增子序列
- 滑动窗口与双指针:最小覆盖子串、盛最多水的容器
典型题目示例(两数之和)
// 使用哈希表实现O(n)时间复杂度
func twoSum(nums []int, target int) []int {
numMap := make(map[int]int) // 存储值到索引的映射
for i, num := range nums {
complement := target - num
if idx, found := numMap[complement]; found {
return []int{idx, i} // 找到配对,返回索引
}
numMap[num] = i // 将当前数值和索引加入map
}
return nil // 未找到解时返回nil
}
该代码通过一次遍历完成查找,利用哈希表将查找补数的时间降为O(1),整体效率优于暴力双重循环。
高频题型分布统计
| 题型 | 占比(近一年) | 代表题目数量 |
|---|---|---|
| 数组/字符串 | 35% | 45 |
| 动态规划 | 20% | 26 |
| 树相关 | 18% | 23 |
| 链表 | 12% | 15 |
graph TD
A[开始刷题] --> B{选择题型}
B --> C[数组与字符串]
B --> D[动态规划]
B --> E[树与图]
C --> F[掌握模板]
D --> F
E --> F
F --> G[模拟面试]
第二章:数组与字符串经典题解
2.1 数组双指针技巧与实战应用
在处理数组问题时,双指针技巧能显著提升效率,尤其适用于有序数组的查找、去重和子数组问题。通过维护两个移动指针,避免使用额外空间或嵌套循环。基本思想与常见模式
双指针主要分为同向指针和对向指针。对向指针常用于两数之和问题,而同向指针多用于移除元素或合并操作。
// 对向指针:在有序数组中寻找两数之和
func twoSum(nums []int, target int) []int {
left, right := 0, len(nums)-1
for left < right {
sum := nums[left] + nums[right]
if sum == target {
return []int{left, right}
} else if sum < target {
left++
} else {
right--
}
}
return []int{}
}
该代码利用数组有序特性,left 从起始位置右移,right 从末尾左移。当和小于目标值时,增大 left;反之减小 right,时间复杂度为 O(n)。
实际应用场景
- 有序数组中查找满足条件的数对
- 原地修改数组,如删除重复元素
- 合并两个有序数组
2.2 滑动窗口算法原理与高频变种
滑动窗口是一种用于优化区间查询问题的经典技巧,特别适用于数组或字符串中连续子序列的处理。其核心思想是通过维护一个可变或固定大小的窗口,动态调整左右边界,避免重复计算。基本原理
使用双指针模拟窗口滑动:左指针控制窗口起始位置,右指针扩展窗口范围。当窗口内状态不满足条件时收缩左边界,从而在线性时间内完成遍历。常见变种与应用场景
- 定长窗口:求固定长度子数组的最大和
- 可变窗口(最短/最长满足条件的子串):如最小覆盖子串
- 前缀和 + 哈希优化:用于子数组和为目标值等问题
// 示例:可变滑动窗口模板
func slidingWindow(s string, t string) string {
left, right := 0, 0
valid := 0
window := make(map[byte]int)
need := make(map[byte]int)
for i := range t {
need[t[i]]++
}
start, length := 0, math.MaxInt32
for right < len(s) {
c := s[right]
right++
if need[c] > 0 {
window[c]++
if window[c] == need[c] {
valid++
}
}
for valid == len(need) {
if right-left < length {
start = left
length = right - left
}
d := s[left]
left++
if need[d] > 0 {
if window[d] == need[d] {
valid--
}
window[d]--
}
}
}
if length == math.MaxInt32 {
return ""
}
return s[start : start+length]
}
该代码实现“最小覆盖子串”问题,利用两个哈希表记录目标字符频次与当前窗口匹配情况,通过移动右指针扩展、左指针收缩实现最优解搜索。变量 valid 表示已完全匹配的字符种类数,是判断窗口合法性的关键。
2.3 前缀和与差分数组的优化策略
在处理高频区间更新与查询问题时,前缀和与差分数组是两种高效的技术手段。通过预处理数据结构,显著降低时间复杂度。前缀和优化区间求和
对于静态数组的多次区间求和查询,使用前缀和可将每次查询从 O(n) 降至 O(1):// 构建前缀和数组
prefix[i] = prefix[i-1] + arr[i-1]
// 查询 [l, r] 区间和
sum = prefix[r+1] - prefix[l]
该方法适用于无更新或更新极少的场景,预处理时间 O(n),空间开销 O(n)。
差分数组处理频繁更新
当面临大量区间增减操作时,差分数组更优:- 在区间 [l, r] 增加 val:diff[l] += val, diff[r+1] -= val
- 最终通过前缀还原原数组
2.4 字符串匹配与回文判断模板
基础字符串匹配方法
在处理字符串问题时,朴素匹配法是最直观的起点。其核心思想是逐位比较主串与模式串。func match(s, pattern string) int {
n, m := len(s), len(pattern)
for i := 0; i <= n-m; i++ {
j := 0
for j < m && s[i+j] == pattern[j] {
j++
}
if j == m {
return i
}
}
return -1
}
该函数返回模式串在主串中的起始索引。外层循环控制主串的匹配起点,内层循环逐字符比对,时间复杂度为 O(n×m)。
回文串高效判定
使用双指针从两端向中心逼近,可在线性时间内判断回文。- 初始化左指针为0,右指针为len(s)-1
- 循环直至两指针相遇
- 若对应字符不等,则非回文
2.5 贪心思想在区间问题中的体现
在处理区间调度与覆盖类问题时,贪心策略常能提供高效且最优的解决方案。其核心思想是:每一步都选择当前最优的局部解,期望最终得到全局最优。经典问题:区间调度
给定一组闭区间,目标是选出最多不重叠的区间。贪心策略为按结束时间升序排序,优先选择最早结束的区间。func maxNonOverlapping(intervals [][]int) int {
sort.Slice(intervals, func(i, j int) bool {
return intervals[i][1] < intervals[j][1]
})
count := 0
end := -1
for _, interval := range intervals {
if interval[0] >= end {
count++
end = interval[1]
}
}
return count
}
该代码通过排序后遍历实现,时间复杂度为 O(n log n),主要开销在排序。参数说明:intervals 为输入区间数组,每个元素 [start, end] 表示一个区间;count 记录选中区间数,end 维护上一个选中区间的结束位置。
适用条件
- 问题具有最优子结构
- 贪心选择性质成立:局部最优可导向全局最优
第三章:链表与树的核心套路
3.1 链表反转与环检测统一模型
在链表操作中,反转与环检测看似无关,实则可通过双指针技术构建统一模型。通过快慢指针或前后指针的协同移动,可同时解决两类问题。核心思想:双指针范式
使用两个指针以不同速度遍历链表,不仅能检测环(如Floyd算法),也能在反转过程中维护前驱关系。链表反转实现
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
curr := head
for curr != nil {
next := curr.Next // 临时保存下一节点
curr.Next = prev // 反转当前指针
prev = curr // 前移prev
curr = next // 前移curr
}
return prev // 新头节点
}
该代码通过迭代将每个节点的Next指向其前驱,最终prev指向原尾部,完成反转。
环检测扩展
当快指针与慢指针相遇时,即存在环。结合反转逻辑,可在检测后逆向验证环的起点,形成统一处理框架。3.2 二叉树遍历递归与迭代实现
递归实现:简洁直观的遍历方式
二叉树的三种主要遍历方式——前序、中序和后序,递归实现最为直观。以下为前序遍历的示例:
def preorder_recursive(root):
if not root:
return
print(root.val) # 访问根
preorder_recursive(root.left) # 遍历左子树
preorder_recursive(root.right) # 遍历右子树
该方法利用函数调用栈隐式维护访问顺序,逻辑清晰,但深度过大时可能引发栈溢出。
迭代实现:显式栈控制流程
使用栈模拟递归过程可避免系统栈限制。以下是前序遍历的迭代版本:
def preorder_iterative(root):
stack, result = [], []
while root or stack:
if root:
result.append(root.val)
stack.append(root)
root = root.left
else:
root = stack.pop()
root = root.right
return result
通过手动管理栈结构,程序在时间和空间效率上更具可控性,适用于大规模树结构处理。
3.3 BST性质在路径问题中的运用
BST路径搜索优化原理
二叉搜索树(BST)的中序遍历具有单调递增特性,这一性质可用于加速从根到叶节点的路径查找。当寻找特定值路径时,可依据当前节点值与目标的大小关系,决定仅递归左子树或右子树。- 若当前节点值大于目标,路径必存在于左子树
- 若当前节点值小于目标,路径必存在于右子树
- 显著降低无效递归调用,提升查询效率
def find_path(root, target, path):
if not root:
return False
path.append(root.val)
if root.val == target:
return True
if (root.left and root.val > target and find_path(root.left, target, path)) or \
(root.right and root.val < target and find_path(root.right, target, path)):
return True
path.pop()
return False
上述代码利用BST性质剪枝:每次递归前判断目标方向,避免无意义的子树遍历。参数 path 记录当前路径,回溯时弹出节点,确保空间效率。
第四章:搜索与动态规划精讲
4.1 DFS与BFS框架对比及剪枝技巧
核心框架差异
DFS基于栈结构实现回溯,适合路径探索;BFS使用队列逐层扩展,适用于最短路径问题。| 特性 | DFS | BFS |
|---|---|---|
| 数据结构 | 递归/栈 | 队列 |
| 空间复杂度 | O(h) | O(w) |
剪枝优化策略
通过提前终止无效分支显著提升效率。常见剪枝包括可行性剪枝、最优性剪枝。// DFS剪枝示例:N皇后问题
func dfs(row int, cols, diag1, diag2 map[int]bool) {
if row == n {
count++
return
}
for col := 0; col < n; col++ {
if cols[col] || diag1[row-col] || diag2[row+col] {
continue // 剪枝:冲突位置跳过
}
// 标记并递归
cols[col], diag1[row-col], diag2[row+col] = true, true, true
dfs(row+1, cols, diag1, diag2)
// 回溯
cols[col], diag1[row-col], diag2[row+col] = false, false, false
}
}
上述代码中,利用三个哈希表记录已占用列和对角线,避免非法状态搜索,大幅减少递归深度。
4.2 回溯法解决排列组合类题目
回溯法是解决排列、组合与子集问题的核心算法思想,通过尝试所有可能的分支并及时剪枝来高效遍历解空间。基本框架
回溯法通常采用递归实现,其核心在于“做选择”与“撤销选择”:def backtrack(path, options, result):
if not options:
result.append(path[:])
return
for item in options:
path.append(item)
next_options = options - {item}
backtrack(path, next_options, result)
path.pop() # 撤销选择
其中 path 记录当前路径,options 表示可选列表,result 收集最终解。
经典应用场景
- 全排列问题(Permutations)
- 组合总和(Combination Sum)
- 子集生成(Subsets)
4.3 一维与二维动态规划状态设计
在动态规划问题中,状态设计是核心环节。一维DP通常用于线性结构的问题,如斐波那契数列或爬楼梯问题,其状态转移仅依赖前几个已计算的状态。一维动态规划示例
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
该代码实现斐波那契数列的动态规划求解。dp[i] 表示到达第i级台阶的方法数,状态仅依赖于前两个状态,空间复杂度可优化至O(1)。
二维动态规划场景
当问题涉及两个变量维度时(如字符串匹配、矩阵路径),需采用二维DP。例如在网格中从左上到右下移动,状态定义为 dp[i][j] 表示到达位置(i,j)的路径数。| i\j | 0 | 1 | 2 |
|---|---|---|---|
| 0 | 1 | 1 | 1 |
| 1 | 1 | 2 | 3 |
4.4 背包模型在面试题中的变形应用
在实际面试中,背包问题常以隐式状态或复合约束形式出现,需识别其本质并灵活转化。常见变形类型
- 分组背包:每组物品中至多选一个
- 多重背包:物品数量有限制
- 依赖背包:选择物品存在先后依赖
典型代码实现(0-1背包空间优化)
// dp[j] 表示容量为 j 时的最大价值
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = W; j >= weight[i]; j--) {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
该代码通过逆序遍历实现空间压缩,避免重复选择。外层循环物品,内层倒序更新状态,确保每个物品仅被使用一次。
应用场景对比
| 类型 | 状态转移特点 | 复杂度 |
|---|---|---|
| 0-1背包 | 倒序更新 | O(nW) |
| 完全背包 | 正序更新 | O(nW) |
第五章:高频题冲刺策略与模板总结
常见算法模式归纳
在高频面试题中,滑动窗口、双指针、DFS/BFS 和动态规划出现频率极高。掌握其通用模板可大幅提升解题效率。- 滑动窗口适用于子数组/子串问题,如“最长无重复字符子串”
- 双指针常用于有序数组中的两数之和、合并区间等问题
- 树的遍历优先考虑递归DFS或迭代BFS
动态规划状态转移模板
// 典型背包问题状态转移
dp[i][w] = max(
dp[i-1][w], // 不选第i个物品
dp[i-1][w-weight[i]] + value[i] // 选第i个物品
)
高频题优化技巧
使用哈希表预处理数据可将O(n²)降为O(n),例如两数之和问题中用map存储target - nums[i]。| 题型 | 推荐方法 | 时间复杂度 |
|---|---|---|
| 子数组最大和 | Kadane算法 | O(n) |
| 合并K个有序链表 | 最小堆 | O(N log k) |
实战调试建议
输入测试用例时优先覆盖:
- 空输入 []
- 单元素 [1]
- 边界情况 如溢出INT_MAX
逐步打印中间状态,验证状态转移正确性
21天攻克LeetCode Top 50高频题
1772

被折叠的 条评论
为什么被折叠?



