算法与数据结构入门学习总结:从零看懂核心原理与 LeetCode 避坑技巧
- 约 16 分钟读完 - 全文 6101 字算法与数据结构入门学习总结
目标读者:刚开始刷题、想系统梳理基础的同学。
参考文档:D:\doc\算法\combined_document.md(《剑指 Offer(专项突破版)》题解合集)。
阅读目标:理解核心原理、记住常用模板、少踩常见坑。
0. 写在前面:为什么要先建立“知识地图”
刷题最怕两种情况:
- 拿到题完全没思路,只能硬写暴力,时间超了也不知道怎么优化。
- 大概知道方向,但细节错一堆:数组越界、整数溢出、指针空指针、边界条件漏掉。
这篇博客想解决的就是这两个问题:先搭好知识框架,再集中火力避坑。只要你知道“这道题可能考什么数据结构/算法思想”,就能快速定位解法。
0.1 学习路线图
flowchart LR
A[语言基础] --> B[复杂度分析]
B --> C[线性结构<br/>数组/链表/栈/队列]
C --> D[非线性结构<br/>树/堆/图]
D --> E[高级结构<br/>哈希/并查集/Trie]
E --> F[基础算法思想]
F --> G[专项刷题]
F --> H[面试/竞赛]
建议学习顺序:
- 先学 复杂度分析,它是评判代码好坏的尺子。
- 再学 数组、链表、栈、队列,它们是后续所有结构的“建筑材料”。
- 进入 树、堆、图,理解非线性结构的遍历方式。
- 最后系统学习 算法思想:双指针、滑动窗口、二分、前缀和、回溯、动态规划、BFS/DFS。
1. 复杂度:衡量算法的尺子
1.1 时间复杂度
时间复杂度描述的是算法运行时间随输入规模增长的趋势,不是真正的秒数。
常见复杂度(从小到大):
| 复杂度 | 名称 | 典型场景 |
|---|---|---|
| O(1) | 常数 | 哈希表查询、数组随机访问 |
| O(log n) | 对数 | 二分查找、倍增 |
| O(n) | 线性 | 一次遍历 |
| O(n log n) | 线性对数 | 快排、归并排序 |
| O(n²) | 平方 | 双重循环、朴素 DP |
| O(2ⁿ) / O(n!) | 指数/阶乘 | 回溯、全排列 |
估算口诀:看循环层数 + 看每次循环内部有没有“折半”操作。
1.2 空间复杂度
空间复杂度描述算法额外占用的内存。注意:
- 输入数组本身不计入额外空间。
- 递归调用栈深度也要算空间。
flowchart TD
A[分析代码] --> B[有没有开辟新数组/哈希表?]
A --> C[有没有递归?]
B --> D[空间 = 数据结构大小]
C --> E[空间 = 递归最大深度]
D --> F[取最大值]
E --> F
2. 数据结构全景图
mindmap
root((数据结构与算法))
线性结构
数组
链表
栈
队列
非线性结构
树
二叉树
二叉搜索树
堆
图
集合与映射
哈希表
并查集
字符串专属
前缀树 Trie
算法思想
双指针/滑动窗口
二分/前缀和
递归/回溯
动态规划
贪心/分治
2.1 各数据结构的核心能力
| 数据结构 | 核心能力 | 最适合解决的问题 |
|---|---|---|
| 数组 | 随机访问 O(1) | 按索引查询、前缀和、双指针 |
| 链表 | 动态增删 O(1) | 反转、合并、环检测 |
| 栈 | 后进先出 LIFO | 括号匹配、表达式求值、单调栈 |
| 队列 | 先进先出 FIFO | BFS、滑动窗口、单调队列 |
| 哈希表 | 快速查找 O(1) | 去重、计数、两数之和 |
| 树/ BST | 分层、有序 | 遍历、最近公共祖先、中序有序 |
| 堆 | 快速取最值 O(log n) | TopK、合并 K 个有序数组 |
| 图 | 描述多对多关系 | 最短路径、拓扑排序、连通性 |
| Trie | 前缀匹配 | 单词前缀、最大异或、自动补全 |
3. 线性结构:数组、链表、栈、队列
3.1 数组:最朴素但最万能
数组的特点是连续内存 + 随机访问。数组题最常见的套路是:
- 双指针:相向(两数之和)、同向(滑动窗口)。
- 前缀和:把“区间求和”变成“两个前缀和相减”。
- 排序后处理:很多题先排序,复杂度就从 O(n²) 降到 O(n log n)。
双指针模板
flowchart TD
A[初始化 left, right] --> B{指针方向?}
B -->|相向| C[left=0, right=n-1]
B -->|同向| D[left=0, right=0]
C --> E[while left < right]
D --> F[for right in 0..n-1]
E --> G[根据条件移动 left 或 right]
F --> H[按需收缩 left 扩展窗口]
G --> I[更新答案]
H --> I
例题思路串讲:
| 题目 | 核心技巧 | 关键记忆点 |
|---|---|---|
| 排序数组中两数之和 | 相向双指针 | 和大了 right--,和小了 left++ |
| 三数之和 | 排序 + 双指针 + 去重 | 外层和内层都要 while 跳过重复值 |
| 和 ≥ target 的最短子数组 | 同向双指针/滑动窗口 | 右扩左缩,记录最小长度 |
| 乘积 < K 的子数组 | 同向双指针 | 满足条件时新增 right-left+1 个子数组 |
| 和为 K 的子数组 | 前缀和 + 哈希表 | map.put(0, 1) 必不可少 |
| 0 和 1 个数相同的子数组 | 0 变 -1 后转“和为 0” | map.put(0, -1) 处理从开头开始的子数组 |
| 二维子矩阵的和 | 二维前缀和 | sum[i+1][j+1] = sum[i][j+1] + rowSum |
三数之和去重是新人重灾区,一定要记住:排序后,如果当前值和上一个值相同,就跳过,否则会产生重复三元组。
3.2 链表:虚拟头节点 + 快慢指针
链表不像数组能随机访问,所以核心技巧是:
- 虚拟头节点(dummy):方便删除头节点,统一代码。
- 快慢指针:找中点、找环、找倒数第 K 个。
- 反转链表:三指针
pre / now / next一遍完成。
// 反转链表模板
public ListNode reverseList(ListNode head) {
ListNode pre = null, now = head;
while (now != null) {
ListNode next = now.next;
now.next = pre;
pre = now;
now = next;
}
return pre;
}链表常见坑:
- 忘记处理空链表或单节点。
- 移动指针时把
next覆盖了,后面没法继续遍历。 - 找中点时快慢指针初始化/终止条件写错(建议画图手推一次)。
3.3 栈:后进先出 + 单调栈
栈的两个高频用法:
- 辅助计算:括号匹配、后缀表达式、进制转换。
- 单调栈:找“下一个更大/更小元素”、直方图最大矩形。
flowchart LR
A[遍历元素] --> B{栈顶与新元素关系?}
B -->|满足单调性| C[压栈]
B -->|破坏单调性| D[弹出栈顶并计算]
D --> B
C --> E[继续遍历]
3.4 队列:先进先出 + BFS 利器
队列最经典的应用是 BFS(广度优先搜索):一层一层扩展,天然适合“最短路径/最浅结果”。
flowchart TD
A[起点入队] --> B[while 队列非空]
B --> C[队首出队]
C --> D[扩展所有邻居]
D --> E[未访问的邻居入队]
E --> B
4. 非线性结构:树、堆、图
4.1 二叉树:递归是王道
二叉树题目 90% 都能用递归解决,核心想清楚三件事:
- 当前节点要做什么?
- 递归返回什么?
- 边界条件是什么?(通常是
node == null)
flowchart TD
A[处理当前节点] --> B[递归左子树]
A --> C[递归右子树]
B --> D[合并左右结果]
C --> D
二叉树遍历:
- 前序:根 → 左 → 右
- 中序:左 → 根 → 右(BST 中序有序)
- 后序:左 → 右 → 根
二叉搜索树(BST)性质:左子树所有节点 < 根 < 右子树所有节点。利用这个性质可以 O(h) 查找、插入、删除。
4.2 堆:优先队列 = 动态最值
堆通常用优先队列实现。核心能力:
- 插入 O(log n)
- 取最值 O(log n)
- 看最值 O(1)
TopK 问题(找前 K 大/小)是堆最经典的面试题。思路:维护一个大小为 K 的小根堆,堆里保存目前最大的 K 个数。
4.3 图:BFS vs DFS
图是最灵活的数据结构,几乎所有图算法都基于 BFS 或 DFS。
flowchart TD
A[图的遍历] --> B{要最短/分层?}
B -->|是| C[BFS 队列]
B -->|否| D[DFS 递归/栈]
C --> E[visited 集合防重复]
D --> E
图的常见题型:
| 问题 | 首选算法 |
|---|---|
| 岛屿数量、连通分量 | DFS / 并查集 |
| 最短路径(无权图) | BFS |
| 拓扑排序 | Kahn 算法(入度)/ DFS 后序逆序 |
| 最小生成树 | Kruskal / Prim |
| 单源最短路(带权) | Dijkstra / Bellman-Ford |
5. 高级结构与技巧
5.1 哈希表:用空间换时间
哈希表能把“查找”从 O(n) 降到 O(1)。刷题时常见的哈希用法:
- 计数:统计字符/数字出现次数。
- 去重:判断元素是否出现过。
- 记录位置:LRU、两数之和、前缀和下标。
经典设计题:
- RandomizedSet:数组 + 哈希表,删除时把末尾元素 swap 到被删位置,保证 O(1)。
- LRU Cache:哈希表 + 双向链表,get/put 都把节点移到尾部,超出容量时删头部。
5.2 前缀树 Trie
Trie 用于高效处理字符串前缀、自动补全、最大异或等问题。
flowchart TD
A[root] --> B[a]
A --> C[b]
B --> D[p]
B --> E[p2]
C --> F[e]
D --> G[p<br/>isEnd=true]
E --> H[p<br/>isEnd=true]
F --> I[e<br/>isEnd=true]
每个节点代表一个字符前缀;叶子或标记节点表示完整单词。
5.3 并查集
并查集解决“连通性”问题,三个操作:
find(x):找到 x 的根。union(x, y):合并两个集合。connected(x, y):判断是否同根。
配合 路径压缩 和 按秩合并,单次操作近似 O(α(n)),可以认为接近 O(1)。
6. 算法思想:把题归类
6.1 二分查找:不是只能查有序数组
二分查找的精髓是:找到一个可以判定“答案是否可行”的条件,然后不断缩小答案区间。
flowchart TD
A[确定二分对象] --> B[写出 canmid 判定]
B --> C[初始化 l, r]
C --> D[while l < r]
D --> E{canmid?}
E -->|true| F[答案在左半区<br/>r = mid]
E -->|false| G[答案在右半区<br/>l = mid + 1]
F --> D
G --> D
D --> H[返回 l]
二分查找避坑:
- 循环条件写成
l <= r还是l < r,取决于你要不要包含右端点。 mid = l + (r - l) / 2比(l + r) / 2更安全,防止整数溢出。- 左/右边界更新要对称:左闭右闭就
l = mid + 1, r = mid - 1;左闭右开要相应调整。
6.2 滑动窗口:双指针的高级形态
滑动窗口适合处理子数组/子串问题,前提是“右指针右移时,窗口内某种性质单调变化”。
flowchart LR
A[初始化 left=0] --> B[right 从左到右遍历]
B --> C[扩大窗口 更新状态]
C --> D{窗口是否合法?}
D -->|不合法| E[移动 left 收缩窗口]
D -->|合法| F[更新答案]
E --> F
F --> B
什么时候不能用滑动窗口? 当数组不是单调(例如有负数)时,和/积不具备单调性,要改用前缀和 + 哈希表。
6.3 前缀和:区间查询的利器
前缀和把“区间和”问题变成“差分”:
sum(i, j) = prefix[j+1] - prefix[i]
二维前缀和公式:
prefix[i][j] = matrix[i-1][j-1]
+ prefix[i-1][j] + prefix[i][j-1]
- prefix[i-1][j-1]
一维前缀和 + 哈希表 可以把“和为 K 的子数组”从 O(n²) 降到 O(n)。
6.4 递归与回溯:暴力枚举的艺术
回溯 = 递归 + 做选择 + 撤销选择。模板:
void backtrack(路径, 选择列表) {
if (满足结束条件) {
result.add(路径);
return;
}
for (选择 in 选择列表) {
做选择;
backtrack(路径, 选择列表);
撤销选择;
}
}graph TD
A[开始 backtrack] --> B{剪枝/终止?}
B -->|是| C[记录答案]
B -->|否| D[做选择]
D --> E[递归下一层]
E --> F[撤销选择]
F --> G[尝试下一个选择]
G --> D
C --> H[返回]
回溯常见题型:子集、组合、排列、N 皇后、括号生成、复原 IP。
6.5 动态规划:找状态转移
DP 是很多新人的噩梦。其实套路很固定:
flowchart LR
A["定义状态 dp[i] 或 dp[i][j]"] --> B[写状态转移方程]
B --> C[确定初始状态]
C --> D[确定遍历顺序]
D --> E[空间优化可选]
入门 DP 分类:
| 类型 | 特征 | 例题 |
|---|---|---|
| 线性 DP | 一维数组递推 | 爬楼梯、房屋偷盗、最长递增子序列 |
| 区间 DP | 区间 [i,j] 状态 | 最长回文子序列、最少回文分割 |
| 背包 DP | 选或不选 | 分割等和子集、最少硬币 |
| 路径 DP | 二维网格 | 不同路径、最小路径和 |
| 状态压缩 DP | 用二进制表示集合 | 旅行商问题、棋盘覆盖 |
DP 避坑:
- 状态定义不清晰就开始写,写到一半发现转移不了。
- 遍历顺序搞反,导致用到还没计算出来的状态。
- 忘记初始化
dp[0]或边界行/列。
6.6 贪心:每次局部最优
贪心没有通用模板,关键是证明局部最优能推导出全局最优。常见贪心场景:
- 区间问题:按结束时间排序选最多不重叠区间。
- 跳跃游戏:每次选能跳最远的位置。
- 哈夫曼编码:每次合并最小的两个。
如果你不确定贪心对不对,建议先用 DP 或回溯验证小例子。
7. 位运算与整数技巧
参考文档第一章专门讲整数,里面有很多位运算技巧,面试题也常考。
7.1 常用位运算技巧
| 技巧 | 含义 | 例题 |
|---|---|---|
n & (n-1) |
把 n 最右侧的 1 变成 0 | 统计二进制 1 的个数 |
n & (-n) |
只保留最右侧的 1 | 判断是否是 2 的幂 |
a ^ a = 0 |
相同数异或为 0 | 只出现一次的数字 |
a ^ 0 = a |
与 0 异或不变 | 两数交换 |
7.2 整数除法:倍增法
最朴素的除法是不断减,最坏 O(n)。倍增法的思想是:判断被除数能容纳除数的 1 倍、2 倍、4 倍……直到不能容纳,然后减去最大的倍数,重复这个过程。
复杂度:O(log n)。
避坑:
Integer.MIN_VALUE / -1会溢出。- 为了处理符号,通常把两个数都转成负数计算,最后再加符号。
- 累加时要判断是否超过
0xc0000000(即最小负数的一半),防止溢出。
7.3 状态压缩:用 int 表示集合
当数据范围只有 26 个小写字母、或 20 个以内的元素时,可以用一个 int 的每一位表示“选/没选”。
例如“单词长度的最大乘积”中,用 int 的低 26 位表示一个单词包含哪些字母,两个单词的位掩码 & 为 0 就说明没有公共字母。
8. 排序算法回顾
| 排序 | 平均时间 | 空间 | 稳定性 | 特点 |
|---|---|---|---|---|
| 冒泡/选择/插入 | O(n²) | O(1) | 稳定(插入) | 简单,数据量小可用 |
| 快速排序 | O(n log n) | O(log n) | 不稳定 | 平均最快 |
| 归并排序 | O(n log n) | O(n) | 稳定 | 链表排序、逆序对 |
| 堆排序 | O(n log n) | O(1) | 不稳定 | TopK、优先队列 |
| 计数/桶/基数 | O(n+k) | O(k) | 稳定 | 数据范围有限 |
实战建议:
- 数组排序直接用语言内置函数(Java
Arrays.sort())。 - 链表排序适合归并(自顶向下或自底向上)。
- 找第 K 大可以用快速选择(平均 O(n))或堆。
9. LeetCode 刷题闭坑指南(重点)
这部分把参考文档里反复出现的“坑”集中整理出来,刷题前先看一遍,能少错很多。
9.1 拿到题后先做什么
flowchart TD
A[拿到题目] --> B[读三遍题意]
B --> C[列出所有边界条件]
C --> D[手写 2-3 个小例子]
D --> E[判断数据结构与算法]
E --> F[写代码]
F --> G[脑内跑一遍边界]
G --> H[提交]
9.2 整数与边界
| 坑 | 案例 |
|---|---|
| 整数溢出 | Integer.MIN_VALUE / -1 返回 Integer.MAX_VALUE |
| 越界累加 | 除法倍增时判断 value >= 0xc0000000 |
| 负数右移 | 负数算术右移最高位永远补 1 |
mid 溢出 |
二分写 mid = l + (r - l) / 2 |
9.3 数组与字符串
| 坑 | 案例 |
|---|---|
| 下标越界 | right = n 还是 right = n-1 要统一 |
| 左/右边界更新 | 注意是 ++left 还是 left++(三数之和去重用 nums[++left]) |
| 前缀和初始化 | 和为 K 子数组必须 map.put(0, 1) |
| 二维前缀和 | 通常多开一行一列避免边界判断 |
| 字符串变位词 | 用长度 26/58 的 int 数组模拟哈希表 |
9.4 链表
| 坑 | 解决方案 |
|---|---|
| 头节点可能被删 | 使用 dummy 节点 |
| 空指针 | 访问 node.next 前先判空 |
| 找中点/环 | 快慢指针初始化和终止条件画图确认 |
| 反转丢失 next | 先用临时变量保存 now.next |
9.5 递归与回溯
| 坑 | 解决方案 |
|---|---|
| 终止条件漏写 | 先写边界,再写递归体 |
| 共享状态被污染 | 回溯时记得“撤销选择” |
| 重复结果 | 排序后跳过相同元素,或用 visited 数组 |
| 栈溢出 | 深度太大时考虑改迭代 + 栈 |
9.6 动态规划
| 坑 | 解决方案 |
|---|---|
| 状态定义错误 | 先用一句话描述“dp[i] 表示什么” |
| 遍历顺序错误 | 画依赖图,确保计算 dp[i] 时所需状态已算好 |
| 边界没初始化 | 先填第 0 行/列,或 dp[0] |
| 空间可以优化 | 滚动数组、一维降维 |
9.7 二分查找
| 坑 | 解决方案 |
|---|---|
| 循环条件写错 | 根据开闭区间选择 l < r 或 l <= r |
| 边界更新不对称 | l = mid + 1 对应 r = mid 或 r = mid - 1 |
| 死循环 | 左中点 mid = l + (r-l)/2 与右中点 mid = l + (r-l+1)/2 选清楚 |
10. 一张图串联所有知识点
flowchart TD
A[算法题] --> B{看到什么关键词?}
B -->|子数组/子串<br/>和/积/去重| C[双指针 / 滑动窗口]
B -->|区间和/连续和| D[前缀和 + 哈希表]
B -->|有序 / 最大化最小值| E[二分查找]
B -->|所有组合/排列/子集| F[回溯]
B -->|最优子结构| G[动态规划]
B -->|最短路径/层序| H[BFS]
B -->|连通/全遍历| I[DFS / 并查集]
B -->|TopK / 动态最值| J[堆]
B -->|前缀匹配| K[Trie]
C --> L[写代码]
D --> L
E --> L
F --> L
G --> L
H --> L
I --> L
J --> L
K --> L
11. 给新人的刷题路线建议
- 第 1-2 周:数组 + 链表
- 两数之和、三数之和、反转链表、合并两个有序链表、环形链表。
- 第 3-4 周:栈 + 队列 + 哈希表
- 有效的括号、最小栈、LRU 缓存、字母异位词。
- 第 5-6 周:树 + 递归
- 二叉树前中后序遍历、二叉树层序遍历、二叉搜索树验证、路径总和。
- 第 7-8 周:回溯 + 动态规划
- 全排列、组合总和、爬楼梯、最长公共子序列、不同路径。
- 第 9-10 周:图 + 高级结构
- 岛屿数量、课程表(拓扑排序)、数据流第 K 大、实现 Trie。
- 持续:按标签二刷 + 总结错题本
刷题原则:
- 第一遍不要死磕,20 分钟没思路直接看题解,重点理解“为什么这样想”。
- 第二天再独立写一遍,一周后回来再写一遍。
- 把每道题的“思路关键词”写进错题本,考前只看错题本。
12. 结语
算法和数据结构不是背代码,而是建立**“看到题目 → 识别模式 → 套用模板 → 处理边界”**的条件反射。希望这篇总结能帮你把零散的知识点串成一张网,刷题时少踩坑、多 AC。
最后送一句话:慢即是快。把一类题真正搞懂,比囫囵吞枣刷一百道更有效。
参考文档:`(《剑指 Offer(专项突破版)》题解合集)。
本文已整理成博客,欢迎在留言区补充你踩过的坑。