Files
article/java/先序遍历原理.md
2025-08-15 17:26:45 +08:00

193 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 先序遍历原理:从树结构到实际应用的深度解析
先序遍历Pre-order Traversal是树结构中最基础、最常用的遍历方式之一其核心思想是“先访问根节点再递归遍历左子树最后递归遍历右子树”。在部门树形结构等场景中先序遍历的衍生设计如左值右值标记法能极大提升数据操作效率。以下从基础概念到实际应用展开详细解说。
## 一、先序遍历的基础定义与核心规则
### 1. 树结构的基本概念
在讲解先序遍历前,需明确树的核心术语:
- **节点**:树中的每个元素(如部门树形结构中的“部门”)。
- **根节点**:树的顶层节点(无父节点,如公司总部)。
- **子节点**:被父节点直接包含的节点(如“技术部”是“研发中心”的子节点)。
- **叶子节点**:无任何子节点的节点(如“前端开发组”)。
- **路径**:从根节点到某一节点的所有节点组成的序列。
### 2. 先序遍历的核心逻辑
先序遍历的遍历顺序可概括为 **“根 → 左 → 右”**,即:
1. 首先访问当前节点(记录或处理节点信息);
2. 递归地对当前节点的**左子树**执行先序遍历;
3. 递归地对当前节点的**右子树**执行先序遍历。
#### 示例:二叉树的先序遍历
对如下二叉树A为根节点B、C为子节点
```
A
/ \
B C
/ \
D E
```
先序遍历顺序为:
**A → B → D → E → C**
- 访问根节点A → 遍历左子树B → 访问B → 遍历B的左子树D → 访问D无左右子树→ 遍历B的右子树E → 访问E → 左子树遍历完成 → 遍历根节点A的右子树C → 访问C。
## 二、多叉树的先序遍历(部门树场景适配)
部门树等实际场景中,树通常是**多叉树**(一个父节点可包含多个子节点),先序遍历规则调整为:
**“根 → 子树1 → 子树2 → ... → 子树n”**,即访问根节点后,按顺序递归遍历每个子树。
### 1. 多叉树先序遍历示例
以某公司部门树为例(根节点为“总公司”,下设“技术部”“市场部”,技术部下设“前端组”“后端组”):
```
总公司(根节点)
/ \
技术部 市场部
/ \
前端组 后端组
```
先序遍历顺序为:
**总公司 → 技术部 → 前端组 → 后端组 → 市场部**
- 访问总公司 → 遍历第一个子树“技术部” → 访问技术部 → 遍历技术部第一个子树“前端组” → 访问前端组(无子节点)→ 遍历技术部第二个子树“后端组” → 访问后端组 → 技术部子树遍历完成 → 遍历总公司第二个子树“市场部” → 访问市场部。
## 三、先序遍历在部门树存储中的核心应用左值lft与右值rgt标记
在部门树形结构存储优化中,先序遍历的核心价值在于通过**“进入标记”和“离开标记”** 记录节点的层级关系即左值lft和右值rgt的设计。
### 1. 标记规则
遍历过程中,每个节点会被标记两个值:
- **左值lft**首次访问节点进入节点时记录的序号从1开始递增
- **右值rgt**:遍历完该节点的**所有子节点**后(离开节点)记录的序号。
### 2. 标记示例(基于上述部门树)
| 遍历步骤 | 操作 | 节点 | lft值 | rgt值 |
|----------------|---------------|------------|-------|-------|
| 1 | 进入根节点 | 总公司 | 1 | - |
| 2 | 进入子节点 | 技术部 | 2 | - |
| 3 | 进入子节点 | 前端组 | 3 | - |
| 4 | 离开前端组(无子节点) | 前端组 | - | 4 |
| 5 | 进入子节点 | 后端组 | 5 | - |
| 6 | 离开后端组(无子节点) | 后端组 | - | 6 |
| 7 | 离开技术部(所有子节点遍历完成) | 技术部 | - | 7 |
| 8 | 进入子节点 | 市场部 | 8 | - |
| 9 | 离开市场部(无子节点) | 市场部 | - | 9 |
| 10 | 离开总公司(所有子节点遍历完成) | 总公司 | - | 10 |
最终标记结果:
| 节点 | lft | rgt |
|------------|-----|-----|
| 总公司 | 1 | 10 |
| 技术部 | 2 | 7 |
| 前端组 | 3 | 4 |
| 后端组 | 5 | 6 |
| 市场部 | 8 | 9 |
### 3. 核心规律(标记的价值所在)
通过先序遍历标记的lft和rgt形成以下关键规律支撑高效查询
- **父子关系**父节点的lft < 所有子节点的lft且父节点的rgt > 所有子节点的rgt如总公司lft=1 < 技术部lft=2rgt=10 > 技术部rgt=7
- **子孙范围**一个节点的所有子孙节点其lft和rgt均在该节点的lft和rgt之间如技术部的子孙节点前端组、后端组lft均在2-7之间
- **叶子节点特征**叶子节点无子孙因此rgt = lft + 1如前端组lft=3rgt=4=3+1
- **子孙数量计算**:某节点的子孙总数 = (rgt - lft - 1) / 2如技术部子孙数=(7-2-1)/2=2即前端组和后端组
## 四、先序遍历的优缺点与适用场景
### 1. 优势
- **查询效率极高**基于lft和rgt的规律无需递归即可快速查询子孙节点、计算数量、判断叶子节点等时间复杂度低。
- **关系表达直观**通过数值范围直接体现层级关系避免传统parent_id的链式依赖。
### 2. 局限性
- **维护成本高**新增或删除节点时需批量更新后续节点的lft和rgt如插入一个节点需给所有lft大于插入位置的节点+2数据量大时操作耗时。
- **初始化复杂**现有树形结构迁移时需全量遍历计算lft和rgt层级过深或数据量大时成本高。
### 3. 适用场景
最适合**查询频繁、结构稳定、数据量中等**的树形场景,如:
- 企业组织架构(部门层级变动少,查询需求多);
- 权限菜单树(菜单结构稳定,需频繁查询子菜单);
- 分类目录树(如电商商品分类,查询远多于增删)。
## 五、总结
先序遍历作为树结构的基础遍历方式其核心是“先根后子”的访问顺序。在部门树等场景中通过衍生的lft和rgt标记法将树形关系转化为数值范围关系实现了查询效率的质的提升。理解先序遍历的原理不仅能掌握这一优化方案的核心逻辑更能深入理解树形数据结构在计算机存储与操作中的设计思想。实际应用中需结合业务的查询与写入频率、数据量大小权衡其优势与维护成本避免盲目套用。
## 六、附
查出所有子孙部门
```sql
SET @lft := 9;
SET @rgt := 18;
SELECT * FROM department WHERE lft BETWEEN @lft AND @rgt ORDER BY lft ASC;
/*例子中用BETWEEN将被查部门本身也查了出来。实际中可以用大于小于*/
```
查询子孙部门总数
```
总数 = (右值 - 左值 - 1) / 2
```
新增部门
```sql
SET @lft := 7;/*新部门的左值*/
SET @rgt := 8;/*新部门的左值*/
SET @level := 5;/*新部门的层级*/
begin;
/*将插入的后续边缘的节点左右数+2*/
UPDATE department SET lft=lft+2 WHERE lft > @lft;
UPDATE department SET rgt=rgt+2 WHERE rgt >= @lft;
/*插入数据*/
INSERT INTO department(name,lft,rgt,level) VALUES('新部门',@lft,@rgt,level);
/*新增影响行数为0时必须回滚*/
commit;
/*rollback;*/
```
删除
```sql
SET @lft := 7;/*要删除的节点左值*/
SET @rgt := 8;/*要删除的节点右值*/
begin;
UPDATE department SET lft=lft-2 WHERE lft > @lft;
UPDATE department SET rgt=rgt-2 WHERE rgt > @lft;
/*删除节点*/
DELETE FROM department WHERE lft=@lft AND rgt=@rgt;
/*删除影响行数为0时必须回滚*/
commit;
```
直接子节点
```sql
SET @level := 2;/*总经理的level*/
SET @lft := 2;/*总经理的左值*/
SET @rgt := 19;/*总经理的右值*/
SELECT * FROM department WHERE lft > @lft AND rgt < @rgt AND level = @level+1;
```
祖宗链路
```sql
SET @lft := 3;/*产品部左值*/
SET @rgt := 8;/*产品部右值*/
SELECT * FROM department WHERE lft < @lft AND rgt > @rgt ORDER BY lft ASC;
```
计算层级
```sql
-- 统计祖链节点数量再加1即为当前节点层级
SELECT COUNT(*) + 1 AS level
FROM department
WHERE lft < 3 AND rgt > 4;
```
```
直接存储 层级
```