193 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			193 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
# 先序遍历原理:从树结构到实际应用的深度解析
 | 
						||
先序遍历(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=2,rgt=10 > 技术部rgt=7)。
 | 
						||
- **子孙范围**:一个节点的所有子孙节点,其lft和rgt均在该节点的lft和rgt之间(如技术部的子孙节点前端组、后端组,lft均在2-7之间)。
 | 
						||
- **叶子节点特征**:叶子节点无子孙,因此rgt = lft + 1(如前端组lft=3,rgt=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;
 | 
						||
```
 | 
						||
 | 
						||
```
 | 
						||
直接存储 层级
 | 
						||
```
 | 
						||
 |