数据结构与算法|算法思维总结与实战
- [第二十四章 算法思维总结与实战](#第二十四章 算法思维总结与实战)
-
- [24.1 十大算法策略回顾](#24.1 十大算法策略回顾)
-
- [24.1.1 分治(Divide and Conquer)](#24.1.1 分治(Divide and Conquer))
- [24.1.2 贪心(Greedy)](#24.1.2 贪心(Greedy))
- [24.1.3 动态规划(Dynamic Programming)](#24.1.3 动态规划(Dynamic Programming))
-
- [1. 定义状态](#1. 定义状态)
- [2. 推导状态转移方程](#2. 推导状态转移方程)
- [3. 确定边界条件](#3. 确定边界条件)
- [4. 确定计算顺序](#4. 确定计算顺序)
- [24.1.4 回溯(Backtracking)](#24.1.4 回溯(Backtracking))
- [24.1.5 搜索(Search)](#24.1.5 搜索(Search))
- [24.1.6 枚举(Enumeration)](#24.1.6 枚举(Enumeration))
- [24.1.7 模拟(Simulation)](#24.1.7 模拟(Simulation))
- [24.1.8 位运算(Bit Manipulation)](#24.1.8 位运算(Bit Manipulation))
- [24.1.9 双指针(Two Pointers)](#24.1.9 双指针(Two Pointers))
- [24.1.10 滑动窗口(Sliding Window)](#24.1.10 滑动窗口(Sliding Window))
- [24.2 十大策略横向对比与选择指南](#24.2 十大策略横向对比与选择指南)
- [24.3 刷题方法论:如何高效 LeetCode](#24.3 刷题方法论:如何高效 LeetCode)
-
- [24.3.1 刷题的正确姿势](#24.3.1 刷题的正确姿势)
-
- [1. 按专题刷,不要随机刷](#1. 按专题刷,不要随机刷)
- [2. 一题多解,追求深度](#2. 一题多解,追求深度)
- [3. 定期复盘,建立知识体系](#3. 定期复盘,建立知识体系)
- [24.3.2 刷题节奏规划](#24.3.2 刷题节奏规划)
- [24.3.3 解题思维框架](#24.3.3 解题思维框架)
- [24.4 设计模式中的数据结构](#24.4 设计模式中的数据结构)
-
- [24.4.1 观察者模式与发布-订阅](#24.4.1 观察者模式与发布-订阅)
- [24.4.2 责任链模式](#24.4.2 责任链模式)
- [24.4.3 策略模式](#24.4.3 策略模式)
- [24.4.4 设计模式与数据结构对应总结](#24.4.4 设计模式与数据结构对应总结)
- [24.5 真实工程场景中的算法应用](#24.5 真实工程场景中的算法应用)
-
- [24.5.1 搜索引擎------倒排索引与 TF-IDF](#24.5.1 搜索引擎——倒排索引与 TF-IDF)
- [24.5.2 推荐系统------协同过滤](#24.5.2 推荐系统——协同过滤)
- [24.5.3 分布式系统------一致性哈希](#24.5.3 分布式系统——一致性哈希)
- [24.5.4 工程应用总结](#24.5.4 工程应用总结)
- [24.6 算法与架构设计的关系](#24.6 算法与架构设计的关系)
-
- [24.6.1 数据结构选择影响架构](#24.6.1 数据结构选择影响架构)
- [24.6.2 算法思维在架构中的应用](#24.6.2 算法思维在架构中的应用)
- [24.7 大厂面试算法题高频考点总结](#24.7 大厂面试算法题高频考点总结)
-
- [24.7.1 高频数据结构考点](#24.7.1 高频数据结构考点)
- [24.7.2 高频算法策略考点](#24.7.2 高频算法策略考点)
- [24.7.3 大厂面试高频 Top 20 题目](#24.7.3 大厂面试高频 Top 20 题目)
- [24.7.4 面试算法题解题模板](#24.7.4 面试算法题解题模板)
- [24.8 本系列全篇知识图谱](#24.8 本系列全篇知识图谱)
- [24.9 总结](#24.9 总结)
-
- [24.9.1 核心回顾](#24.9.1 核心回顾)
- [24.9.2 学习算法的三个境界](#24.9.2 学习算法的三个境界)
上篇:第二十三章、高级数据结构
第二十四章 算法思维总结与实战
历经二十三章的学习,我们从线性结构一路走到高级数据结构,从递归分治讲到动态规划与回溯。本章是整个系列的收官之作------不是终点,而是将所有知识点串联起来的枢纽。我们将系统回顾十大算法策略,梳理刷题方法论,探讨算法在工程中的真实应用,并总结大厂面试的高频考点。
24.1 十大算法策略回顾
在数据结构与算法的世界中,解决问题的方式是有"套路"可循的。以下十大策略几乎覆盖了所有常见算法题的核心思路。
十大算法策略
分治
贪心
动态规划
回溯
搜索
枚举
模拟
位运算
双指针
滑动窗口
24.1.1 分治(Divide and Conquer)
分治策略:将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题,递归求解子问题,最后合并子问题的解得到原问题的解。
三步走框架:
分治(问题 P):
if P 的规模足够小:
直接求解并返回
将 P 分解为若干子问题 P1, P2, ..., Pk
for each Pi:
递归求解 Pi 的解 Si
合并 S1, S2, ..., Sk 得到 P 的解
返回 P 的解
典型应用:
| 问题 | 分解方式 | 合并方式 | 时间复杂度 |
|---|---|---|---|
| 归并排序 | 从中间一分为二 | 两个有序数组合并 | O(n log n) |
| 快速排序 | 以基准元素分为两部分 | 原地拼接 | O(n log n) |
| 最大子数组和 | 按中点分为左右两部分 | 跨中点的最大和 | O(n log n) |
| 汉诺塔 | 分为 n-1 个盘和 1 个盘 | 按规则移动 | O(2ⁿ) |
核心要点:分治的效率取决于子问题之间是否独立。如果子问题重叠,应考虑动态规划。
24.1.2 贪心(Greedy)
贪心策略:在每一步选择中都采取当前状态下的最优选择(局部最优),期望通过一系列局部最优选择达到全局最优。
贪心成立的两个条件:
- 贪心选择性质:局部最优选择能导致全局最优解
- 最优子结构:问题的最优解包含子问题的最优解
贪心 vs 动态规划:
| 对比维度 | 贪心算法 | 动态规划 |
|---|---|---|
| 决策方式 | 每步做局部最优选择 | 考虑所有可能的子问题解 |
| 是否回溯 | 不回溯,一旦选择不可更改 | 可以回溯,通过状态转移重新选择 |
| 适用条件 | 贪心选择性质 + 最优子结构 | 最优子结构 + 重叠子问题 |
| 时间效率 | 通常更快(O(n) ~ O(n log n)) | 相对较慢(O(n²) ~ O(n³)) |
| 解的保证 | 不一定最优 | 一定最优 |
典型应用:
- 活动选择问题:按结束时间排序,每次选最早结束的活动
- 分数背包问题:按单位价值排序,优先选单位价值最高的物品
- 哈夫曼编码:每次合并频率最小的两棵树
- 跳跃游戏:每步维护能到达的最远位置
24.1.3 动态规划(Dynamic Programming)
动态规划策略:将复杂问题分解为重叠子问题,通过存储已求解的子问题结果避免重复计算,从而大幅降低时间复杂度。
DP 解题四步法:
1. 定义状态
明确 dp[i] 或 dp[i][j] 的含义------这是解题的关键一步。
2. 推导状态转移方程
找出状态之间的递推关系。
3. 确定边界条件
基础情况的初始值。
4. 确定计算顺序
自底向上(递推)或自顶向下(记忆化搜索)。
经典 DP 模型:
| 模型 | 代表问题 | 状态定义 | 转移方程 |
|---|---|---|---|
| 线性 DP | 最长递增子序列 | dp[i]:以 i 结尾的 LIS 长度 | dp[i] = max(dp[j] + 1) |
| 区间 DP | 矩阵链乘法 | dp[i][j]:区间 [i,j] 的最小代价 | dp[i][j] = min(dp[i][k] + dp[k+1][j] + cost) |
| 背包 DP | 0-1 背包 | dp[i][w]:前 i 个物品容量 w 的最大价值 | dp[i][w] = max(dp[i-1][w], dp[i-1][w-wi] + vi) |
| 树形 DP | 打家劫舍 III | dp[node][0/1]:选/不选当前节点 | 递推依赖子节点状态 |
| 状态压缩 DP | 旅行商问题 | dp[mask][i]:经过集合 mask 到达 i 的最短路 | dp[mask][i] = min(dp[mask ^ (1<<i)][j] + dist[j][i]) |
核心要点:DP 的难点在于状态定义和状态转移方程的推导。建议从暴力递归出发,发现重叠子问题后加备忘录优化,最后转为递推形式。
24.1.4 回溯(Backtracking)
回溯策略:在问题的解空间中按深度优先的方式进行搜索,当发现当前路径不可能得到有效解时,回退到上一个节点尝试其他选择。
回溯解题框架:
java
void backtrack(路径, 选择列表) {
if (满足结束条件) {
记录结果;
return;
}
for (选择 : 选择列表) {
做选择;
backtrack(新路径, 新选择列表);
撤销选择; // 回溯
}
}
回溯的核心------剪枝:
剪枝是回溯效率的关键,常见的剪枝策略:
| 剪枝类型 | 说明 | 示例 |
|---|---|---|
| 排序剪枝 | 先排序,跳过不可能的分支 | 组合总和(排序后跳过大于 target 的数) |
| 去重剪枝 | 跳过重复元素避免重复解 | 全排列 II(same level 去重) |
| 约束剪枝 | 不满足约束条件直接跳过 | N 皇后(同列/对角线冲突) |
| 限界剪枝 | 当前解不可能优于已知最优解 | 分配问题(剩余最小值仍超当前最优) |
24.1.5 搜索(Search)
搜索策略:在状态空间中系统地寻找目标状态,主要包括 DFS(深度优先搜索)和 BFS(广度优先搜索)。
DFS vs BFS 对比:
| 对比维度 | DFS | BFS |
|---|---|---|
| 数据结构 | 栈(递归调用栈) | 队列 |
| 空间复杂度 | O(h),h 为深度 | O(w),w 为宽度 |
| 最短路径 | 不保证 | 无权图保证最短路径 |
| 适用场景 | 连通性判断、拓扑排序、所有解 | 最短路径、层序遍历、最少步数 |
搜索的变种:
- 双向 BFS:从起点和终点同时搜索,当两边相遇时找到最短路径
- A 搜索*:使用启发式函数评估节点的优先级,优先搜索更有希望的节点
- 迭代加深 DFS(IDDFS):限制 DFS 深度,逐步加深,兼有 DFS 的空间效率和 BFS 的完备性
24.1.6 枚举(Enumeration)
枚举策略:遍历问题的所有可能解空间,逐一验证是否满足条件。看似"暴力",但在某些场景下是唯一可行的方案。
枚举的优化思路:
| 优化方式 | 说明 | 效果 |
|---|---|---|
| 缩小范围 | 通过分析减少枚举范围 | O(n²) → O(n) |
| 提前终止 | 不满足条件时立即跳出 | 减少无效计算 |
| 对称性剪枝 | 利用问题的对称性减半枚举 | 时间减半 |
| 状态压缩 | 用位运算表示状态减少枚举量 | O(2ⁿ) → O(2ⁿ/k) |
典型应用: 两数之和(暴力枚举 O(n²))、子集枚举(位掩码)、暴力破解密码
24.1.7 模拟(Simulation)
模拟策略:按照题目描述的规则,一步一步地模拟过程,不需要复杂的数学推导或优化。
模拟的核心能力:
- 读懂题意:准确理解每一步操作规则
- 边界处理:处理好初始状态和终止条件
- 代码实现:将自然语言描述精确翻译为代码
典型应用: 螺旋矩阵、旋转图像、Z 字形变换、生命游戏
注意 :模拟题不考察算法设计能力,但考察代码实现能力 和细节处理能力。面试中往往作为第一道题出现。
24.1.8 位运算(Bit Manipulation)
位运算策略:利用二进制位的操作来优化计算,常用于集合表示、状态压缩和特定数学运算。
常用位运算技巧:
| 操作 | 位运算写法 | 说明 |
|---|---|---|
| 判断奇偶 | (n & 1) == 1 |
最低位为 1 则奇数 |
| 交换两数 | a ^= b; b ^= a; a ^= b; |
不用临时变量 |
| 乘以 2 的 k 次方 | n << k |
等价于 n * 2ᵏ |
| 除以 2 的 k 次方 | n >> k |
等价于 n / 2ᵏ |
| 取最低位 1 | n & (-n) |
lowbit 操作 |
| 消去最低位 1 | n & (n - 1) |
统计 1 的个数 |
| 判断 2 的幂 | (n & (n - 1)) == 0 |
只有一位为 1 |
典型应用: 只出现一次的数字、子集枚举、两整数之和、位图(BitMap)
24.1.9 双指针(Two Pointers)
双指针策略:使用两个指针在数据结构上进行协同遍历,常见于数组、链表和字符串问题。
三种常见模式:
| 模式 | 初始位置 | 移动方向 | 适用场景 |
|---|---|---|---|
| 对撞指针 | 左右两端 | 向中间移动 | 有序数组的两数之和、回文判断、容器盛水 |
| 快慢指针 | 同一起点 | 速度不同 | 链表环检测、链表中点、删除倒数第 N 个节点 |
| 滑动窗口 | 同一起点 | 一前一后 | 子串匹配、最长/最短子数组 |
典型应用:
- 有序数组的两数之和:对撞指针 O(n)
- 盛最多水的容器:对撞指针,短边移动
- 删除有序数组的重复项:快慢指针
- 三数之和:排序 + 对撞指针
24.1.10 滑动窗口(Sliding Window)
滑动窗口策略:在数组或字符串上维护一个动态窗口,通过窗口的滑动来求解问题,是双指针的高级应用。
滑动窗口框架:
java
public String minWindow(String s, String t) {
// 1. 初始化窗口和计数器
int[] need = new int[128];
int[] window = new int[128];
for (char c : t.toCharArray()) need[c]++;
int left = 0, right = 0;
int valid = 0; // 窗口中满足条件的字符种类数
int start = 0, minLen = Integer.MAX_VALUE;
// 2. 扩大窗口
while (right < s.length()) {
char c = s.charAt(right);
right++;
if (need[c] > 0) {
window[c]++;
if (window[c] == need[c]) valid++;
}
// 3. 缩小窗口
while (valid == countUniqueChars(t)) {
if (right - left < minLen) {
start = left;
minLen = right - left;
}
char d = s.charAt(left);
left++;
if (need[d] > 0) {
if (window[d] == need[d]) valid--;
window[d]--;
}
}
}
return minLen == Integer.MAX_VALUE ? "" : s.substring(start, start + minLen);
}
典型应用: 最小覆盖子串、无重复字符的最长子串、找到字符串中所有字母异位词、长度最小的子数组
24.2 十大策略横向对比与选择指南
面对一道算法题,如何快速选择合适的策略?以下对比表提供了决策参考。
| 策略 | 核心思想 | 适用特征 | 时间复杂度量级 | 代表题型 |
|---|---|---|---|---|
| 分治 | 分解 → 解决 → 合并 | 可独立分解的子问题 | O(n log n) | 排序、最近点对 |
| 贪心 | 局部最优 → 全局最优 | 存在贪心选择性质 | O(n) ~ O(n log n) | 调度、霍夫曼 |
| 动态规划 | 状态转移 → 最优子结构 | 重叠子问题 | O(n²) ~ O(n³) | 背包、LCS、LIS |
| 回溯 | DFS + 剪枝 | 求所有解/可行解 | O(2ⁿ) 或 O(n!) | 排列组合、N 皇后 |
| 搜索 | 状态空间遍历 | 图/网格上的路径/连通性 | O(V + E) | 迷宫、最短路径 |
| 枚举 | 遍历所有可能 | 解空间较小或无规律 | 依问题而定 | 暴力破解、穷举 |
| 模拟 | 按规则执行 | 规则明确的过程 | 依问题而定 | 矩阵操作、游戏规则 |
| 位运算 | 二进制操作 | 与位状态相关 | O(1) ~ O(n) | 状态压缩、去重 |
| 双指针 | 协同遍历 | 有序/线性结构 | O(n) | 两数之和、去重 |
| 滑动窗口 | 动态窗口 | 连续子串/子数组 | O(n) | 子串匹配、最值 |
选择策略的决策树:
- 问题是否有最优子结构?→ 是:考虑 DP 或贪心
- 子问题是否重叠?→ 是:用 DP;否:考虑分治
- 是否要求所有解?→ 是:回溯
- 问题在图/网格上?→ 搜索(DFS/BFS)
- 数据是有序/线性的?→ 双指针或滑动窗口
- 规则明确但无明显优化?→ 模拟或枚举
- 涉及位状态/集合?→ 位运算
24.3 刷题方法论:如何高效 LeetCode
24.3.1 刷题的正确姿势
很多人刷了几百道题依然感觉没有进步,根本原因在于刷题方式不对。以下是一套经过验证的高效方法论。
1. 按专题刷,不要随机刷
按数据结构和算法策略分类刷题,同一专题连续做 5-10 道,形成模式识别能力。
推荐刷题顺序:
数组与字符串
链表
栈与队列
哈希表
树
二分查找
图
回溯
动态规划
贪心
高级专题
2. 一题多解,追求深度
同一道题至少尝试两种解法,比较时间/空间复杂度的差异:
java
// 示例:反转链表 ------ 两种经典解法
// 方法一:迭代法(推荐,空间 O(1))
public ListNode reverseListIterative(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
// 方法二:递归法(优雅但空间 O(n))
public ListNode reverseListRecursive(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseListRecursive(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
3. 定期复盘,建立知识体系
每刷完一个专题,写一篇总结:
- 这个专题的核心思路是什么?
- 有哪些常见变体?
- 遇到类似题目如何快速识别?
24.3.2 刷题节奏规划
| 阶段 | 题量 | 重点 | 每日建议 |
|---|---|---|---|
| 入门期(1-2 月) | 100-150 题 | Easy 为主,理解基本数据结构 | 2-3 题/天 |
| 进阶期(3-4 月) | 200-300 题 | Easy + Medium,掌握核心算法策略 | 2 题/天 |
| 冲刺期(1-2 月) | 50-100 题 | Medium + Hard,专题突破 | 1-2 题/天 |
| 面试期 | 持续 | 高频题反复练,模拟面试 | 每日 1 次模拟 |
24.3.3 解题思维框架
面对一道新题,按照以下步骤思考:
1. 审题(2 分钟)
- 输入是什么?输出是什么?
- 数据规模多大?(决定算法复杂度上限)
- 有无特殊条件?(有序、无重复、范围限制等)
2. 思考(5-10 分钟)
- 属于哪个专题?(数据结构 or 算法策略)
- 能否套用已知模板?
- 有没有更优的解法?
3. 编码(15-20 分钟)
- 先写核心逻辑,再处理边界
- 变量命名清晰,逻辑简洁
4. 验证(5 分钟)
- 正常用例
- 边界用例(空输入、单元素、极端值)
- 分析时间/空间复杂度
5. 优化(5 分钟)
- 有无冗余计算?
- 能否空间换时间 or 时间换空间?
关键原则 :如果 20 分钟内完全没有思路,直接看题解。看题解不是偷懒,而是学习新的思维模式。但看完题解后,必须关掉答案自己手写一遍。
24.4 设计模式中的数据结构
设计模式是软件工程中的经典方案,而数据结构是设计模式的底层支撑。理解数据结构在设计模式中的角色,能帮助我们更深刻地理解两者的关联。
24.4.1 观察者模式与发布-订阅
核心数据结构:哈希表 + 链表
观察者模式维护一个"主题 → 观察者列表"的映射关系,天然需要哈希表来快速查找主题,链表来存储观察者。
java
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
/**
* 观察者模式 ------ 事件总线实现
* 底层数据结构:HashMap(事件类型 → 观察者链表)
*/
public class EventBus {
private final Map<Class<?>, List<Observer<?>>> observerMap = new HashMap<>();
/**
* 注册观察者:HashMap.put + LinkedList.add
*/
public <T> void register(Class<T> eventType, Observer<T> observer) {
observerMap.computeIfAbsent(eventType, k -> new ArrayList<>()).add(observer);
}
/**
* 取消注册:LinkedList.remove
*/
public <T> void unregister(Class<T> eventType, Observer<T> observer) {
List<Observer<?>> observers = observerMap.get(eventType);
if (observers != null) {
observers.remove(observer);
}
}
/**
* 发布事件:HashMap.get → 遍历链表通知
*/
@SuppressWarnings("unchecked")
public <T> void post(T event) {
List<Observer<?>> observers = observerMap.get(event.getClass());
if (observers != null) {
for (Observer<?> observer : observers) {
((Observer<T>) observer).onEvent(event);
}
}
}
public interface Observer<T> {
void onEvent(T event);
}
}
24.4.2 责任链模式
核心数据结构:链表
责任链模式将请求沿着处理者链传递,每个处理者决定是否处理或传递给下一个,天然是链表结构。
java
/**
* 责任链模式 ------ 请求处理器链
* 底层数据结构:单向链表
*/
public abstract class Handler {
private Handler next;
/**
* 设置下一个处理者:链表 next 指针
*/
public Handler setNext(Handler next) {
this.next = next;
return next;
}
/**
* 处理请求:链表遍历
*/
public abstract void handleRequest(Request request);
/**
* 传递给下一个处理者
*/
protected void passToNext(Request request) {
if (next != null) {
next.handleRequest(request);
}
}
public static class Request {
private final String type;
private final String data;
public Request(String type, String data) {
this.type = type;
this.data = data;
}
public String getType() { return type; }
public String getData() { return data; }
}
}
24.4.3 策略模式
核心数据结构:哈希表(策略注册表)
策略模式通过将算法封装为独立的策略对象,使用哈希表来管理策略的注册与查找。
java
import java.util.Map;
import java.util.HashMap;
/**
* 策略模式 ------ 支付策略工厂
* 底层数据结构:HashMap(策略名 → 策略实例)
*/
public class PaymentStrategyFactory {
private final Map<String, PaymentStrategy> strategyMap = new HashMap<>();
/**
* 注册策略:HashMap.put
*/
public void register(String type, PaymentStrategy strategy) {
strategyMap.put(type, strategy);
}
/**
* 获取策略:HashMap.get (O(1))
*/
public PaymentStrategy getStrategy(String type) {
PaymentStrategy strategy = strategyMap.get(type);
if (strategy == null) {
throw new IllegalArgumentException("未知的支付方式: " + type);
}
return strategy;
}
public interface PaymentStrategy {
void pay(int amount);
}
}
24.4.4 设计模式与数据结构对应总结
| 设计模式 | 核心数据结构 | 数据结构的作用 |
|---|---|---|
| 观察者模式 | 哈希表 + 链表 | 主题到观察者的映射与存储 |
| 责任链模式 | 链表 | 处理者链的串联与传递 |
| 策略模式 | 哈希表 | 策略名到策略对象的映射 |
| 迭代器模式 | 栈/队列 | 遍历复杂数据结构(如树的迭代遍历) |
| 享元模式 | 哈希表 | 共享对象的缓存池 |
| 命令模式 | 队列/栈 | 命令的排队执行与撤销重做 |
| 状态模式 | 状态机(图) | 状态转换的有限自动机 |
| 组合模式 | 树 | 部分-整体的层次结构 |
24.5 真实工程场景中的算法应用
算法不只是面试题,在真实的工程场景中无处不在。以下是几个典型案例。
24.5.1 搜索引擎------倒排索引与 TF-IDF
搜索引擎的核心是倒排索引 ,本质是一个 哈希表(词 → 文档链表) 的结构。
java
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
/**
* 倒排索引简化实现
* 底层数据结构:HashMap(关键词 → 文档ID列表)
*/
public class InvertedIndex {
private final Map<String, List<Integer>> index = new HashMap<>();
private int docCount = 0;
/**
* 添加文档:分词 → HashMap.put
*/
public int addDocument(String[] keywords) {
int docId = docCount++;
for (String keyword : keywords) {
index.computeIfAbsent(keyword, k -> new ArrayList<>()).add(docId);
}
return docId;
}
/**
* 单词搜索:HashMap.get → O(1)
*/
public List<Integer> search(String keyword) {
return index.getOrDefault(keyword, new ArrayList<>());
}
/**
* 多词 AND 搜索:多个有序链表求交集(双指针)
*/
public List<Integer> searchAnd(String keyword1, String keyword2) {
List<Integer> list1 = search(keyword1);
List<Integer> list2 = search(keyword2);
List<Integer> result = new ArrayList<>();
int i = 0, j = 0;
while (i < list1.size() && j < list2.size()) {
if (list1.get(i).equals(list2.get(j))) {
result.add(list1.get(i));
i++; j++;
} else if (list1.get(i) < list2.get(j)) {
i++;
} else {
j++;
}
}
return result;
}
/**
* 计算 TF-IDF(词频-逆文档频率)
* TF = 词在文档中出现的次数 / 文档总词数
* IDF = log(总文档数 / 包含该词的文档数)
*/
public double tfIdf(String keyword, int docId, int totalWordsInDoc) {
List<Integer> docList = search(keyword);
int tf = 0;
for (int id : docList) {
if (id == docId) tf++;
}
double idf = Math.log((double) docCount / (docList.size() + 1));
return ((double) tf / totalWordsInDoc) * idf;
}
}
算法要点 :倒排索引用哈希表 实现 O(1) 的词查找;多词搜索用双指针 合并有序链表;排序用堆实现 Top-K 结果。
24.5.2 推荐系统------协同过滤
推荐系统中最经典的算法之一是基于用户的协同过滤,核心是计算用户之间的相似度。
java
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
/**
* 协同过滤简化实现
* 核心算法:余弦相似度计算
*/
public class CollaborativeFiltering {
// userId → (itemId → rating)
private final Map<Integer, Map<Integer, Double>> userRatings = new HashMap<>();
public void addUserRating(int userId, int itemId, double rating) {
userRatings.computeIfAbsent(userId, k -> new HashMap<>()).put(itemId, rating);
}
/**
* 计算两个用户的余弦相似度
* cos(A, B) = (A·B) / (|A| × |B|)
*
* 仅对两个用户共同评价过的物品计算
*/
public double cosineSimilarity(int userA, int userB) {
Map<Integer, Double> ratingsA = userRatings.get(userA);
Map<Integer, Double> ratingsB = userRatings.get(userB);
if (ratingsA == null || ratingsB == null) return 0.0;
// 求交集:共同评价过的物品
Set<Integer> commonItems = new HashSet<>(ratingsA.keySet());
commonItems.retainAll(ratingsB.keySet());
if (commonItems.isEmpty()) return 0.0;
double dotProduct = 0.0, normA = 0.0, normB = 0.0;
for (int item : commonItems) {
double a = ratingsA.get(item);
double b = ratingsB.get(item);
dotProduct += a * b;
normA += a * a;
normB += b * b;
}
double denominator = Math.sqrt(normA) * Math.sqrt(normB);
return denominator == 0 ? 0.0 : dotProduct / denominator;
}
/**
* 为指定用户推荐物品
* 找到最相似的 K 个用户,推荐他们喜欢但当前用户未评价的物品
*/
public Map<Integer, Double> recommend(int userId, int topK) {
Map<Integer, Double> similarities = new HashMap<>();
for (int otherUser : userRatings.keySet()) {
if (otherUser != userId) {
similarities.put(otherUser, cosineSimilarity(userId, otherUser));
}
}
// 用堆取 Top-K 相似用户
Map<Integer, Double> recommendations = new HashMap<>();
similarities.entrySet().stream()
.sorted(Map.Entry.<Integer, Double>comparingByValue().reversed())
.limit(topK)
.forEach(entry -> {
Map<Integer, Double> otherRatings = userRatings.get(entry.getKey());
Map<Integer, Double> myRatings = userRatings.get(userId);
for (Map.Entry<Integer, Double> rating : otherRatings.entrySet()) {
if (!myRatings.containsKey(rating.getKey())) {
recommendations.merge(rating.getKey(),
rating.getValue() * entry.getValue(), Double::sum);
}
}
});
return recommendations;
}
}
算法要点 :协同过滤用哈希表 存储用户评分矩阵;用集合求交集 找共同评价;用堆取 Top-K 相似用户。
24.5.3 分布式系统------一致性哈希
一致性哈希是分布式缓存(如 Redis 集群)的核心算法,解决了节点增减时大量数据迁移的问题。
java
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性哈希简化实现
* 底层数据结构:TreeMap(红黑树),保证有序性
*/
public class ConsistentHashing {
private final TreeMap<Integer, String> ring = new TreeMap<>();
private final int virtualNodeCount;
public ConsistentHashing(int virtualNodeCount) {
this.virtualNodeCount = virtualNodeCount;
}
/**
* 添加节点:计算虚拟节点的哈希值,放入 TreeMap
*/
public void addNode(String node) {
for (int i = 0; i < virtualNodeCount; i++) {
int hash = hash(node + "#VN" + i);
ring.put(hash, node);
}
}
/**
* 移除节点
*/
public void removeNode(String node) {
for (int i = 0; i < virtualNodeCount; i++) {
int hash = hash(node + "#VN" + i);
ring.remove(hash);
}
}
/**
* 获取数据应该存放的节点
* 核心:TreeMap.ceilingKey / higherKey → O(log n)
*/
public String getNode(String key) {
if (ring.isEmpty()) return null;
int hash = hash(key);
// 顺时针找第一个大于等于 hash 的节点
SortedMap<Integer, String> tailMap = ring.tailMap(hash);
int nodeHash = tailMap.isEmpty() ? ring.firstKey() : tailMap.firstKey();
return ring.get(nodeHash);
}
/**
* FNV1_32 哈希算法
*/
private int hash(String key) {
final int FNV_32_PRIME = 0x01000193;
int hashValue = 0x811c9dc5;
for (int i = 0; i < key.length(); i++) {
hashValue ^= key.charAt(i);
hashValue *= FNV_32_PRIME;
}
return hashValue;
}
}
算法要点:一致性哈希用**红黑树(TreeMap)**维持哈希环的有序性,使得节点查找为 O(log n);虚拟节点解决数据倾斜问题。
24.5.4 工程应用总结
| 工程场景 | 核心数据结构 | 核心算法 | 时间复杂度 |
|---|---|---|---|
| 搜索引擎 | 倒排索引(哈希表 + 链表) | TF-IDF、双指针合并 | 查询 O(1) + 合并 O(n) |
| 推荐系统 | 评分矩阵(哈希表嵌套) | 余弦相似度、Top-K 堆 | 相似度 O(n),推荐 O(n log K) |
| 分布式缓存 | 一致性哈希环(红黑树) | 一致性哈希 | 查找 O(log n) |
| 消息队列 | 环形数组 / 堆 | 生产者-消费者、优先级调度 | 入队 O(1)/O(log n) |
| 数据库索引 | B+ 树 | 范围查询、顺序访问 | 查找 O(log n) |
24.6 算法与架构设计的关系
24.6.1 数据结构选择影响架构
架构设计的核心是权衡,而数据结构的选择直接决定了系统的性能边界。
| 架构决策 | 数据结构选择 | 性能影响 |
|---|---|---|
| 缓存策略 | LRU(HashMap + 双向链表) | 淘汰 O(1) |
| 路由表 | 前缀树(Trie) | 最长前缀匹配 O(m) |
| 消息去重 | 布隆过滤器 | 空间 O(n/8),可能误判 |
| 任务调度 | 优先队列(堆) | 取最高优先级 O(log n) |
| 限流器 | 滑动窗口计数器 | 判定 O(1) |
24.6.2 算法思维在架构中的应用
架构设计中很多决策本质上是算法问题:
1. 分治思维 → 微服务拆分
将单体应用按业务边界分解为独立服务,每个服务独立开发、部署、扩展。这就是分治思想在架构层面的体现。
2. 缓存思维 → CDN + 多级缓存
用空间换时间,将热点数据放在离用户更近的位置。
3. 贪心思维 → 负载均衡
每次请求分配给当前负载最小的服务器,是一种贪心策略。
4. 回溯思维 → 重试与熔断
当一次请求失败,回退并尝试其他服务实例;当失败次数超阈值,熔断避免雪崩。
5. 动态规划思维 → 渐进式优化
不追求一次性最优,而是基于当前状态做出最优选择,逐步迭代到更好的架构。
算法思维
分治 → 微服务
缓存 → CDN/Redis
贪心 → 负载均衡
回溯 → 重试/熔断
DP → 渐进式优化
滑动窗口 → 限流
24.7 大厂面试算法题高频考点总结
24.7.1 高频数据结构考点
| 排名 | 数据结构 | 高频题数 | 典型题目 |
|---|---|---|---|
| 1 | 数组/字符串 | ★★★★★ | 两数之和、三数之和、最长子串 |
| 2 | 链表 | ★★★★ | 反转链表、合并有序链表、环检测 |
| 3 | 二叉树 | ★★★★ | 层序遍历、最大深度、路径总和 |
| 4 | 哈希表 | ★★★★ | 两数之和、字母异位词、LRU 缓存 |
| 5 | 栈/队列 | ★★★ | 有效的括号、每日温度、滑动窗口最大值 |
| 6 | 堆 | ★★★ | Top K、合并 K 个链表、数据流中位数 |
| 7 | 图 | ★★ | 岛屿数量、课程表、网络延迟时间 |
| 8 | Trie | ★★ | 单词搜索、前缀匹配 |
24.7.2 高频算法策略考点
| 排名 | 算法策略 | 高频题数 | 典型题目 |
|---|---|---|---|
| 1 | 动态规划 | ★★★★★ | 爬楼梯、打家劫舍、零钱兑换、LCS |
| 2 | 双指针/滑动窗口 | ★★★★ | 两数之和、最小覆盖子串、接雨水 |
| 3 | 二分查找 | ★★★★ | 搜索旋转数组、寻找峰值、区间查找 |
| 4 | BFS/DFS | ★★★★ | 岛屿数量、单词搜索、最短路径 |
| 5 | 回溯 | ★★★ | 全排列、N 皇后、组合总和 |
| 6 | 贪心 | ★★★ | 跳跃游戏、区间调度、分发糖果 |
| 7 | 分治 | ★★ | 排序、最大子数组和、搜索二维矩阵 |
| 8 | 位运算 | ★★ | 只出现一次的数字、子集枚举 |
24.7.3 大厂面试高频 Top 20 题目
以下是根据 LeetCode 频率和面试反馈整理的 Top 20 高频题:
| 排名 | 题目 | 难度 | 核心策略 | 数据结构 |
|---|---|---|---|---|
| 1 | 两数之和 | Easy | 哈希表 | HashMap |
| 2 | 反转链表 | Easy | 迭代/递归 | 链表 |
| 3 | 无重复字符的最长子串 | Medium | 滑动窗口 | HashMap |
| 4 | 二叉树层序遍历 | Medium | BFS | 队列 |
| 5 | 合并两个有序链表 | Easy | 双指针 | 链表 |
| 6 | 有效的括号 | Easy | 栈匹配 | 栈 |
| 7 | 最大子数组和 | Medium | DP / 分治 | 数组 |
| 8 | 三数之和 | Medium | 排序 + 双指针 | 数组 |
| 9 | 爬楼梯 | Easy | DP | 数组 |
| 10 | 岛屿数量 | Medium | DFS/BFS | 矩阵 |
| 11 | LRU 缓存 | Medium | 哈希 + 双向链表 | HashMap + LinkedList |
| 12 | 接雨水 | Hard | 双指针/栈/DP | 数组/栈 |
| 13 | 二叉树最大深度 | Easy | DFS/递归 | 树 |
| 14 | 搜索旋转排序数组 | Medium | 二分查找 | 数组 |
| 15 | 全排列 | Medium | 回溯 | 数组 |
| 16 | 零钱兑换 | Medium | DP | 数组 |
| 17 | 打家劫舍 | Medium | DP | 数组 |
| 18 | 合并 K 个升序链表 | Hard | 分治/堆 | 堆/链表 |
| 19 | 最长递增子序列 | Medium | DP / 二分 | 数组 |
| 20 | 课程表 | Medium | 拓扑排序 | 图 + 队列 |
24.7.4 面试算法题解题模板
以下是应对面试算法题的通用模板代码:
1. 二分查找模板:
java
public int binarySearch(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
2. BFS 模板:
java
import java.util.Queue;
import java.util.LinkedList;
public void bfs(int[][] grid, int startRow, int startCol) {
int rows = grid.length, cols = grid[0].length;
boolean[][] visited = new boolean[rows][cols];
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{startRow, startCol});
visited[startRow][startCol] = true;
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
while (!queue.isEmpty()) {
int[] curr = queue.poll();
int r = curr[0], c = curr[1];
for (int[] dir : directions) {
int nr = r + dir[0], nc = c + dir[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols
&& !visited[nr][nc] && grid[nr][nc] == 1) {
visited[nr][nc] = true;
queue.offer(new int[]{nr, nc});
}
}
}
}
3. DFS 模板:
java
public void dfs(int[][] grid, int r, int c, boolean[][] visited) {
int rows = grid.length, cols = grid[0].length;
if (r < 0 || r >= rows || c < 0 || c >= cols
|| visited[r][c] || grid[r][c] == 0) {
return;
}
visited[r][c] = true;
dfs(grid, r - 1, c, visited);
dfs(grid, r + 1, c, visited);
dfs(grid, r, c - 1, visited);
dfs(grid, r, c + 1, visited);
}
4. 回溯模板:
java
import java.util.List;
import java.util.ArrayList;
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(int[] nums, int start, List<Integer> path,
List<List<Integer>> result) {
result.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
backtrack(nums, i + 1, path, result);
path.remove(path.size() - 1);
}
}
5. DP 一维模板:
java
public int climbStairs(int n) {
if (n <= 2) return n;
int prev2 = 1, prev1 = 2;
for (int i = 3; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
24.8 本系列全篇知识图谱
二十四章的知识体系可以用以下图谱来概括:
数据结构与算法
基础篇
非线性结构篇
算法设计篇
高级专题篇
线性表
链表算法
数组与矩阵
栈
队列
树-基础
树-进阶
堆与优先队列
哈希表
跳表
图
递归与分治
排序-比较类
排序-非比较类
二分查找
贪心
DP-基础
DP-进阶
回溯
字符串算法
位运算
高级数据结构
算法思维总结
24.9 总结
24.9.1 核心回顾
本章从十大算法策略出发,系统回顾了分治、贪心、DP、回溯、搜索、枚举、模拟、位运算、双指针和滑动窗口的核心思想与适用场景。然后深入探讨了刷题方法论、设计模式中的数据结构、工程场景中的算法应用、算法与架构的关系,最后总结了面试高频考点。
24.9.2 学习算法的三个境界
- 见山是山:理解每种数据结构和算法的基本原理,能看懂别人的代码
- 见山不是山:能识别问题背后的算法模式,将不同题目归类到对应的策略
- 见山还是山:融会贯通,面对新问题能灵活组合多种策略,甚至创造新方法
最后一句话 :算法学习没有捷径,但有方法。理解原理 → 动手实现 → 大量练习 → 总结复盘,这四个环节缺一不可。希望这个系列能成为你算法之路上的一块基石,而非终点。
全系列完结:至此,《数据结构与算法(Java 语言)》系列博客二十四章全部完成。从第一章绪论到本章的思维总结,我们系统学习了数据结构与算法的核心知识。未来的路上,持续练习、持续思考,算法思维将成为你解决一切复杂问题的利器。
上篇:第二十三章、高级数据结构