线索二叉树
一 概念
线索二叉树(Threaded Binary Tree)是一种对二叉树的优化结构,主要解决传统二叉树遍历时需要借助栈或递归(额外空间开销)的问题。通过利用节点中的空指针(nullptr
)存储遍历过程中的前驱或后继节点信息(称为 “线索”),可以实现无需额外空间的二叉树遍历。
传统二叉树中,每个节点有两个指针(左孩子lchild
、右孩子rchild
)。对于一棵有n
个节点的二叉树,共有2n
个指针域,但其中只有n-1
个指针被有效使用(连接子节点),剩余n+1
个指针为空(nullptr
)。
线索二叉树的核心思想是:将这些空指针利用起来,存储节点在某种遍历顺序(如中序、前序、后序)中的前驱或后继节点的地址,这些被重新利用的指针称为 “线索”。
为了区分一个指针是指向子节点还是线索,每个节点需要增加两个标志位:
ltag
:左标志位ltag = 0
:lchild
指向左子节点ltag = 1
:lchild
是前驱线索(指向当前节点在遍历顺序中的前驱)
rtag
:右标志位rtag = 0
:rchild
指向右子节点rtag = 1
:rchild
是后继线索(指向当前节点在遍历顺序中的后继)
二 代码
1.线索二叉树结点
//线索二叉树结点
struct TreeNode {int data;TreeNode* left;TreeNode* right;bool ltag; //左线索,若为true,则left表示前驱结点,否则为左孩子结点bool rtag; //右线索,若为true,则right表示前驱结点,否则为右孩子结点TreeNode(int d) : data(d), left(nullptr), right(nullptr), ltag(false), rtag(false) {}
};
新增左右线索标志。若左线索为true,则left表示前驱结点,否则为左孩子结点。若右线索为true,则right表示后继结点,否则为右孩子结点。
2.构建线索二叉树
//构建线索二叉树(通过前序遍历序列构建)
TreeNode* CreateTree(vector<int>& preorder, int& index, int nullnode) {//若索引超出数组范围或数组索引处为空值nullnode,则令index++并返回空if (index >= preorder.size() || preorder[index] == nullnode) {index++;return nullptr;}TreeNode* node = new TreeNode(preorder[index++]); //为新结点分配内存node->left = CreateTree(preorder, index, nullnode); //递归构建左子树node->right = CreateTree(preorder, index, nullnode); //递归构建右子树return node; //返回当前结点
}
通过前序遍历序列来构建线索二叉树,若传入的索引超出数组范围或数组索引处为空值nullnode,则令Index++并返回空。为新结点分配内存,然后递归构建左右子树。
3.中序遍历线索化
//中序遍历线索化
TreeNode* pre = nullptr; //定义前驱结点void inorderThreading(TreeNode* node) {if (node == nullptr) return; //当传入结点为空时直接返回inorderThreading(node->left); //递归左子树,找到最左边孩子结点//传入结点的左子结点为空时,令其左指针指向前驱结点preif (node->left == nullptr) { node->left = pre;node->ltag = true;}//当前驱结点不为空且其右子结点为空时,令前驱结点的右指针指向当前结点if (pre && pre->right == nullptr) { pre->right = node;pre->rtag = true;}pre = node; //令前驱结点指向当前结点inorderThreading(node->right); //递归右子树
}
中序遍历线索化是最常用的线索化方式。首先在函数外定义一个全局结点变量pre表示前驱结点。
在函数体中,当传入的结点为空时直接返回。先递归左子树找到最左边孩子结点,当传入结点的左子结点为空时,令其左指针指向前驱结点pre,当前驱结点不为空且右子结点为空时,令前驱结点的右指针指向当前结点。最后令前驱结点指向当前结点并递归右子树。
4.中序遍历线索二叉树
//中序遍历线索二叉树
void inorderTraversal(TreeNode* root) {TreeNode* p = root;//找到中序遍历第一个节点(最左叶子)while (!p->ltag) { // 左线索为false表示有左子树,继续往左找p = p->left;}//循环遍历所有节点while (p != nullptr) {cout << p->data; //访问当前结点//找后继结点if (p->rtag) { //若rtag为true,则直接通过右线索找到后继结点p = p->right;}else { //若rtag为false,则找右子树的最左子结点p = p->right;while (!p->ltag) {p = p->left;}}}
}
首先找到中序遍历的第一个结点(最左叶子结点),左线索为false表示有左子树,继续往左找。
找到后循环遍历所有结点,每次循环先访问当前结点,然后找后继结点。若rtag为true,则直接通过右线索找到后继结点,若rtag为false,则找右子树的最左子结点。
5.释放内存
//释放线索二叉树内存
void FreeTree(TreeNode* root) {if (root == nullptr) return; // 空树直接返回// 保存当前节点的左右真实子节点(仅当标志为false时有效)TreeNode* leftChild = (root->ltag == false) ? root->left : nullptr;TreeNode* rightChild = (root->rtag == false) ? root->right : nullptr;// 递归释放左右子树(仅处理真实子节点)FreeTree(leftChild);FreeTree(rightChild);//释放当前结点delete root;root = nullptr; //指向空,避免野指针
}
若传入结点为空,直接返回。保存当前结点的左右真实结点,仅当标志为false时有效。接着递归释放左右子树,仅处理真实子结点。然后释放当前结点并让当前结点指向空以避免野指针。
三 线索二叉树优化之处
线索二叉树相比传统二叉树的核心优化在于空间利用率和遍历效率,主要解决了传统二叉树在遍历和前驱 / 后继查找时的痛点。以下是具体优化点的对比分析:
1. 优化了空指针的浪费,节省存储空间
传统二叉树中,每个节点有两个指针(左 / 右孩子),但实际只有n-1
个指针被有效使用(连接子节点),剩余n+1
个指针为空(nullptr
)。这些空指针未被利用,造成了空间浪费(以n
个节点的二叉树为例,空指针占比约 50%)。
线索二叉树通过引入标志位(ltag
/rtag
),将空指针重新定义为 “线索”,存储节点在某种遍历顺序(如中序)中的前驱或后继节点地址。原本被浪费的n+1
个空指针被充分利用,实现了空间的高效复用。
2. 优化了遍历的空间复杂度,无需额外栈 / 递归
传统二叉树的遍历(如中序、前序、后序)依赖递归或显式栈结构:
- 递归遍历:隐含使用调用栈,空间复杂度为
O(h)
(h
为树的高度,最坏情况O(n)
); - 迭代遍历:需手动维护栈,空间复杂度同样为
O(h)
。
线索二叉树通过线索直接记录前驱 / 后继关系,遍历过程中无需额外栈或递归,空间复杂度降至O(1)
(仅需一个指针变量)。例如,中序线索二叉树的遍历只需从最左节点开始,沿后继线索依次访问即可。
3. 优化了前驱 / 后继的查找效率
在传统二叉树中,查找某个节点的前驱或后继(如中序前驱 / 后继)需要重新遍历整棵树或回溯父节点,时间复杂度为O(n)
(最坏情况)。
线索二叉树中,若节点的线索标志位为 1(ltag=1
或rtag=1
),则其前驱 / 后继可直接通过线索指针获取(时间复杂度O(1)
);若标志位为 0(仍指向子节点),则仅需遍历子树即可找到前驱 / 后继(时间复杂度O(h)
,优于传统二叉树的O(n)
)。
总结:优化的本质是 “空间换时间” 的平衡
线索二叉树通过牺牲少量标志位(每个节点增加 2 个int
标志),将原本浪费的空指针转化为遍历线索,最终实现:
- 空间优化:复用空指针,减少额外存储(如栈空间);
- 时间优化:遍历和前驱 / 后继查找的效率显著提升(尤其在需要频繁遍历的场景中)。
这一设计使得线索二叉树在编译器语法分析、文件系统目录遍历等需要高效顺序访问的场景中被广泛应用。