7.7 线索二叉树

来源:互联网 发布:配乐朗诵录音软件 编辑:程序博客网 时间:2024/05/22 20:46

为什么要创建线索二叉树

对于普通含有n个结点的二叉树来说,一共含有2n个指针域,而实际上,树的顶点集中元素的个数V和边集中元素的个数E的关系满足|V|=|E|+1,也就是说在上述二叉树中n=e+1,得e=n1,共n1条边,于是只需要n1个指针就可以刻画这颗二叉树了。但是在采用二叉链存储结构的二叉树来说,对于大多数语言来说,声明一个二叉树结点,必须静态指定两个指针域,分别表示左孩子和右孩子,这样必然会浪费掉n+1个指针,他们指向空,就等于说是完全浪费了。
因此采用线索二叉树,就是在于充分利用这些空余的指针,来提高一种树的常用操作——遍历的效率。不过实际上,往往并不会刻意去构造一颗线索二叉树,在最后会作分析。

基本概念

考虑到二叉树的遍历结果是一个线性序列,因此,很自然地需要一个东西用来表示前驱和后继的关系。
因此定义:线索(thread)是指向当前结点的“前驱”或者“后继”的指针。创建线索的过程称之为线索化。
为了区分一个结点上的指针到底是线索还是实际的孩子,我们需要使用一个辅助标记tag_is_thread,如果为真则为线索,反之指向孩子的指针。在实际实现的过程中,往往还需要一个头结点(哑元),指向树根。

相关算法

因为线索确实不是很常见,因此没有实现具体的代码,基本上是参考书上的代码,故此处代码从略。

创建线索

创建线索实际上就是在一棵已经构造好了的二叉树上遍历,在遍历过程中添加线索。线索实际上是一个将树扁平化的过程,因此每一步添加线索的关键在于,要找到前驱pre。这样cur的前驱就是pre,而pre的后继就是cur。至于处理的位置,就是遍历过程中应该访问的位置。比如中序遍历,就要在访问左子树和右子树中间进行处理,这样线索就维持了中序遍历的前后关系。
特殊地方的处理在于哑元结点。哑元结点指向树根,而整个递归过程结束时,pre恰好指向最后一个结点。因此pre的后继就是哑元,而哑元的前驱就是pre了。
为了递归过程中pre的可用性,pre是全局变量。
实际上,利用线索,将二叉树遍历过程也转化成了一个双向循环链表。

使用线索

使用线索就更为简单了。首先需要借助开始的哑元结点找到序列的第一个元素,然后一直找右孩子结点。如果是线索,直接挪向这个线索指向的结点;如果不是线索,就去找左孩子。直到回到开始的哑元结点,整个遍历结束。

为什么线索二叉树并不常见

首先,较为简单的,也就是书上提到的建立线索的算法,是对于静态集合而言。对于动态插入和删除的树(平衡搜索树),其在集合发生变化时,维持线索的复杂程度要高得多。

为什么STL和linux都使用红黑树作为平衡树的实现? - 雷鹏的回答 - 知乎
https://www.zhihu.com/question/20545708/answer/31176525

这个回答确实提出,使用线索时,正向遍历效率很高,但是其实现是非常依赖技巧的。它将一棵红黑树的两个结点用数组表示,ltagrtag标记位用1位二进制分别并在lchildrchild的31位后面,也就是说childtag一共才占用一个机器字长。而如果使用二叉链的指针式表示,这是不可能的:指针必须占用一个机器字长,才能对整个地址空间进行寻址。另外再用一个tag,由于内存对齐原因,还要再占用一个机器字长。不同于数据结构课程中理想化考虑tag用作bool布尔型只需要一个二进制位,实际上由于内存对齐的原因,这样反而额外浪费了一倍的空间(ARM架构要求指令必须对齐,而x86虽然可以不对齐,但是取指令就需要两次周期,效率显而易见。)
况且,对于二叉树遍历来说,一旦“碰底”,也就是当前子树处理完毕进行回溯,以中序遍历为例,有左孩子的非叶子结点需要被访问两次(想想非递归,先进栈后出栈),因此,额外增加的时间开销也最多根没有左孩子的非叶子结点个数成正比,最坏的情况不过O(n),并不会增加渐进复杂度。
该回答中线索提高的效率,属于常数优化,并且和顺序结构代替链式结构,有效地利用了存储器的空间局部性也极大地提高了效率。

原创粉丝点击