前言
我们现在提倡节约型社会,一切都应该节约为本。对待我们的程序当然也不例外,能不浪费的时间或空间,都应该考虑节约。
对于一颗二叉树,我们发现并不是所有指针域都充分利用了,比如下图这颗二叉树,我们发现好几个节点都有着空指针域,我们应该想办法利用起来。
首先分析空指针的个数
对于一颗n个节点的二叉树,一共有n - 1条边,2 * n个指针域,所以有2 * n - (n - 1) = n + 1个空指针域,上图右10个节点,也就是说有11个空指针域,这些空间不存储信息,造成了空间浪费。
另一方面,我们对该二叉树进行中序遍历时,得到了HDIBJEAFCG这一序列,遍历过后我们知道了每个节点的前驱和后继,但这是建立在中序遍历的基础上的,在一个二叉树中我们只知道一个节点的左右孩子的地址,如果想要知道其前驱节点是谁,后继节点是谁,必须经过一次遍历,所以我们为什么不考虑在创建的时候就存储其前驱和后继呢?
线索二叉树的概念
综合刚才两个角度的分析,我们可以考虑利用那些空地址,我们把这种指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树(Thread Binary Tree)。
如下图,我们把这颗二叉树进行中序遍历后,将所有空指针域的right,指向它的后继节点,于是我们可以知道H的后继是D,I的后继是B,J的后继是E,F的后继是C,G的后继不存在所以指向空
同样的,当我们将所有空指针域的left指向它的前驱节点,就得到了一颗完整的线索二叉树,如下
线索二叉树的节点改造
好事多磨,我们虽然已经明确了线索二叉树的概念,但是如何去知道某一结点的left指向左孩子还是前驱,right指向右孩子还是后继呢?(线索二叉树仍然是二叉树,我们需明确其左右孩子的意义)
比如E节点left指向了左孩子,但是right却指向了后继,显然我们对此需要增加标识,我们在每个节点增加两个标志域:ltag和rtag,只是存放0或1的布尔型变量,其占用的内存空间小于指针变量。于是有了如下节点结构:
其中: ■ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。 ■rtag为0时指向该结点的右孩子,为1时指向该结点的后继。 因此对于左下图的二叉链表图可以修改为右下图的样子。
中序线索二叉树结构的实现
中序线索二叉树的结构
// 枚举类
enum class PointerTag
{
Link,
Thread
};
// 线索二叉树的节点定义
typedef char TElemType;
struct BiThrNode
{
BiThrNode(const TElemType &data, PointerTag ltag = PointerTag::Link, PointerTag rtag = PointerTag::Link) : _left(nullptr), _right(nullptr), _data(data), _ltag(ltag), _rtag(rtag)
{
}
// 指针域
BiThrNode *_left;
BiThrNode *_right;
// 数据域
TElemType _data;
// 孩子/后继标识
PointerTag _ltag;
PointerTag _rtag;
};
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。由于前驱和后继的信息只有在遍历该二叉树时才能得到,所以线索化的过程就是在遍历的过程中修改空指针的过程。
中序遍历线索化递归代码
void InThreading(BiThrNode *root, BiThrNode *&prev) // 主程序调用该函数时prev为nullptr
{
if (root)
{
InThreading(root->_left, prev); // 递归线索化左子树
if (!root->_left)
{
// 当前节点左为空,则当前节点左指向前驱也就是prev
root->_left = prev;
root->_ltag = PointerTag::Thread;
}
if (prev != nullptr && !(prev->_right))
// 前驱节点右为空,则前驱节点指向后继也就是root
// 前驱节点的左已经在前驱节点的线索化函数内处理过了
{
prev->_right = root;
prev->_rtag = PointerTag::Thread;
}
prev = root;
// 处理当前节点的右
// 右不为空则不必处理root的右为Thread
// 如果为空,那么回溯到某次调用,root作为prev则会处理右
InThreading(root->_right, prev);
}
}
仔细观察发现,除了注释部分代码,其余部分和中序遍历递归代码极其类似,只不过把打印节点的语句改为了线索化的功能。
if(!root->_left)表示如果某结点的左指针域为空,因为其前驱结点刚刚访问过,赋值给了prev,所以可以将prev赋值给root -> _left ,并修改root -> _ltag = Thread (也就是定义为1)以完成前驱结点的线索化。
后继就要稍稍麻烦一些。因为此时root结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针_right做判断,if(!prev >right)表示如果为空,则p就是prev的后继, 于是prev -> _right = root,并且设置prev ->rtag = Thread,完成后继结点的线索化。
中序线索二叉树的遍历
有了线索二叉树后,我们对它进行遍历时发现,其实就等于是操作一个双向链表结构。.
中序线索二叉树的遍历代码
void InOrderTraverse_Thr(BiThrNode *root)
{
if (!root)
return;
// 找到中序遍历第一个节点
while (root->_left)
root = root->_left;
while (root)
{
// 当_ltag为Link就找到了当前子树中序遍历第一个节点
while (root->_ltag == PointerTag::Link)
{
root = root->_left;
}
cout << root->_data << " ";
// 当_rtag为Thread说明当前节点没有右子树,只有后继
while (root->_rtag == PointerTag::Thread && root->_right)
{
root = root->_right;
cout << root->_data << " ";
}
// 当前节点右右子树,则进入新的子树(也有可能到了中序遍历最后一个节点)
root = root->_right;
}
}
运行结果展示
主程序代码
// 构建示例图二叉树
BiThrNode *_root = new BiThrNode('A');
_root->_left = new BiThrNode('B');
_root->_right = new BiThrNode('C');
_root->_left->_left = new BiThrNode('D');
_root->_left->_right = new BiThrNode('E');
_root->_right->_left = new BiThrNode('F');
_root->_right->_right = new BiThrNode('G');
_root->_left->_left->_left = new BiThrNode('H');
_root->_left->_left->_right = new BiThrNode('I');
_root->_left->_right->_left = new BiThrNode('J');
// 线索化前的中序遍历
cout << "线索化前的中序遍历"
<< ": ";
InOrder(_root);
// 线索化后的中序遍历
BiThrNode *prev = nullptr;
InThreading(_root, prev);
cout << "线索化后的中序遍历"
<< ": ";
InOrderTraverse_Thr(_root);