1. 数据结构复习指南:从基础到实战
作为一名计算机专业的老兵,我深知数据结构在编程和算法中的核心地位。每次面试或技术评审前,我都会系统地梳理数据结构知识体系。今天分享的这份复习指南,是我多年积累的实战笔记,包含高频考点和易错点分析。
数据结构不仅是计算机科学的理论基础,更是解决实际工程问题的利器。无论是校招笔试、考研复试还是日常开发,扎实的数据结构功底都能让你事半功倍。本指南采用渐进式结构,从线性表到复杂树结构,最后到高级图算法,每个章节都配有典型例题和优化技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线性结构:程序设计的基石
2.1 数组与链表的性能博弈
数组的随机访问特性(O(1)时间复杂度)使其成为缓存友好的数据结构,特别是在处理矩阵运算、图像处理等场景。但固定大小的限制和插入删除的高成本(O(n))是其硬伤。
链表则相反,动态扩展能力和O(1)的插入删除性能,使其成为实现栈、队列等抽象数据类型的理想选择。但遍历查找需要O(n)时间,且指针带来的内存开销不可忽视。
实战技巧:在LeetCode #146 LRU缓存题中,结合哈希表(快速查找)和双向链表(快速调整)是最优解,这种复合结构在实际工程中很常见。
2.2 栈与队列的妙用
栈的后进先出特性在以下场景表现突出:
- 函数调用栈(系统自动维护)
- 括号匹配校验(如LeetCode #20)
- 非递归的DFS实现
队列的先进先出特性则适用于:
- BFS层序遍历
- 消息队列缓冲
- 滑动窗口问题(如LeetCode #239)
c复制// 用数组实现循环队列的要点
typedef struct {
int *data;
int front;
int rear;
int size;
} MyCircularQueue;
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj = malloc(sizeof(MyCircularQueue));
obj->data = malloc(sizeof(int)*(k+1)); // 多留一个空位判满
obj->front = obj->rear = 0;
obj->size = k+1;
return obj;
}
3. 树形结构:层次关系的艺术
3.1 二叉树遍历的六种姿势
前序、中序、后序的递归实现虽然简洁,但面试中常要求非递归写法。以中序遍历为例:
c复制// 非递归中序遍历模板
void inorderTraversal(TreeNode* root) {
stack<TreeNode*> stk;
while(root || !stk.empty()){
while(root){
stk.push(root);
root = root->left;
}
root = stk.top();
stk.pop();
printf("%d ", root->val);
root = root->right;
}
}
层次遍历则需要队列辅助,变形题目如锯齿形遍历(LeetCode #103)、右视图(LeetCode #199)都基于此。
3.2 平衡二叉树的维护策略
AVL树通过旋转操作保持平衡:
- 左旋:处理右子树过高
- 右旋:处理左子树过高
- 先左后右/先右后左:处理LR/RL型不平衡
红黑树则通过颜色标记和更宽松的平衡条件,减少了旋转次数,适合频繁插入删除的场景(如STL中的map)。
4. 图算法:复杂关系的建模
4.1 存储结构的工程选择
邻接矩阵适合稠密图,可快速判断两顶点是否相连(O(1)),但空间复杂度O(V²)。邻接表则适合稀疏图,空间O(V+E),但查询效率O(degree(v))。
cpp复制// 邻接表DFS模板
void dfs(int u, vector<vector<int>>& graph, vector<bool>& visited) {
visited[u] = true;
for(int v : graph[u]){
if(!visited[v]){
dfs(v, graph, visited);
}
}
}
4.2 最短路径算法对比
| 算法 | 适用场景 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| Dijkstra | 无负权边 | O(E+VlogV) | O(V) |
| Bellman-Ford | 含负权边检测负环 | O(VE) | O(V) |
| Floyd | 多源最短路径 | O(V³) | O(V²) |
避坑指南:使用Dijkstra时,优先队列的实现很关键。C++中可用priority_queue,但要注意默认是大顶堆,需传入greater比较器。
5. 高级数据结构实战
5.1 并查集的路径压缩
cpp复制class UnionFind {
public:
vector<int> parent;
UnionFind(int n) {
parent.resize(n);
iota(parent.begin(), parent.end(), 0);
}
int find(int x) {
return parent[x] == x ? x : (parent[x] = find(parent[x])); // 路径压缩
}
void unite(int x, int y) {
parent[find(x)] = find(y);
}
};
这种数据结构在解决朋友圈问题(LeetCode #547)、岛屿数量(LeetCode #200)等问题时效率极高。
5.2 跳表:Redis的有序集合实现
跳表通过多级索引实现O(logN)的查询效率,相比红黑树更易实现且支持区间查询。其核心是通过概率维持平衡:
python复制import random
def random_level():
level = 1
while random.random() < 0.5 and level < MAX_LEVEL:
level += 1
return level
6. 排序算法深度对比
6.1 时间复杂度全景图
| 算法 | 最优 | 平均 | 最差 | 空间 | 稳定 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 是 |
| 快速排序 | O(nlogn) | O(nlogn) | O(n²) | O(logn) | 否 |
| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 是 |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 否 |
6.2 工程实践中的选择
- 小规模数据(n<50):插入排序实际更快
- 内存受限:堆排序
- 需要稳定:归并排序
- 平均性能:快速排序(但要注意最坏情况)
java复制// 快速排序的经典partition写法
int partition(int[] arr, int left, int right) {
int pivot = arr[right];
int i = left;
for(int j=left; j<right; j++){
if(arr[j] < pivot){
swap(arr, i++, j);
}
}
swap(arr, i, right);
return i;
}
7. 哈希表的设计艺术
7.1 冲突解决策略对比
- 开放定址法:线性探测、二次探测
- 链地址法:Java HashMap的实现方式
- 再哈希法:使用第二个哈希函数
7.2 负载因子与扩容策略
当负载因子α=元素数/桶数超过阈值(通常0.75)时触发扩容。Java的HashMap扩容时会将桶数量翻倍,并重新哈希所有元素。
python复制class MyHashMap:
def __init__(self):
self.size = 1000
self.buckets = [[] for _ in range(self.size)]
def _hash(self, key):
return key % self.size
def put(self, key, value):
hash_key = self._hash(key)
bucket = self.buckets[hash_key]
for i, (k, v) in enumerate(bucket):
if k == key:
bucket[i] = (key, value)
return
bucket.append((key, value))
8. 海量数据处理技巧
8.1 位图法应用场景
- 布隆过滤器:使用k个哈希函数和位数组,以一定误判率为代价节省空间
- 统计不重复整数:40亿整数用512MB位图即可处理
8.2 外排序与MapReduce
当数据无法全部装入内存时:
- 分割文件并分别排序
- 使用多路归并(败者树优化)
- Hadoop等分布式系统采用MapReduce范式
9. 算法优化思维训练
9.1 空间换时间典型案例
- 前缀和数组:快速求子数组和(LeetCode #303)
- 单调栈:解决"下一个更大元素"问题(LeetCode #496)
- 双指针:处理有序数组的两数和问题(LeetCode #167)
9.2 递归转迭代的通用方法
通过显式栈模拟调用过程:
cpp复制// 递归版DFS
void dfs(TreeNode* root) {
if(!root) return;
visit(root);
dfs(root->left);
dfs(root->right);
}
// 迭代版DFS
void dfs_iterative(TreeNode* root) {
stack<TreeNode*> stk;
stk.push(root);
while(!stk.empty()){
TreeNode* cur = stk.top();
stk.pop();
if(!cur) continue;
visit(cur);
stk.push(cur->right); // 注意压栈顺序
stk.push(cur->left);
}
}
这份复习资料我会持续更新,建议结合《算法导论》和《数据结构与算法分析》系统学习。在实际刷题时,多思考不同数据结构的适用场景,逐步培养算法直觉。遇到难题时,先暴力求解再逐步优化是个不错的策略。
