数据结构详解与算法关联指南

数据结构详解与算法关联指南

理解数据结构的本质,掌握数据结构与算法的深层联系


目录

  1. 数据结构总览
  2. 数组:连续存储的基石
  3. 链表:离散存储的灵活结构
  4. 栈与队列:受限访问的有序结构
  5. 哈希表:极速查找的映射结构
  6. 树:层次组织的递归结构
  7. 堆:优先级管理的特殊树
  8. 图:复杂关系的网络结构
  9. 数据结构选择策略
  10. 数据结构与算法映射表

1. 数据结构总览

1.1 什么是数据结构?

本质理解:数据结构是数据的"组织方式",决定了数据的存储方式和访问方式。

复制代码
数据 + 结构 = 数据如何存储 + 如何访问

类比理解

  • 数组 = 一排连续的柜子(编号访问)
  • 链表 = 一串连接的盒子(顺序访问)
  • 栈 = 只能从顶部取的盘子堆(后进先出)
  • 队列 = 只能从一头进的排队(先进先出)
  • 树 = 分层的管理结构(上下级关系)
  • 图 = 复杂的人际网络(任意连接)

1.2 数据结构的分类

复制代码
数据结构
├── 线性结构(一对一关系)
│   ├── 数组(连续存储)
│   ├── 链表(离散存储)
│   ├── 栈(受限访问:只能一端操作)
│   ├── 队列(受限访问:两端操作)
│   └── 哈希表(映射存储)
│
├── 树形结构(一对多关系)
│   ├── 二叉树
│   ├── 二叉搜索树(BST)
│   ├── 平衡树(AVL、红黑树)
│   ├── 堆(优先队列)
│   └── B树/B+树
│
└── 图结构(多对多关系)
    ├── 有向图
    ├── 无向图
    ├── 加权图
    └── 稀疏图/稠密图

1.3 数据结构的三个维度

存储维度:数据如何在内存中存储

  • 连续存储(数组):地址连续,索引访问快
  • 离散存储(链表):地址不连续,指针连接
  • 映射存储(哈希表):键值映射,散列存储

访问维度:数据如何被访问

  • 随机访问(数组):O(1)索引访问
  • 顺序访问(链表):O(n)顺序遍历
  • 受限访问(栈/队列):只能特定位置访问
  • 键值访问(哈希表):O(1)键查找

操作维度:支持哪些操作

  • 插入:在哪里插入?O(?)复杂度?
  • 删除:删除哪个元素?O(?)复杂度?
  • 查找:查找特定元素?O(?)复杂度?
  • 遍历:如何访问所有元素?

1.4 数据结构选择的核心问题

选择数据结构时,回答四个核心问题:

复制代码
Q1: 数据规模多大?(决定空间效率)
Q2: 主要操作是什么?(插入/删除/查找/遍历?)
Q3: 操作频率如何?(频繁查找?频繁插入?)
Q4: 时间要求?(O(1)? O(n)? O(log n)?)

2. 数组:连续存储的基石

2.1 数组的基本原理

物理存储:数组在内存中连续存储,每个元素占用固定大小空间。

复制代码
内存地址:    1000    1004    1008    1012    1016
数组元素:    arr[0]  arr[1]  arr[2]  arr[3]  arr[4]
元素值:      1       2       3       4       5

地址计算公式:address(arr[i]) = base + i * sizeof(type)

为什么索引从0开始?

复制代码
如果索引从1开始:
address(arr[1]) = base + 1 * sizeof(type)
address(arr[i]) = base + i * sizeof(type)

计算arr[1]的地址需要额外减1:
address(arr[1]) = base + (1-1) * sizeof(type) = base

从0开始可以直接计算,无需额外减法操作。

核心特性

复制代码
1. 连续存储 → 索引访问 O(1)
2. 固定大小 → 预分配空间
3. 元素类型相同 → sizeof固定
4. 内存连续 → 缓存友好

2.2 数组的时间复杂度

操作 时间复杂度 原因
索引访问 O(1) 直接计算地址
修改元素 O(1) 知道位置直接修改
尾部插入 O(1) 直接放后面(动态数组需考虑扩容)
中间插入 O(n) 后面元素都要移动
中间删除 O(n) 后面元素都要移动
查找元素 O(n) 需要遍历比较
排序 O(n log n) 使用高效排序算法

2.3 数组与算法的关联

数组的连续性 → 算法的基石

复制代码
连续存储的优势:
├── 索引访问快 → 支持随机访问算法
├── 内存连续 → 缓存命中率高 → 性能好
├── 地址可计算 → 支持二分查找(跳跃访问)
└── 局部性好 → 支持双指针、滑动窗口

适合数组的算法

算法 利用数组的什么特性 为什么高效
二分查找 连续存储+索引访问 可以跳跃访问,每次排除一半
双指针 连续存储+索引访问 两个指针协同,避免嵌套循环
滑动窗口 连续存储+索引访问 动态调整边界,无需重建
前缀和 连续存储+索引访问 预处理区间信息,O(1)查询
快速排序 索引交换 原地排序,无需额外空间
堆排序 索引表示父子关系 数组模拟堆结构

2.4 二维数组(矩阵)

内存布局

复制代码
C++:行主序(一行接一行)
内存:[row0_col0][row0_col1]...[row0_colN][row1_col0][row1_col1]...

地址计算:address(arr[i][j]) = base + (i * cols + j) * sizeof(type)

Java:同样是行主序

遍历建议:外层遍历行,内层遍历列(缓存友好)

为什么行主序遍历更快?

复制代码
行主序遍历:访问arr[0][0]→arr[0][1]→arr[0][2]...(连续地址)
缓存一次加载多个连续地址到缓存行,后续访问命中率高。

列主序遍历:访问arr[0][0]→arr[1][0]→arr[2][0]...(跳跃地址)
每次访问可能跨越缓存行,缓存命中率低。

2.5 数组的局限性

复制代码
局限性:
├── 大小固定(静态数组) → 不灵活
├── 插入删除慢 → O(n)移动元素
├── 内存浪费 → 预分配可能过大
└── 连续内存要求 → 大数组可能分配失败

解决方案:
├── 动态数组(vector) → 按需扩容
├── 链表 → 频繁插入删除用链表
├── 预估大小 → 根据需求合理预分配
└── 分段数组 → 大数组分段存储

2.6 动态数组(vector)原理

扩容机制

复制代码
初始容量:capacity = n
已用大小:size = m

当 size == capacity 时:
1. 分配新内存:new_capacity = 2 * capacity(通常翻倍)
2. 复制旧元素:copy(old, new)
3. 释放旧内存:delete old
4. 更新指针:指向新内存

时间复杂度:
- 单次扩容:O(n)
- 均摊分析:O(1)(每次扩容后可以用很久)

为什么翻倍而不是固定增加?

复制代码
翻倍扩容:
n次插入的总复制次数:1 + 2 + 4 + ... + n ≈ 2n
均摊每次插入:O(1)

固定增加k:
n次插入的总复制次数:k + 2k + 3k + ... = k*n²/k = n²
均摊每次插入:O(n)

翻倍扩容保证均摊O(1)。

3. 链表:离散存储的灵活结构

3.1 链表的基本原理

物理存储:链表节点离散存储,通过指针连接。

复制代码
节点结构:
struct Node {
    int data;       // 数据域
    Node* next;     // 指针域(指向下一个节点)
};

内存布局:
节点A(data=1, next→B)   节点B(data=2, next→C)   节点C(data=3, next→NULL)
地址:1000                 地址:2000                地址:3000
    ↓                       ↓                       ↓
[1|→B]    →    [2|→C]    →    [3|NULL]

核心特性

复制代码
1. 离散存储 → 无需连续内存
2. 动态大小 → 随时添加删除节点
3. 顺序访问 → 只能从头遍历
4. 插入删除快 → 只改指针,O(1)
5. 内存开销 → 每个节点额外存指针

3.2 链表的时间复杂度

操作 时间复杂度 原因
索引访问 O(n) 必须从头遍历到位置
头部插入 O(1) 直接改头指针
头部删除 O(1) 直接改头指针
中间插入 O(n) 先遍历到位置,再插入 O(1)
中间删除 O(n) 先遍历到位置,再删除 O(1)
查找元素 O(n) 需要遍历比较
反转链表 O(n) 需要遍历所有节点

3.3 链表与算法的关联

链表的离散性 → 算法的约束

复制代码
离散存储的限制:
├── 无索引访问 → 不能直接跳到某个位置
├── 必须顺序访问 → 不能用二分查找
├── 无法随机访问 → 不能用滑动窗口
└── 无连续性 → 缓存效率低

离散存储的优势:
├── 插入删除快 → 链表操作算法
├── 大小灵活 → 动态管理
├── 无需连续内存 → 可处理大数据
└── 特殊指针操作 → 快慢指针算法

适合链表的算法

算法 利用链表的什么特性 为什么适合
快慢指针 指针操作灵活 可以控制步数,找中点、判断环
虚拟头节点 指针操作灵活 统一头节点和非头节点的处理
反转链表 指针操作灵活 只改指针,不移动数据
合并链表 指针操作灵活 比较后改指针连接
删除节点 插入删除O(1) 找到后直接改指针

3.4 快慢指针原理

为什么快慢指针能找中点?

复制代码
慢指针:每次走1步
快指针:每次走2步

设链表长度为n:
慢指针走过的步数:s
快指针走过的步数:f = 2s

当快指针到达终点(f = n)时:
慢指针位置:s = n/2 → 正好在中点!

数学推导:
起点 → 1步 → 2步 → 3步 → ... → n步(终点)
慢指针:0 → 1 → 2 → ... → n/2
快指针:0 → 2 → 4 → ... → n

为什么快慢指针能判断环?

复制代码
无环:快指针最终到达终点(NULL)
有环:快指针在环内绕圈,慢指针进入环后会被快指针追上

追上的原因:
快指针相对慢指针每次多走1步(相对速度为1)
环长度为k时,慢指针进入环后最多走k步就会被追上

数学证明:
慢指针在环内位置:s
快指针在环内位置:f = 2s mod k
相遇条件:f == s(mod k)
即:2s mod k == s mod k → s mod k == 0
最多k步后必定相遇。

3.5 虚拟头节点原理

为什么需要虚拟头节点?

复制代码
问题:删除链表节点时,头节点和非头节点处理逻辑不同

删除非头节点:
prev->next = cur->next;
delete cur;

删除头节点:
head = head->next;
delete old_head;

两种逻辑不同,代码需要分支判断。

虚拟头节点解决方案:
创建dummy节点,dummy->next = head;
统一处理:所有节点都有"前驱节点"
删除逻辑统一:prev->next = cur->next;

代码统一,无需特殊判断。

3.6 链表 vs 数组对比

特性 数组 链表
内存存储 连续 离散
空间效率 高(无额外开销) 低(每个节点存指针)
索引访问 O(1) O(n)
插入删除 O(n)(移动元素) O(1)(改指针)
缓存效率 高(连续内存) 低(跳跃访问)
大小管理 固定或动态扩容 动态,随时增删
适用场景 查找多、修改少 插入删除频繁

选择建议

复制代码
选择数组:
- 需要频繁索引访问
- 数据大小相对固定
- 内存空间有限
- 查找操作频繁

选择链表:
- 需要频繁插入删除
- 数据大小变化大
- 无法预知数据量
- 不需要随机访问

4. 栈与队列:受限访问的有序结构

4.1 栈的基本原理

定义:栈是只能在一端(栈顶)进行插入和删除的线性结构。

复制代码
特性:LIFO(Last In First Out)后进先出

类比:一摞盘子,只能从顶部取

栈结构:
        栈顶
        ↓
    [元素D] ← 只能从这里操作
    [元素C]
    [元素B]
    [元素A]
        ↑
        栈底

操作:
push:压栈(顶部插入)
pop:弹栈(顶部删除)
top:查看栈顶元素

核心特性

复制代码
1. 单端操作 → 只能操作栈顶
2. 后进先出 → 最新元素最先处理
3. 顺序有序 → 保持插入顺序
4. 操作简单 → 只有push/pop两种

4.2 栈的时间复杂度

操作 时间复杂度 原因
push O(1) 直接添加到栈顶
pop O(1) 直接删除栈顶
top O(1) 直接访问栈顶
查找 O(n) 需要遍历所有元素

4.3 栈与算法的关联

栈的LIFO特性 → 算法应用

复制代码
后进先出的应用:
├── 递归模拟 → 保存函数调用状态
├── 括号匹配 → 最近遇到的括号先匹配
├── 表达式计算 → 运算符优先级处理
├── DFS迭代 → 保存待访问节点
├── 树遍历 → 前序/中序/后序迭代版
└── 单调栈 → 维护单调序列

适合栈的算法

算法 利用栈的什么特性 为什么适合
括号匹配 LIFO:最近遇到的先匹配 左括号等待右括号,最近的先配对
表达式计算 LIFO:运算符优先级 高优先级运算符先出栈计算
DFS迭代版 LIFO:后进先出遍历 子节点后入栈,先出栈访问
单调栈 LIFO:维护单调序列 不满足单调性的元素弹出
二叉树遍历 LIFO:模拟递归 递归本质是栈调用

4.4 栈模拟递归原理

递归的本质是栈

复制代码
递归调用:
func(3) {
    func(2) {
        func(1) {
            // 处理
        }
        // 处理
    }
    // 处理
}

栈模拟:
push(3)
push(2)
push(1)
pop → 处理1
pop → 处理2
pop → 处理3

递归就是隐式使用栈,迭代是显式使用栈。

4.5 括号匹配原理

为什么用栈?

复制代码
括号匹配规则:
遇到左括号 → 等待匹配
遇到右括号 → 匹配最近的左括号

栈的作用:
遇到左括号 → push进栈
遇到右括号 → pop栈顶匹配
栈空或不匹配 → 错误

为什么最近?
因为括号必须成对且有序:
[()]  正确:[等待(,(等待),)匹配(,]匹配[
[(])  错误:[等待(,(等待],]不能匹配(

栈的LIFO特性正好满足"最近匹配"需求。

4.6 队列的基本原理

定义:队列是只能在一端(队尾)插入、另一端(队头)删除的线性结构。

复制代码
特性:FIFO(First In First Out)先进先出

类比:排队买票,先排队的人先买

队列结构:
队头                队尾
 ↓                   ↓
[元素A][元素B][元素C][元素D] ← 只能从队尾插入
 ↑
 只能从队头删除

操作:
enqueue:入队(队尾插入)
dequeue:出队(队头删除)
front:查看队头元素

4.7 队列与算法的关联

队列的FIFO特性 → 算法应用

复制代码
先进先出的应用:
├── BFS → 按层次遍历
├── 滑动窗口 → 维护窗口元素
├── 任务调度 → 先来先服务
├── 生产者-消费者 → 缓冲区
└── 消息队列 → 异步处理

适合队列的算法

算法 利用队列的什么特性 为什么适合
BFS FIFO:先入队的先处理 层次遍历,上一层先处理完
滑动窗口 FIFO:维护窗口顺序 窗口左端出队,右端入队
层序遍历 FIFO:按层处理 父节点先入队,子节点后入队

4.8 BFS用队列原理

为什么BFS用队列?

复制代码
BFS遍历顺序:
按层次遍历:第1层 → 第2层 → 第3层 ...

队列保证顺序:
父节点入队 → 出队时访问 → 子节点入队
同一层节点按入队顺序依次访问
子节点在父节点之后入队 → 保证层次顺序

对比DFS:
DFS用栈:后入栈的先访问(深入优先)
BFS用队列:先入队的先访问(横向优先)

4.9 双端队列(deque)

定义:两端都可以插入和删除的队列。

复制代码
操作:
push_front:前端插入
push_back:后端插入
pop_front:前端删除
pop_back:后端删除

应用:
滑动窗口:两端都可能移除元素

4.10 优先队列(堆)

定义:按优先级出队的队列,总是取出优先级最高的元素。

复制代码
实现:堆(完全二叉树)
特性:
- 插入:O(log n)
- 取出最高优先级:O(1)
- 删除最高优先级:O(log n)

应用:
- Top K问题:维护最大的K个元素
- 任务调度:优先级高的先执行
- Dijkstra算法:找最短距离节点

5. 哈希表:极速查找的映射结构

5.1 哈希表的基本原理

定义:哈希表通过哈希函数将键映射到存储位置,实现快速查找。

复制代码
核心思想:
键 → 哈希函数 → 存储位置 → 值

工作流程:
1. 计算哈希值:hash = hash_func(key)
2. 计算存储位置:index = hash % table_size
3. 存取数据:table[index] = value

示例:
key = "apple"
hash = hash_func("apple") = 5
index = 5 % 10 = 5
table[5] = "苹果"

核心特性

复制代码
1. 键值映射 → 快速定位数据位置
2. 平均O(1)查找 → 直接计算位置
3. 无序存储 → 不保持插入顺序(unordered)
4. 键唯一 → 每个键对应一个值
5. 空间换时间 → 预分配空间加速查找

5.2 哈希函数原理

哈希函数的作用:将任意键转换为固定范围的整数。

复制代码
好的哈希函数特性:
1. 均匀分布:键均匀映射到各个位置
2. 计算快速:哈希计算效率高
3. 确定性:相同键总是得到相同哈希值

常见哈希函数:
- 整数:hash = key(直接用)
- 字符串:hash = Σ(char_i * base^i) mod table_size
- 自定义:std::hash<T>

5.3 哈希碰撞处理

碰撞问题:不同键映射到同一位置。

复制代码
原因:
table_size有限,键无限多
必然存在:key1 ≠ key2,但 hash(key1) == hash(key2)

解决方法:
├── 链地址法(拉链法)
│   每个位置存储链表,碰撞元素追加到链表
│   C++ unordered_map/set 使用此方法
│
├── 开放地址法
│   碰撞时寻找下一个空位置
│   线性探测:+1, +2, +3...
│   二次探测:+1², +2², +3²...
│
└── 再哈希法
    使用多个哈希函数

链地址法详解

复制代码
哈希表结构:
table[0] → nullptr
table[1] → [key1, value1] → [key2, value2] → nullptr
table[2] → [key3, value3] → nullptr
...

查找流程:
hash = hash_func(key)
index = hash % table_size
遍历 table[index] 的链表查找 key

时间复杂度:
理想(无碰撞):O(1)
最坏(全部碰撞):O(n)
平均(良好哈希):O(1)

5.4 哈希表的时间复杂度

操作 平均 最坏 原因
插入 O(1) O(n) 最坏全部碰撞到同一位置
删除 O(1) O(n) 同上
查找 O(1) O(n) 同上

5.5 哈希表与算法的关联

哈希表的映射特性 → 算法应用

复制代码
键值映射的应用:
├── 快速查找 → 判断元素是否存在
├── 计数 → 统计元素出现次数
├── 去重 → 判断是否重复
├── 映射 → 存储键值关系
└── 缓存 → 快速数据访问

适合哈希表的算法

算法 利用哈希表的什么特性 为什么适合
两数之和 O(1)查找 查找目标值-当前值
判断重复 键唯一性 重复键碰撞
字符计数 键值存储 记录每个字符出现次数
交集/并集 键唯一性 快速判断是否存在
最长无重复子串 键唯一性 判断字符是否在窗口内

5.6 哈希表应用场景

复制代码
使用哈希表场景:
├── 需要快速查找(O(1))
├── 需要判断元素是否存在
├── 需要统计出现次数
├── 需要去重
├── 需要存储映射关系
└── 键的类型支持哈希

不适合哈希表场景:
├── 需要保持有序
├── 需要范围查找
├── 需要遍历所有元素
├── 键类型不支持哈希(自定义类型需定义哈希函数)
└── 内存受限(哈希表空间开销大)

5.7 哈希表 vs 有序容器

特性 哈希表(unordered) 有序容器(ordered)
查找 O(1) 平均 O(log n)
插入 O(1) 平均 O(log n)
删除 O(1) 平均 O(log n)
有序遍历 不支持 支持
范围查找 不支持 支持
空间效率 较低 较高
实现复杂度 中等 高(平衡树)

选择建议

复制代码
选择哈希表:
- 只需要单点查找
- 不需要有序遍历
- 查找操作频繁
- 键类型可哈希

选择有序容器:
- 需要有序遍历
- 需要范围查找
- 需要找前驱后继
- 需要最值查找

6. 树:层次组织的递归结构

6.1 树的基本原理

定义:树是由节点组成的层次结构,每个节点有一个父节点(除根节点)和多个子节点。

复制代码
树的结构:
            根节点(A)
           /      \
        子节点(B)  子节点(C)
        /    \         \
      叶节点(D) 叶节点(E) 叶节点(F)

术语:
- 根节点:没有父节点
- 叶节点:没有子节点
- 层次:根在第1层,子节点在第2层...
- 深度:从根到某节点的路径长度
- 高度:从某节点到叶节点的最长路径长度
- 子树:以某节点为根的树

核心特性

复制代码
1. 层次结构 → 父子关系
2. 递归定义 → 子树也是树
3. 无环 → 不能回到祖先
4. 根唯一 → 只有一个根节点
5. 节点有限 → 有n个节点的树有n-1条边

6.2 二叉树

定义:每个节点最多有两个子节点的树。

复制代码
二叉树结构:
        A
       / \
      B   C
     / \   \
    D   E   F

节点表示:
struct TreeNode {
    int val;
    TreeNode* left;   // 左子节点
    TreeNode* right;  // 右子节点
};

特殊类型

复制代码
满二叉树:
每个节点要么是叶节点,要么有两个子节点
        A
       / \
      B   C
     / \ / \
    D  E F  G

完全二叉树:
前k-1层满,第k层从左到右填充
        A
       / \
      B   C
     / \  /
    D  E F

二叉搜索树(BST):
左子树值 < 根值 < 右子树值
        5
       / \
      3   7
     / \ / \
    2  4 6  8

6.3 树的存储方式

链式存储

cpp 复制代码
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
};

// 空间开销:每个节点存2个指针
// 适合:任意形状的树

数组存储(完全二叉树)

复制代码
索引关系:
父节点索引:parent = (i - 1) / 2
左子节点索引:left = 2 * i + 1
右子节点索引:right = 2 * i + 2

示例:
        A(0)
       / \
    B(1)  C(2)
   / \    /
 D(3)E(4)F(5)

数组:[A, B, C, D, E, F]

优点:
- 空间紧凑(无指针开销)
- 索引访问快(堆排序用此存储)

缺点:
- 只适合完全二叉树
- 需要预留空间

6.4 树与算法的关联

树的层次性 → 算法应用

复制代码
层次结构的应用:
├── 递归遍历 → 天然适合递归
├── DFS → 深度探索
├── BFS → 层次遍历
├── 分治 → 子树独立处理
└── 动态规划 → 树形DP

适合树的算法

算法 利用树的什么特性 为什么适合
DFS 层次结构 深入探索子树
BFS 层次结构 按层遍历
递归遍历 递归定义 子树也是树
分治 子树独立性 子树可独立处理
树形DP 子树独立性 子树结果可传递

6.5 树遍历原理

前序遍历:根 → 左 → 右

复制代码
递归:
void preorder(TreeNode* root) {
    if (!root) return;
    visit(root);           // 先访问根
    preorder(root->left);  // 递归左子树
    preorder(root->right); // 递归右子树
}

迭代(栈):
void preorder(TreeNode* root) {
    stack<TreeNode*> stk;
    stk.push(root);
    while (!stk.empty()) {
        TreeNode* node = stk.top(); stk.pop();
        visit(node);
        if (node->right) stk.push(node->right);  // 右先入栈
        if (node->left) stk.push(node->left);    // 左后入栈(先出栈)
    }
}

应用:复制树、前缀表达式

中序遍历:左 → 根 → 右

复制代码
递归:
void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root->left);   // 先递归左子树
    visit(root);           // 访问根
    inorder(root->right);  // 递归右子树
}

迭代(栈):
void inorder(TreeNode* root) {
    stack<TreeNode*> stk;
    TreeNode* cur = root;
    while (cur || !stk.empty()) {
        while (cur) {           // 左子树全部入栈
            stk.push(cur);
            cur = cur->left;
        }
        cur = stk.top(); stk.pop();
        visit(cur);             // 访问
        cur = cur->right;       // 转右子树
    }
}

应用:BST排序输出、中缀表达式

后序遍历:左 → 右 → 根

复制代码
递归:
void postorder(TreeNode* root) {
    if (!root) return;
    postorder(root->left);  // 递归左子树
    postorder(root->right); // 递归右子树
    visit(root);            // 最后访问根
}

迭代(栈):
方法:前序(根→左→右)反转 = 后序(左→右→根)
1. 前序遍历改为:根→右→左
2. 反转结果得到:左→右→根

应用:计算树高度、删除树、后缀表达式

层序遍历:按层从左到右

复制代码
迭代(队列):
void levelorder(TreeNode* root) {
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        int size = q.size();  // 当前层节点数
        for (int i = 0; i < size; i++) {
            TreeNode* node = q.front(); q.pop();
            visit(node);
            if (node->left) q.push(node->left);
            if (node->right) q.push(node->right);
        }
    }
}

应用:按层处理、找某层节点

6.6 二叉搜索树(BST)

特性

复制代码
1. 左子树所有值 < 根值
2. 右子树所有值 > 根值
3. 左右子树也是BST
4. 中序遍历 = 升序序列

BST查找原理

复制代码
查找流程:
target < root → 在左子树查找
target > root → 在右子树查找
target == root → 找到

时间复杂度:
平均:O(log n)(平衡树)
最坏:O(n)(退化为链表)

为什么高效?
每次比较排除一半子树(类似二分查找)

BST插入原理

复制代码
插入流程:
1. 查找插入位置(类似查找)
2. 找到空位置后创建节点
3. 连接到父节点

时间复杂度:O(log n) 平均

BST删除原理

复制代码
删除流程:
1. 叶节点:直接删除
2. 只有一个子节点:用子节点替换
3. 有两个子节点:
   - 找右子树最小节点(或左子树最大节点)
   - 用该节点替换被删除节点
   - 删除替换节点原位置

为什么用右子树最小?
右子树最小 > 左子树所有值
右子树最小 < 右子树其他值
满足BST性质

7. 堆:优先级管理的特殊树

7.1 堆的基本原理

定义:堆是满足堆性质的完全二叉树,通常用数组存储。

复制代码
堆性质:
大顶堆:父节点 ≥ 子节点
小顶堆:父节点 ≤ 子节点

大顶堆示例:
        9(0)
       /    \
     8(1)    7(2)
    / \      /
   5(3)6(4) 4(5)

数组存储:[9, 8, 7, 5, 6, 4]

索引关系:
父节点:(i-1)/2
左子节点:2*i+1
右子节点:2*i+2

核心特性

复制代码
1. 完全二叉树 → 数组存储高效
2. 堆顶是最大/最小 → 优先队列
3. 部分有序 → 不是完全排序
4. 操作高效 → 插入删除O(log n)

7.2 堆的时间复杂度

操作 时间复杂度 原因
取堆顶 O(1) 直接访问根节点
插入 O(log n) 向上调整
删除堆顶 O(log n) 向下调整
堆化 O(n) 从底向上构建

7.3 堆调整原理

向上调整(插入)

复制代码
插入流程:
1. 新元素加到数组末尾
2. 与父节点比较
3. 不满足堆性质则交换
4. 重复直到满足或到达根节点

示例(大顶堆):
插入3到[9,8,7,5,6]
数组变为[9,8,7,5,6,3]
父节点(5):3<5,满足,停止

插入10到[9,8,7,5,6]
数组变为[9,8,7,5,6,10]
父节点(5):10>5,交换 → [9,8,7,10,6,5]
父节点(8):10>8,交换 → [9,10,7,8,6,5]
父节点(9):10>9,交换 → [10,9,7,8,6,5]
满足大顶堆,停止

向下调整(删除)

复制代码
删除流程:
1. 移除堆顶元素
2. 将最后一个元素移到堆顶
3. 与子节点比较
4. 不满足堆性质则与最大子节点交换
5. 重复直到满足或到达叶节点

示例(大顶堆):
删除[10,9,7,8,6,5]的堆顶
移除10,末尾5移到堆顶 → [5,9,7,8,6]
子节点(9,7):5<9,与9交换 → [9,5,7,8,6]
子节点(8,6):5<8,与8交换 → [9,8,7,5,6]
满足大顶堆,停止

7.4 堆与算法的关联

堆的优先级特性 → 算法应用

复制代码
堆顶最值的应用:
├── Top K问题 → 维护最大/最小的K个元素
├── 优先队列 → 高优先级先处理
├── 堆排序 → 利用堆顶最值排序
├── 合并有序数组 → 维护最小元素
└── Dijkstra → 最短距离节点优先

适合堆的算法

算法 利用堆的什么特性 为什么适合
Top K 堆顶最值 维护大小为K的堆,堆顶是第K大/小
堆排序 堆顶最值 每次取出堆顶得到有序序列
合并K数组 小顶堆堆顶最小 每次取最小元素合并
优先队列 堆顶优先级最高 高优先级先处理

7.5 Top K问题原理

为什么用堆?

复制代码
问题:找最大的K个元素

方法1:排序
排序后取前K个:O(n log n)

方法2:小顶堆
维护大小为K的小顶堆:
- 堆顶是当前第K大的元素
- 新元素大于堆顶:弹出堆顶,插入新元素
- 新元素小于堆顶:跳过

时间复杂度:O(n log K)

为什么更快?
K通常远小于n
log K < log n

原理详解

复制代码
小顶堆维护最大的K个:
堆顶 = 当前K个中最小的 = 当前第K大

新元素处理:
元素 > 堆顶 → 元素比当前第K大更大 → 替换堆顶
元素 < 堆顶 → 元素比当前第K大更小 → 跳过

结果:
堆中保存最大的K个元素
堆顶是第K大的元素

8. 图:复杂关系的网络结构

8.1 图的基本原理

定义:图是由顶点和边组成的结构,表示元素之间的关系。

复制代码
图的结构:
顶点(V):节点
边(E):连接关系

术语:
- 无向图:边无方向
- 有向图:边有方向
- 加权图:边有权值
- 度:与顶点相连的边数
- 路径:从一个顶点到另一个顶点的边序列
- 连通:两顶点之间存在路径

核心特性

复制代码
1. 任意连接 → 多对多关系
2. 复杂结构 → 无固定层次
3. 网络表示 → 关系建模
4. 可遍历 → DFS/BFS

8.2 图的存储方式

邻接矩阵

复制代码
矩阵表示:matrix[i][j] = 1表示有边

示例:
    A B C D
A [ 0 1 1 0 ]
B [ 1 0 0 1 ]
C [ 1 0 0 1 ]
D [ 0 1 1 0 ]

优点:
- 判断是否有边:O(1)
- 适合稠密图

缺点:
- 空间:O(V²)
- 不适合稀疏图

邻接表

复制代码
链表/数组表示:每个顶点存储相连顶点列表

示例:
A → [B, C]
B → [A, D]
C → [A, D]
D → [B, C]

优点:
- 空间:O(V+E)
- 适合稀疏图

缺点:
- 判断是否有边:O(V)遍历列表

8.3 图的时间复杂度

操作 邻接矩阵 邻接表
判断是否有边 O(1) O(V)
找所有邻居 O(V) O(degree)
空间复杂度 O(V²) O(V+E)
DFS/BFS O(V²) O(V+E)

8.4 图与算法的关联

图的网络特性 → 算法应用

复制代码
网络结构的应用:
├── DFS → 深度探索路径
├── BFS → 找最短路径(无权图)
├── 最短路径 → Dijkstra/Floyd
├── 拓扑排序 → 任务依赖关系
├── 连通性 → 判断是否连通
└── 最小生成树 → Kruskal/Prim

适合图的算法

算法 利用图的什么特性 为什么适合
DFS 网络连接 深入探索一条路径
BFS 网络连接 找无权图最短路径
Dijkstra 加权边 找加权最短路径
拓扑排序 有向边 处理依赖关系
最小生成树 连通性 找最小成本连接

8.5 DFS与BFS原理

DFS(深度优先)

复制代码
原理:一条路走到底,走不通再回头

流程:
1. 从起点开始
2. 选一个方向深入
3. 走到底或走不通
4. 回退一步,选另一个方向
5. 重复直到遍历完

实现:栈或递归
void DFS(Graph& g, int v) {
    visited[v] = true;
    for (int neighbor : g.neighbors(v)) {
        if (!visited[neighbor]) {
            DFS(g, neighbor);
        }
    }
}

应用:
- 判断连通性
- 拓扑排序
- 找所有路径

BFS(广度优先)

复制代码
原理:一层一层遍历,先近后远

流程:
1. 从起点开始
2. 访问所有相邻节点(第1层)
3. 访问第1层的相邻节点(第2层)
4. 重复直到遍历完

实现:队列
void BFS(Graph& g, int start) {
    queue<int> q;
    q.push(start);
    visited[start] = true;
    
    while (!q.empty()) {
        int v = q.front(); q.pop();
        for (int neighbor : g.neighbors(v)) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                q.push(neighbor);
            }
        }
    }
}

应用:
- 无权图最短路径
- 层次遍历
- 找最近节点

9. 数据结构选择策略

9.1 按操作频率选择

复制代码
查找频率高:
├── 单点查找 → 哈希表(O(1))
├── 范围查找 → 平衡树(O(log n))
├── 有序查找 → 二分查找(有序数组)
└── 频繁查找特定值 → 哈希表

插入删除频率高:
├── 头部操作 → 链表(O(1))
├── 中间操作 → 链表(O(1)找到位置后)
├── 末尾操作 → 数组(O(1))
└── 优先级操作 → 堆(O(log n))

遍历频率高:
├── 顺序遍历 → 数组(缓存友好)
├── 有序遍历 → 平衡树
└── 层次遍历 → BFS用队列

9.2 按数据特征选择

复制代码
数据有序:
├── 保持有序 → 平衡树
├── 不需保持 → 哈希表(查找更快)
└── 只需查找 → 有序数组+二分

数据唯一性:
├── 需要去重 → set
├── 需要计数 → map
└── 需要键值 → map

数据大小变化:
├── 大小固定 → 数组
├── 大小动态 → vector/list
└── 大小未知 → 链表

数据有层次:
├── 父子关系 → 树
├── 多对多 → 图
└── 优先级 → 堆

9.3 按时间要求选择

复制代码
要求O(1):
├── 查找 → 哈希表
├── 头部操作 → 链表
├── 末尾操作 → 数组
└── 取最值 → 堆顶(堆)

要求O(log n):
├── 查找 → 平衡树/BST
├── 插入删除 → 平衡树/堆
├── 范围查找 → 平衡树
└── 有序数组操作 → 二分+移动

要求O(n):
├── 链表查找
├── 数组中间插入删除
├── 哈希表最坏情况
└── 图遍历

9.4 按空间限制选择

复制代码
空间紧张:
├── 优先数组(无额外开销)
├── 避免哈希表(预分配空间)
├── 避免链表(指针开销)
└── 避免平衡树(节点开销)

空间充足:
├── 可用哈希表(查找快)
├── 可用平衡树(有序)
└── 可用堆(优先队列)

10. 数据结构与算法映射表

10.1 数据结构 → 支持的算法

数据结构 核心特性 支持的算法 原理关联
数组 连续存储 二分查找、双指针、滑动窗口 索引访问、跳跃访问
链表 离散存储 快慢指针、反转、合并 指针操作灵活
LIFO 括号匹配、DFS迭代、单调栈 最近先处理
队列 FIFO BFS、滑动窗口、层序遍历 先进先处理
哈希表 键值映射 快速查找、计数、去重 O(1)定位
层次结构 DFS、BFS、递归遍历、分治 子树独立性
堆顶最值 Top K、堆排序、优先队列 优先级管理
网络连接 DFS、BFS、最短路径、拓扑排序 关系遍历

10.2 算法 → 需要的数据结构

算法 核心需求 推荐数据结构 选择原因
二分查找 有序、索引访问 有序数组 可以跳跃访问
双指针 索引访问 数组 两个位置协同
滑动窗口 索引访问、顺序 数组、队列 动态调整边界
快慢指针 指针操作 链表 控制步数找位置
DFS 后进先出 栈、递归 最近访问优先
BFS 先进先出 队列 层次顺序
回溯 保存状态、回退 递归栈 状态保存恢复
动态规划 状态存储 数组 存储dp值
哈希优化 快速查找 哈希表 O(1)查找
单调栈 维护单调性 不满足则弹出
Top K 维护最值 堆顶是第K大/小
Dijkstra 最短距离优先 取最小距离

10.3 问题类型 → 数据结构+算法组合

问题类型 数据结构 算法 组合原理
有序查找 有序数组 二分查找 连续存储支持跳跃
子数组问题 数组 滑动窗口 索引调整边界
链表操作 链表 快慢指针 指针灵活操作
括号匹配 LIFO匹配 最近左括号先匹配
层次遍历 队列 BFS 先入队的先处理
判断重复 哈希表 快速查找 键唯一性
树遍历 树+栈/队列 DFS/BFS 层次结构遍历
最短路径 图+堆 Dijkstra 最小距离优先
Top K 维护K个最大 堆顶是第K大

10.4 数据结构转换技巧

数组 → 哈希表优化

复制代码
原问题:查找数组中是否存在某值
暴力:O(n)遍历
优化:哈希表预处理,O(1)查找

适用:查找频率高

数组 → 双指针优化

复制代码
原问题:两数之和、三数之和
暴力:O(n²)嵌套循环
优化:双指针O(n)或O(n log n)

适用:有序或可通过排序变有序

链表 → 数组转换

复制代码
原问题:链表随机访问
链表:O(n)遍历
优化:转为数组O(1)访问

适用:需要多次随机访问
代价:转换成本O(n)

递归 → 栈转换

复制代码
原问题:DFS遍历
递归:隐式栈
优化:显式栈(避免栈溢出)

适用:深度大、怕栈溢出

总结

数据结构是算法的载体,选择正确的数据结构是解题的关键:

复制代码
核心原则:
1. 数据规模决定复杂度要求
2. 操作频率决定数据结构选择
3. 数据特征决定算法方向
4. 时间空间权衡

记住

  • 数组支持跳跃 → 二分、双指针、滑动窗口
  • 链表指针灵活 → 快慢指针、反转、合并
  • 栈后进先出 → 括号匹配、DFS、单调栈
  • 队列先进先出 → BFS、滑动窗口
  • 哈希表O(1)查找 → 快速查找、计数、去重
  • 树层次结构 → DFS、BFS、递归
  • 堆堆顶最值 → Top K、优先队列
  • 图网络结构 → DFS、BFS、最短路径

最后更新:2024 年

相关推荐
sleven fung1 小时前
llama-cpp-python 本地部署入门
开发语言·python·算法·llama
头歌实践平台1 小时前
C++面向对象 - 运算符重载的应用
开发语言·c++·算法
晚风予卿云月1 小时前
《二分答案》算法练习
数据结构·c++·算法·二分·竞赛·算法随笔
普马萨特1 小时前
搜索核心算法:从召回到排序
算法·搜索引擎
sheeta19981 小时前
LeetCode 每日一题笔记 日期:2026.05.31 题目:2126. 摧毁小行星
笔记·算法·leetcode
INGNIGHT1 小时前
984.不含 AAA 或 BBB 的字符串(贪心)
开发语言·算法·leetcode
飞天狗1112 小时前
2025第十六届蓝桥杯c/c++B组国赛题解
c语言·c++·算法·蓝桥杯
超梦dasgg2 小时前
Tarjan算法解 强连通分量 & 循环依赖
算法·深度优先·图论
散峰而望2 小时前
【算法练习】算法练习精选:从 Phone numbers 到 Decrease,覆盖字符串、模拟、图论思维题
数据结构·c++·算法·贪心算法·github·动态规划·图论