目录

一、线索二叉树的定义

1.1 线索的概念

1.2 数据结构

1.3 优缺点

二、线索二叉树的构建

2.1 线索化

2.2 实现中序遍历线索化

三、线索二叉树的应用

3.1 求某个结点的中序后继

3.2 使用前驱后继遍历线索二叉树


对于二叉树,有以下几个问题:

1. 二叉链表的空间利用情况如何?

        答:在n个结点的二叉树左右链表示中,只有n-1个指向子树的指针,却有n+1个空指针域。是否有些浪费?

2. 二叉链表中如何找某个结点的某种遍历的前驱和后驱?

        答:每次都要从根节点开始遍历。是否有些麻烦?

3. 如何遍历二叉链表表示的二叉树

        答:利用栈或队列。可不可以不用呢?

为了解决上述问题,线索二叉树应运而生。


一、线索二叉树的定义

1.1 线索的概念

对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树(Threaded BinaryTree)

1.2 数据结构

其数据结构定义如下:

/* 线索二叉树结构 */
struct Node
{
    char data;
    struct Node *lchild, *rchild;
    int ltag, rtag;
};

其中,

可以看到,child位置被充分利用起来,利用了所有的空指针;tag用以区分该结点的两个指针域指向左右孩子还是前驱后驱。

1.3 优缺点

那么这么做有什么好处呢?

优点:

① 利用线索二叉树进行某种遍历时,不必采用堆栈处理,速度较一般二叉树的遍历速度快,且节约存储空间。

② 任意一个结点都能直接找到它的前驱和后继结点。

缺点:

① 结点的插入和删除麻烦,且速度也较慢。

② 线索子树不能共用。


二、线索二叉树的构建

2.1 线索化

将结点的空指针域指向其前驱/后继的指针被称为线索;结点的空链域存放其前驱后继的过程称为线索化

显然,二叉树的遍历方式有4种,故对应了4种线索二叉树:

(1)先序线索二叉树;

(2)中序线索二叉树;

(3)后序线索二叉树;

(4)层序线索二叉树。

2.2 实现中序遍历线索化

如何将一个二叉树线索化?一言以蔽之:正常遍历的过程中将前后驱存起来即可。因此其框架与遍历相同,只需修改”访问结点“部分。以中序遍历为例,其线索化的操作具体为:

记录前驱:如果该结点没有左孩子,则把其ltag置为1,并把其lchild置为前驱pre;

记录后驱:后驱并不和前驱一样,因为在遍历至当前结点时,并不知道该结点的后驱是谁,所以我们并不能简单地将判断设置为”如果该结点的右孩子为空“,而是要将当前结点的记录后驱部分留给要遍历的下一个结点来做。

因此我们采取以下判断策略:如果当前结点的前驱不为空(说明不是首结点),并且前驱的右孩子为空(说明前驱需要记录后继,而此刻遍历到的结点正是前驱的后继),便将前驱的rchild记为当前结点,同时把其rtag置为1。记录后驱,并不是说把当前结点的后驱记录下来,而是给上一个结点”擦屁股“,把上一个结点的后驱给补上;

③ 最后将前驱更新为当前结点。

这里给出一棵树,以便读者结合图像来理解。

其中序遍历为: G D I H B A E J C F

代码示例:

 ThTree pre = NULL;  // 全局变量,记录前驱
 ​
 /* 中序线索二叉树线索化 */
 void inOrderThTree(ThTree BT)
 {
     if (BT)
     {
         inOrderThTree(BT->lchild); // 遍历左子树
 ​
         // 线索化
         if (!BT->lchild) // 没有左孩子时
         {
             BT->ltag = 1;     // 标记tag
             BT->lchild = pre; // 前驱
         }
         if (pre && !pre->rchild)  // 前驱不为空且前驱的右孩子为空
         {
             pre->rchild = BT;
             pre->rtag = 1;
         }
         pre = BT;  // 更新前驱为当前节点
          
         inOrderThTree(BT->rchild); // 遍历右子树
     }
 }

关于其余3种线索树,请读者自行实现。

三、线索二叉树的应用

以下仍以上述构建的中序线索二叉树为例。

3.1 求某个结点的中序后继

思路:

① 当结点(记为p)的rtag为1时,其后续(记为post)即为p->rchild;

② 当p->rtag == 0时,post为p的右子树的最左结点。

代码示例:

 /* 求中序线索树结点的后继 */
 ThTree inNext(ThTree p)
 {
     ThTree post = NULL; // 后继结点
     if (p->rtag)        // 如果有后继,直接返回
         return p->rchild;
     else // 如果有右孩子,则后继在右子树的最左结点
     {
         if(!p->rchild) return NULL;
         post = p->rchild;
         if (post->rtag == 0 && post->rchild == NULL)
             return post;
         while (post->ltag == 0 && post->lchild != NULL) // 往左走到头
         {
             post = post->lchild;
         }
         return post;
     }
 }

3.2 使用前驱后继遍历线索二叉树

当结点中保存了前驱和后继的信息之后,便不需要再反复递归调用来遍历二叉树。

代码示例:

 /* 前驱后继中序遍历线索二叉树 */
 void inOrderTraverseThTree(ThTree BT)
 {
     ThTree head = BT;
     while (head->lchild && head->ltag == 0)  // 找到起点
         head = head->lchild;
     while (head != NULL)
     {
         if (head)
             printf("%c ", head->data);
         head = inNext(head);
     }
 }

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐