(原创)线索二叉树那点小破事

Source

线索二叉树

二叉树的基本定义结构我们都很熟悉,节点数据加上孩纸指针,左孩子指娘家,右孩子指婆家,我们来看这个例子:

我们会发现,有些孩子并没有地方可以去,例子中的树一共十个结点,十一个空闲指针,由此引出我们对于空闲指针的计算公式:一个有 n 个结点的二叉树有 2n 个指针域,而 n 个结点会产生 n-1 个分支,每个分支对应其指向孩子的指针域,所以空闲指针数: 2n - (n-1) = n+1 ,这么多指针域我们怎样规划能避免这些指针便宜null呢?

大佬们提出了这样一个用途,中序遍历这棵树我们可以得到: HDIBJEAFCG ,很轻松。那么我们可以愉快的知道 I 是 B 的前驱,或者 J 是 B 的后继这样的信息,那是借助了遍历,我们能不能把这些空的指针指向前驱和后继呢,当然阔以。

但是马上有人站出来说,那指向孩子的指针原来就存在了,这后来的野生指针和指向孩子的指针可怎么区分呢,大佬们就决定再加个量来区分指针的类型,如果是指向孩子的就用 0 表示,用作前驱后继的就用 1 表示,使用的时候进行遍历就可以一劳永逸地取得前驱后继的信息,OK,看看结构的定义变成啥样了。

和原来差不多对不对,多了个枚举变量的定义

OK,现在我们的线索二叉树可以算正式出炉辽:

我们把指向前驱和后继的指针称为线索,加上线索的二叉链表称为线索链表,相应的二叉树就称为线索二叉树 (Threaded Binary Tree)
来看一看抽象化结点的结构:

  • ltag = 0 或者 Link,表示这个结点的左指针指向左孩子,ltag = 1 或者 Thread,表示这个结点的左指针指向前驱

  • rtag = 0 或者 Link,表示这个结点的右指针指向右孩子,rtag = 1 或者 Thread,表示这个结点的右指针指向后继

改头换面的例子二叉链表

中序线索化遍历的实现

我们来读一下代码,朴素的中序遍历的框架其实还在,中间加粗的是线索化的代码,另外多了 pre 的指针,我们可以发现这个指针永远比我们的主角指针 p 慢一拍,它永远在 p 的前驱结点,每次到了一个结点,我们先判断一哈这个结点有没有左子树,如果没有,就使这个节点的标记值置Thread,表示这个结点被前驱大爷承包了,然后就是让指针 p 的左孩子管 pre 叫干爹, 表示这个结点的前驱就定下了;然后是后继,我们会发现这个和前驱有点不同,这个先看前驱结点的右孩子是否为空,即 如果 pre -> rchild 为空,就把这个引到 p 指针,我们说 p 指针走的快一步,所以这就找到了后继,顺理成章 pre->rtag = Thread,pre -> rchild = p; 完美! 这两步表示一轮线索化的一个轮回,弄完这些让 pre 再向前走一步,就是 pre = p,剩下的和中序遍历没什么区别

我们给这个二叉链表加上一个头指针,并且让头结点的左孩子指向树根节点,让中序的第一个节点指回头结点的左孩子指针,用头结点的右指针指向中序遍历的最后一个结点,并且把这个指针指回头结点的右指针,形成双向的指针循环,这样的好处就在于既可以从第一个结点开始沿中序的后继向后遍历,也可以利用从头开始前序遍历(例子及表述来自《大话数据结构》)

附上带头结点的线索二叉树中序遍历:

首先定义指向根的头结点 p,我们先看外层这个最大的循环,这个循环条件为 p != T ,这个条件首先排除了空树的遍历,其次按照刚才对带头节点二叉线索表的分析,最后的 p 指针会在穷途末路时等于 T ,这相当于 p 指向头结点,这时 p = T ,故如果不加 p != T ,则会无限循环下去。进入循环,首先是判定p -> ltag 等于Link,内容是指向左子树,那么这一个循环表示从根开始直到没有左子树的末端,路径是A →B→D→H,然后打印H,目前 p 的右指针是指向直接后继D的,所以下面一个进入第二个while,打印出D,D的右边是孩子指针,第十五行指向I,然后重复整个过程,相当于从小树到大树以链表搜索的形势进行完整的中序遍历,完整顺序参照下图

好的,我们今天的线索二叉树就到这里辽,最后附上一段我偷来的话:

在实际问题中 ,如果所用的二叉树需经常遍历或查找结点时需要某种遍历序列中的前驱和后继,那么采用线索二叉链表的存储结构就是非常不错的选择。