
看完上一篇二叉树非递归深度遍历,相信大家已经搞定了前序、中序、后序的高频手撕代码。
但二叉树的面试和刷题考点,远不止深度遍历!
除了「一条路走到黑」的深度遍历,还有算法笔试必考 的层序遍历(广度优先遍历) ;同时还有解决二叉树空间冗余、遍历低效的进阶结构------线索二叉树。
很多同学刷题只会普通遍历,不懂层序、不会线索二叉树,遇到二叉树进阶题型直接丢分。
今天这篇文章,一次性讲透两大进阶核心知识点,零基础能看懂,面试能直接用!
一、前置区分:深度遍历 vs 层序遍历
我们之前学的前、中、后序遍历,都属于深度优先遍历(DFS) :优先往树的深层走,走到尽头再回溯,核心依赖栈实现。
而今天的层序遍历(BFS,广度优先遍历),逻辑完全相反:
从上到下、从左到右,一层一层遍历整棵树,不深挖子树,先遍历完当前层所有节点,再遍历下一层。
它的核心工具也彻底改变:放弃栈,只用队列(Queue),利用队列「先进先出」的特性,完美适配层级遍历逻辑。
二、二叉树层序遍历:笔试刷题天花板考点
1. 核心原理
队列先进先出,先存入当前层所有节点,依次取出节点访问,同时将该节点的左、右子节点依次入队,循环往复,直到队列为空,即可实现层级有序遍历。
2. 详细执行步骤
-
判空:如果根节点为空,直接结束遍历;
-
初始化队列,将根节点入队;
-
循环判断队列是否为空,每一轮循环处理一整层节点;
-
获取当前队列长度,即为当前层的节点个数;
-
依次弹出队首节点,访问节点值,同时将该节点的左、右子节点(非空)入队;
-
当前层遍历完成后,进入下一层循环,直至队列清空。
3. C++ 完整可运行代码
cpp
#include <iostream>
#include <queue>
#include <vector>
using namespace std;
// 二叉树节点定义
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// 二叉树层序遍历(广度优先)
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> res;
if (root == nullptr) return res;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
// 当前层节点数量
int levelSize = q.size();
vector<int> levelData;
// 遍历当前层所有节点
for (int i = 0; i < levelSize; i++) {
TreeNode* cur = q.front();
q.pop();
levelData.push_back(cur->val);
// 左右子节点入队,先左后右保证顺序
if (cur->left != nullptr) q.push(cur->left);
if (cur->right != nullptr) q.push(cur->right);
}
res.push_back(levelData);
}
return res;
}
// 测试主函数
int main() {
// 构建测试二叉树
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
vector<vector<int>> ans = levelOrder(root);
// 打印层序遍历结果
for (auto &level : ans) {
for (int num : level) {
cout << num << " ";
}
cout << endl;
}
return 0;
}
4. 核心总结与适用场景
核心口诀:队列存层、逐层出队、子节点入队、层层迭代。
高频适用场景:二叉树广度搜索、二叉树最大/最小深度、二叉树每层平均值、锯齿层序遍历等刷题经典题型,全部基于该核心逻辑改造。
相比于深度遍历,层序遍历更适合层级统计、最短路径类问题,是算法笔试的重中之重。
三、普通二叉树的致命缺陷:为什么需要线索二叉树?
学完四种基础遍历(前、中、后、层序),很多同学会以为二叉树已经学透了,但普通二叉树存在两个严重短板:
1. 空间严重冗余
一棵 n 个节点的二叉树,必然有 n+1 个空指针域,大量空指针浪费内存空间,数据量越大,浪费越严重。
2. 遍历必须依赖栈/递归,效率有限
普通二叉树想要遍历,要么递归(有栈溢出风险),要么手动栈(代码繁琐),且无法直接找到某个节点的前驱、后继节点,每次查询都需要重新遍历。
为了解决这两个问题,线索二叉树 应运而生:利用闲置的空指针,记录节点前驱、后继,不用栈、不用递归,即可高效遍历二叉树。
四、线索二叉树:原理、定义与核心规则
1. 核心概念
线索二叉树,本质是优化后的二叉树:将节点的空左指针指向遍历前驱节点,空右指针指向遍历后继节点,这些指针被称为「线索」。
为了区分普通指针和线索指针,需要给节点新增两个标记位:
-
ltag:0 表示左指针指向左孩子,1 表示左指针是前驱线索
-
rtag:0 表示右指针指向右孩子,1 表示右指针是后继线索
2. 全新节点结构定义(C++)
cpp
// 线索二叉树节点定义
struct ThreadTreeNode {
int val;
ThreadTreeNode* left;
ThreadTreeNode* right;
// 标记位:0-孩子指针,1-线索指针
int ltag, rtag;
ThreadTreeNode(int x) : val(x), left(nullptr), right(nullptr), ltag(0), rtag(0) {}
};
3. 核心规则(以中序线索二叉树为例)
我们最常用、面试最常考的是中序线索二叉树,规则如下:
-
节点左孩子为空:left 指针指向中序遍历的前驱节点,ltag=1
-
节点右孩子为空:right 指针指向中序遍历的后继节点,rtag=1
-
有孩子的节点,指针正常指向左右孩子,标记位为 0
五、中序线索二叉树:构建 + 遍历完整实现
1. 构建核心思路
基于中序遍历逻辑,递归遍历二叉树,用全局前驱节点记录上一个访问节点,每当遇到空指针,就建立前驱、后继线索关联。
2. 完整C++代码实现
cpp
#include <iostream>
using namespace std;
// 线索二叉树节点定义
struct ThreadTreeNode {
int val;
ThreadTreeNode* left;
ThreadTreeNode* right;
int ltag, rtag;
ThreadTreeNode(int x) : val(x), left(nullptr), right(nullptr), ltag(0), rtag(0) {}
};
// 全局前驱节点,记录上一个访问的节点
ThreadTreeNode* pre = nullptr;
// 中序遍历构建线索二叉树
void createThreadTree(ThreadTreeNode* cur) {
if (cur == nullptr) return;
// 1. 递归遍历左子树
createThreadTree(cur->left);
// 2. 处理当前节点
// 左孩子为空,建立前驱线索
if (cur->left == nullptr) {
cur->ltag = 1;
cur->left = pre;
}
// 上一个节点右孩子为空,建立后继线索
if (pre != nullptr && pre->right == nullptr) {
pre->rtag = 1;
pre->right = cur;
}
// 更新前驱节点为当前节点
pre = cur;
// 3. 递归遍历右子树
createThreadTree(cur->right);
}
// 线索二叉树中序遍历(无栈、无递归)
void threadTreeTraverse(ThreadTreeNode* root) {
ThreadTreeNode* cur = root;
// 循环遍历所有节点
while (cur != nullptr) {
// 1. 找到最左侧起始节点
while (cur->ltag == 0) {
cur = cur->left;
}
// 访问当前节点
cout << cur->val << " ";
// 2. 沿着后继线索遍历
while (cur->rtag == 1 && cur->right != nullptr) {
cur = cur->right;
cout << cur->val << " ";
}
// 转向右子树
cur = cur->right;
}
}
// 测试主函数
int main() {
// 构建测试二叉树
ThreadTreeNode* root = new ThreadTreeNode(1);
root->left = new ThreadTreeNode(2);
root->right = new ThreadTreeNode(3);
root->left->left = new ThreadTreeNode(4);
root->left->right = new ThreadTreeNode(5);
// 构建线索二叉树
createThreadTree(root);
// 遍历线索二叉树
cout << "中序线索二叉树遍历结果:";
threadTreeTraverse(root);
return 0;
}
3. 线索二叉树核心优势
-
节省空间:复用空指针,消除内存冗余,无需额外存储前驱后继信息
-
遍历高效:遍历无需栈、无需递归,时间复杂度 O(n),无栈溢出风险
-
查询便捷:可直接获取任意节点的前驱、后继节点,无需重复遍历
六、面试高频考点终极梳理
- 层序遍历和深度遍历的核心区别?
深度遍历(DFS)依靠栈,纵向深挖,优先遍历子树;层序遍历(BFS)依靠队列,横向遍历,优先遍历同级节点,适配层级类、最短路径类问题。
- 层序遍历的核心框架是什么?
队列存节点、循环取层、逐层遍历、子节点入队,是所有BFS算法的基础模板。
- 线索二叉树的作用是什么?
解决普通二叉树空指针冗余问题,实现非递归、无栈高效遍历,快速查找节点前驱后继。
- ltag、rtag 标记位的意义?
区分指针类型,避免遍历时分不清指向的是孩子节点,还是线索前驱/后继节点。
七、全文总结
至此,二叉树深度遍历 + 层序遍历 + 线索二叉树三大核心体系全部讲透:
-
深度遍历(前/中/后):栈实现,纵向遍历,适配常规刷题
-
层序遍历:队列实现,横向遍历,笔试高频必考
-
线索二叉树:优化空间与遍历效率,解决原生二叉树缺陷,进阶核心考点
这三套知识点相辅相成,覆盖了本科考试、算法刷题、求职面试90%以上的二叉树考点,熟练掌握即可轻松应对所有二叉树基础+进阶题型!
后续会更新二叉树平衡、哈夫曼树等高阶内容,想学透数据结构可以持续关注!