第一章:AI算法题1024道:面试必刷清单
在人工智能与大数据驱动的技术浪潮中,算法能力已成为衡量工程师核心竞争力的重要标准。无论是大厂面试还是技术竞赛,扎实的算法功底都是脱颖而出的关键。
高效刷题策略
- 按知识点分类突破:优先掌握数组、链表、树、动态规划等高频主题
- 每日定量训练:建议每天完成3-5道中等难度题目,保持思维活跃
- 复盘错题本:记录解题思路误区与优化路径,形成个人知识图谱
典型题目示例(二叉树层序遍历)
// 使用广度优先搜索实现二叉树层序遍历
package main
import "fmt"
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
func levelOrder(root *TreeNode) [][]int {
if root == nil {
return nil // 空树返回空切片
}
var result [][]int
queue := []*TreeNode{root} // 初始化队列
for len(queue) > 0 {
levelSize := len(queue) // 当前层节点数
var currentLevel []int
for i := 0; i < levelSize; i++ {
node := queue[0]
queue = queue[1:]
currentLevel = append(currentLevel, node.Val)
if node.Left != nil {
queue = append(queue, node.Left)
}
if node.Right != nil {
queue = append(queue, node.Right)
}
}
result = append(result, currentLevel)
}
return result
}
常见算法考察分布
| 算法类别 | 出现频率 | 推荐掌握程度 |
|---|
| 动态规划 | 38% | 熟练掌握 |
| 二叉树操作 | 30% | 熟练掌握 |
| 字符串处理 | 15% | 熟悉应用 |
| 图论算法 | 12% | 理解原理 |
graph TD
A[开始刷题] --> B{选择题目类型}
B --> C[数据结构相关]
B --> D[算法思想类]
C --> E[链表/树/堆]
D --> F[DP/贪心/回溯]
E --> G[提交并验证]
F --> G
G --> H{通过?}
H -->|是| I[记录总结]
H -->|否| J[查看题解学习]
第二章:数据结构核心突破
2.1 数组与链表:从基础操作到高频变形题
核心数据结构对比
数组和链表是线性结构的基石。数组通过连续内存实现O(1)随机访问,但插入删除代价高;链表以指针串联节点,支持O(1)头插头删,但访问需遍历。
| 特性 | 数组 | 链表 |
|---|
| 访问时间 | O(1) | O(n) |
| 插入/删除 | O(n) | O(1)(已知位置) |
| 内存占用 | 紧凑 | 额外指针开销 |
典型操作代码示例
// 单链表节点定义
type ListNode struct {
Val int
Next *ListNode
}
// 在链表头部插入新节点
func addAtHead(head *ListNode, val int) *ListNode {
return &ListNode{Val: val, Next: head}
}
上述代码创建新节点并将其Next指向原头节点,时间复杂度为O(1)。适用于需要频繁插入的场景,如LRU缓存头部更新。
2.2 栈与队列:理解LIFO/FIFO在算法中的巧妙应用
栈的后进先出特性
栈(Stack)是一种遵循“后进先出”(LIFO)原则的数据结构,常用于函数调用、表达式求值等场景。其核心操作包括
push(入栈)和
pop(出栈)。
type Stack struct {
items []int
}
func (s *Stack) Push(val int) {
s.items = append(s.items, val)
}
func (s *Stack) Pop() int {
if len(s.items) == 0 {
return -1
}
val := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return val
}
上述代码实现了一个简单的整数栈。Push 将元素追加到切片末尾,Pop 取出最后一个元素并更新切片长度,时间复杂度为 O(1)。
队列的先进先出机制
队列(Queue)遵循“先进先出”(FIFO),适用于任务调度、广度优先搜索等场景。主要操作为
enqueue 和
dequeue。
- 栈适合回溯类问题,如括号匹配
- 队列常用于层级遍历或消息传递
2.3 哈希表与集合:O(1)查找背后的优化策略
哈希表通过散列函数将键映射到数组索引,实现平均情况下 O(1) 的查找性能。核心挑战在于解决**哈希冲突**,常见策略包括链地址法和开放寻址法。
链地址法的实现示例
type Node struct {
key string
value interface{}
next *Node
}
type HashTable struct {
buckets []*Node
size int
}
func (h *HashTable) Put(key string, value interface{}) {
index := hash(key) % h.size
node := &Node{key: key, value: value, next: h.buckets[index]}
h.buckets[index] = node
}
上述代码使用链表处理冲突,每个桶存储一个链表头节点。`hash` 函数生成哈希值,取模后定位桶位置。插入时头插法避免遍历,但需注意哈希函数的均匀性以减少碰撞。
性能优化关键点
- 负载因子控制:当元素数与桶数比超过阈值(如 0.75),触发扩容并重新哈希
- 高质量哈希函数:如使用 MurmurHash 提升分布均匀性
- 红黑树退化:Java HashMap 在链表过长时转为红黑树,降低最坏情况至 O(log n)
2.4 树与二叉树:递归与迭代遍历的双重视角
递归遍历:自然的分治思维
递归是树结构遍历最直观的方式,利用函数调用栈隐式管理访问顺序。以中序遍历为例:
def inorder(root):
if root:
inorder(root.left) # 遍历左子树
print(root.val) # 访问根节点
inorder(root.right) # 遍历右子树
该实现逻辑清晰,代码简洁。每次递归调用将问题分解为子树处理,符合树的定义本身。
迭代遍历:显式栈的控制力
使用显式栈模拟递归过程,提升空间控制能力:
def inorder_iterative(root):
stack, result = [], []
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
通过手动维护栈,精确掌控节点访问时机,适用于深度较大的树以避免栈溢出。
两种视角的对比
| 方式 | 优点 | 缺点 |
|---|
| 递归 | 代码简洁,易理解 | 深度大时可能栈溢出 |
| 迭代 | 空间可控,效率高 | 代码复杂度略高 |
2.5 图结构与搜索:DFS/BFS在实际题目中的建模技巧
在解决图相关问题时,深度优先搜索(DFS)和广度优先搜索(BFS)不仅是基础算法,更是建模思维的核心。关键在于如何将实际问题抽象为图结构。
建模思维转换
许多非显式图问题可通过状态节点与转移边构建图模型。例如迷宫寻路、课程依赖、社交网络传播等,均可转化为图的遍历问题。
代码实现对比
# BFS:最短路径搜索
from collections import deque
def bfs(graph, start):
queue = deque([start])
visited = {start}
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
该BFS实现使用队列保证层级遍历,适用于求解无权图最短路径。visited集合防止重复访问,避免死循环。
- DFS适合路径探索、拓扑排序等场景
- BFS常用于最短路径、层序遍历等问题
第三章:经典算法思想精讲
3.1 分治法与递归优化:从归并排序到典型分治题型
分治法的核心思想
分治法通过将问题划分为若干子问题,递归求解后合并结果。典型应用如归并排序,其时间复杂度稳定在 O(n log n)。
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, 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
上述代码中,
merge_sort 递归分割数组,
merge 函数合并两个有序子数组。分割操作确保子问题规模减半,合并过程保证有序性。
典型分治题型对比
| 问题 | 划分方式 | 合并代价 |
|---|
| 归并排序 | 均分两半 | O(n) |
| 快速幂 | 指数折半 | O(1) |
3.2 动态规划入门到精通:状态定义与转移方程设计
动态规划(Dynamic Programming, DP)的核心在于合理定义状态和构建状态转移方程。正确识别问题的最优子结构和重叠子问题是第一步。
状态定义的关键原则
状态应能唯一描述问题的某个子问题解。例如,在背包问题中,
dp[i][w] 表示前
i 个物品在容量为
w 时的最大价值。
经典0-1背包代码实现
func max(a, b int) int {
if a > b {
return a
}
return b
}
func knapsack(weights, values []int, W int) int {
n := len(weights)
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, W+1)
}
for i := 1; i <= n; i++ {
for w := 0; w <= W; w++ {
if weights[i-1] <= w {
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weights[i-1]] + values[i-1])
} else {
dp[i][w] = dp[i-1][w]
}
}
}
return dp[n][W]
}
上述代码中,
dp[i][w] 表示考虑前
i 个物品、总重量不超过
w 的最大价值。转移方程根据是否选择第
i 个物品进行分支决策。
常见DP类型归纳
- 线性DP:最长递增子序列
- 区间DP:石子合并问题
- 树形DP:二叉树最大路径和
3.3 贪心算法实战:何时可用?如何证明正确性?
贪心算法在每一步选择中都采取当前状态下最优的决策,期望通过局部最优解达到全局最优。其适用场景通常具备两个关键性质:
贪心选择性质和
最优子结构。
何时可以使用贪心算法?
- 贪心选择性质:局部最优选择能导向全局最优解;
- 最优子结构:问题的最优解包含子问题的最优解。
经典案例包括活动选择问题、霍夫曼编码和最小生成树(如Prim与Kruskal算法)。
正确性证明方法
通常采用
剪枝交换法:假设存在一个更优解,通过将其中的选择替换为贪心选择,构造出不更差的新解,从而证明贪心策略的合理性。
// 活动选择问题:按结束时间排序后贪心选取
func greedyActivitySelection(activities [][]int) int {
sort.Slice(activities, func(i, j int) bool {
return activities[i][1] < activities[j][1] // 按结束时间升序
})
count := 1
end := activities[0][1]
for i := 1; i < len(activities); i++ {
if activities[i][0] >= end { // 下一个活动开始时间不早于上一个结束
count++
end = activities[i][1]
}
}
return count
}
该代码实现活动选择问题,核心逻辑是优先选择最早结束的活动,为后续留出最大时间空间。参数
activities[i][0]表示第i个活动的开始时间,
[1]为结束时间,排序后单次遍历即可完成选择。
第四章:高频题型分类攻克
4.1 双指针与滑动窗口:字符串与数组中的效率利器
在处理数组和字符串问题时,双指针与滑动窗口技术显著提升了算法效率,尤其适用于查找子串、求解最值等问题。
双指针的基本模式
双指针通过两个索引协同移动,避免嵌套循环。常见于有序数组的两数之和问题:
func twoSum(numbers []int, target int) []int {
left, right := 0, len(numbers)-1
for left < right {
sum := numbers[left] + numbers[right]
if sum == target {
return []int{left + 1, right + 1}
} else if sum < target {
left++
} else {
right--
}
}
return nil
}
该代码利用左右指针从两端逼近目标值,时间复杂度为 O(n),优于暴力解法的 O(n²)。
滑动窗口的应用场景
滑动窗口用于连续子区间问题,如寻找最小覆盖子串。通过动态调整窗口边界,实现高效遍历。
4.2 回溯算法与排列组合:系统化解决搜索类问题
回溯算法是一种通过递归尝试所有可能解路径,并在不满足条件时“剪枝”退回的搜索策略,特别适用于排列、组合、子集等组合搜索问题。
核心思想与模板结构
回溯的本质是深度优先搜索(DFS)的暴力枚举,结合状态重置实现路径探索:
def backtrack(path, options, result):
if 满足结束条件:
result.append(path[:]) # 深拷贝
return
for option in options:
path.append(option) # 做选择
new_options = options - {option} # 更新可选列表
backtrack(path, new_options, result)
path.pop() # 撤销选择(回溯)
上述模板中,
path记录当前路径,
options表示剩余可选元素,
result收集合法解。关键在于“做选择”与“撤销选择”的对称操作。
典型应用场景对比
| 问题类型 | 是否允许重复 | 是否有序 | 示例 |
|---|
| 排列 | 否 | 是 | 全排列 [1,2] → [1,2],[2,1] |
| 组合 | 否 | 否 | C(n,k) |
| 子集 | 否 | 否 | 所有子集 |
4.3 二分查找进阶:边界处理与非传统应用场景
边界条件的精准控制
在实际应用中,二分查找常需定位目标值的左右边界。例如在有序数组中查找第一个大于等于目标值的位置,需调整收缩策略:
func lowerBound(nums []int, target int) int {
left, right := 0, len(nums)
for left < right {
mid := left + (right-left)/2
if nums[mid] < target {
left = mid + 1
} else {
right = mid
}
}
return left
}
该实现采用左闭右开区间,确保不会遗漏边界元素,适用于插入位置计算等场景。
非单调函数中的二分思想
二分不仅限于有序数组。只要问题满足“决策单调性”,即可应用。例如在旋转排序数组中查找最小值:
- 比较中点与右端点值决定搜索方向
- 时间复杂度仍为 O(log n)
4.4 堆与优先队列:Top-K问题与动态维护极值
在处理大规模数据流时,高效获取最大或最小的K个元素是常见需求。堆作为一种特殊的完全二叉树,能以O(log n)时间维护插入和删除操作,成为解决Top-K问题的理想结构。
最小堆实现Top-K维护
使用最小堆可动态维护最大的K个元素,堆顶始终为当前第K大值:
import heapq
def top_k_elements(stream, k):
heap = []
for num in stream:
if len(heap) < k:
heapq.heappush(heap, num)
elif num > heap[0]:
heapq.heapreplace(heap, num)
return sorted(heap, reverse=True)
上述代码中,
heapq维护一个大小为K的最小堆。当新元素大于堆顶时,替换堆顶并重新调整。最终保留的是流中最大的K个元素。
时间复杂度分析
- 每条数据插入:O(log K)
- 总时间复杂度:O(N log K),远优于排序的O(N log N)
- 空间复杂度:O(K)
第五章:AI算法题1024道:面试必刷清单
高频题型分类与实战策略
- 动态规划:掌握状态转移方程构建,如背包问题、最长递增子序列
- 图论算法:熟练实现Dijkstra、Floyd及拓扑排序,应对社交网络或路径推荐场景
- 二叉树遍历:递归与迭代双实现,重点理解中序与层序遍历在BST中的应用
- 字符串匹配:KMP算法手写能力决定编码题成败
典型代码模板示例
// 快速排序实现(常用于Top K问题)
func quickSort(arr []int, left, right int) {
if left >= right {
return
}
pivot := partition(arr, left, right)
quickSort(arr, left, pivot-1)
quickSort(arr, pivot+1, right)
}
func partition(arr []int, left, right int) int {
pivot := arr[right]
i := left
for j := left; j < right; j++ {
if arr[j] < pivot {
arr[i], arr[j] = arr[j], arr[i]
i++
}
}
arr[i], arr[right] = arr[right], arr[i]
return i
}
大厂真题分布统计
| 公司 | 动态规划占比 | 链表题频次 | 设计类题目 |
|---|
| Google | 32% | 高 | LRU Cache |
| Meta | 28% | 极高 | TinyURL |
| Amazon | 35% | 中 | 文件系统模拟 |
刷题路径建议
新手阶段 → 按标签刷前100道 → 模拟面试环境限时作答 → 归纳错题模式 → 冲刺高频TOP50