重拾算法(2)——线索二叉树

重拾算法(2)——线索二叉树

上一篇我们实现了二叉树的递归和非递归遍历,并为其复用精心设计了遍历方法Traverse(TraverseOrder order, NodeWorker<T> worker);今天就在此基础上实现线索二叉树。

什么是线索二叉树

二叉树中容易找到结点的左右孩子信息,但该结点在某一序列中的直接前驱和直接后继只能在某种遍历过程中动态获得。

先依遍历规则把每个结点某一序列中对应的前驱和后继线索预存起来,这叫做"线索化"。

意义:从任一结点出发都能快速找到其某一序列中前驱和后继,且不必借助堆栈。

这就是线索二叉树(Threaded Binary Tree)

数据结构

如何预存这类信息?有两种解决方法。

每个结点增加两个域:fwd和bwd

与原有的左右孩子指针域"复用"

任意一个二叉树,其结点数为N,则有N+1个空链域。空链域就是叶结点的lchild和rchild。比如下面这个二叉树

它有7个结点,有(7+1)个空链域。这个结论可以用数学归纳法证明。

大体思路就是充分利用那n+1个空链域,用它存储前驱结点和后继结点。

我们规定:

  • 若结点有左子树,则lchild指向其左孩子;否则,lchild指向其直接前驱(即线索);
  • 若结点有右子树,则rchild指向其右孩子;否则,rchild指向其直接后继(即线索) 。

那么就有如下一个问题:如何判断是孩子指针还是线索指针?

容易的很,加两个标志域:

标志域只需要2个bit,而用增加指针域的方式则需要2个指针的空间(一个指针就是一个int的长度)。所以这个方案极大地节省了空间。

我们规定:

  • 当Tag域为0时,表示孩子情况;
  • 当Tag域为1时,表示线索情况。

有关线索二叉树的几个术语

线索链表:用含Tag的结点样式所构成的二叉链表。

线 索:指向结点前驱和后继的指针。

线索二叉树:加上线索的二叉树。

线 索 化:对二叉树以某种次序遍历使其变为线索二叉树的过程。

线索化过程就是在遍历过程中修改空指针的过程:

  • 将空的lchild改为结点的直接前驱;
  • 将空的rchild改为结点的直接后继;
  • 非空指针呢?仍然指向孩子结点。(称为"正常情况")

C#实现线索二叉树

数据结构

线索二叉树是(is a)二叉树。所以最初我想继承上一篇的二叉树BinaryTreeNode<T>类型。但后来发现代码不太好掌控,不如来得直接些,让二叉树和线索二叉树作为两个毫不相干的类型。以后重构的时候再考虑考虑如何整合。

 1     public partial class ThreadedBinaryTreeNode<T>
 2     {
 3         public T Value { get;set; }
 4         public ThreadedBinaryTreeNode<T> Parent { get;set; }
 5         public ThreadedBinaryTreeNode<T> Left { get;set; }
 6         public ThreadedBinaryTreeNode<T> Right { get;set; }
 7         public bool Point2PreviousNode { get; private set; }
 8         public bool Point2NextNode { get; private set; }
 9         public TraverseOrder Order { get;set; }
10         private bool threadedOnce = false;
11
12         public ThreadedBinaryTreeNode(T value, ThreadedBinaryTreeNode<T> parent = null, ThreadedBinaryTreeNode<T> left = null, ThreadedBinaryTreeNode<T> right = null)
13         {
14             this.Value = value; this.Parent = parent; this.Left = left; this.Right = right;
15         }
16
17         public void Traverse(TraverseOrder order, ThreadedNodeWorker<T> worker)
18         {
19             if (worker == null) { return; }
20
21             if ((!threadedOnce) || (order != this.Order))
22             {
23                 var router = new Router<T>(worker);
24                 this.NormalTraverse(order, router);
25                 this.Order = order;
26                 this.threadedOnce = true;
27             }
28             else
29             {
30                 this.ThreadedTraverse(order, worker);
31             }
32         }
33     }
34
35     public abstract class ThreadedNodeWorker<T>
36     {
37         public abstract void DoActionOnNode(ThreadedBinaryTreeNode<T> node);
38     }

C#里用bool类型作为标志变量大概再好不过了。

线索化

线索二叉树的线索化是首先要解决的问题。

 1     public partial class ThreadedBinaryTreeNode<T>
 2     {
 3         /* 略 */
 4         private void RefreshThread(TraverseOrder order)
 5         {
 6             var threader = new Threader<T>();
 7             this.NormalTraverse (order, threader);
 8             this.Order = order;
 9         }
10
11         class Threader<TThreader> : ThreadedNodeWorker<TThreader>
12         {
13             Queue<ThreadedBinaryTreeNode<TThreader>> queue = new Queue<ThreadedBinaryTreeNode<TThreader>>();
14             public override void DoActionOnNode(ThreadedBinaryTreeNode<TThreader> node)
15             {
16                 this.queue.Enqueue(node);
17                 if (this.queue.Count <= 1) { return; }
18
19                 var pre = this.queue.Dequeue();
20                 var next = this.queue.Peek();
21                 if (pre.Right == null)
22                 {
23                     pre.Right = next;
24                     pre.Point2NextNode = true;
25                 }
26                 if (next.Left == null)
27                 {
28                     next.Left = pre;
29                     next.Point2PreviousNode = true;
30                 }
31             }
32         }
33     }

代码中的this.NormalTraverse()函数与上一篇中二叉树的遍历方法Traverse()思路是一样的,唯一区别在于传入的操作结点的对象threader,这个threader在用前序、中序、后序或层次遍历二叉树时,将结点存入一个队列queue,当queue中有2个结点时,就设置他们互为前驱后驱(若此2个结点的Left/Right为空的前提下)。简单来说,就是遍历二叉树,根据访问结点的先后顺序依次修改其前驱后驱指针(及标识)

完成线索化后,就可以遍历了。

前序遍历

前序遍历线索二叉树的思路是:对于一个结点A及其左右子树Left和Right,先访问A;如果A的标记Point2NextNode为true,则A的下一结点通过A.Right就能确定;否则,有两种情况:(1)A的下一结点在A.Left上,而以A.Left为根的这个子树的第一个被访问的结点,恰恰是A.Left这个结点。(2)A.Left为空,这说明A的下一结点在A.Right上。若A.Right也为空,就说明整个树遍历结束。

 1         void ThreadedPreorderTraverse(ThreadedNodeWorker<T> worker)
 2         {
 3             var node = this;// root node is the first node to work with.
 4             while (node != null)
 5             {
 6                 worker.DoActionOnNode(node);
 7                 if (node.Point2NextNode)
 8                 {
 9                     node = node.Right;
10                 }
11                 else
12                 {
13                     if (node.Left != null)// visit left sub tree
14                     {
15                         node = node.Left;// first node of left sub tree is the ‘root‘ of left sub tree
16                     }
17                     else
18                     {
19                         node = node.Right;// node.Right cannot be null because the tree is threaded.
20                         // Only one condition can break this when node.Right is the last node to visit.
21                     }
22                 }
23             }
24         }

中序遍历

中序遍历线索二叉树的思路是:对于一个结点A及其左右子树Left和Right,第一个要访问的结点在A.Left子树,A.Left子树中第一个要访问的结点则在A.Left.Left子树中,依次循环,直到最后一个叶结点的Left。所以A树要访问的第一个结点就用GetLeftMostNode()这个函数去找。访问完A,就该A的右子树了,所以A.Right就成为下一个A了,所以就用GetLeftMostNode(A.Right)获取下一个结点。

 1         ThreadedBinaryTreeNode<T> GetLeftMostNode(ThreadedBinaryTreeNode<T> node)
 2         {
 3             if (node == null) { return null; }
 4             while (node.Left != null)
 5             {
 6                 node = node.Left;
 7             }
 8             return node;
 9         }
10
11         void ThreadedInorderTraverse(ThreadedNodeWorker<T> worker)
12         {
13             var node = GetLeftMostNode(this);
14             while (node != null)
15             {
16                 worker.DoActionOnNode(node);
17                 if (node.Point2NextNode)
18                 {
19                     node = node.Right;
20                 }
21                 else
22                 {
23                     node = GetLeftMostNode(node.Right);// visit right sub tree.
24                 }
25             }
26         }

后续遍历

后序遍历线索二叉树的思路是:对于一个结点A及其左右子树Left和Right,第一个要访问的结点在A.Left子树,A.Left子树中第一个要访问的结点则在A.Left.Left子树中,依次循环,直到最后一个叶结点的Left。所以A树要访问的第一个结点就用GetLeftMostNode()这个函数去找。访问完A树,下一步分三种情况:(1)A是整个树的根,这时整个遍历结束。(2)A树是A.Parent的左子树,这时就该A.Parent.Right了,于是A.Parent.Right就成了之前的A。(3)A树是A.Parent的右子树,这时就该A结点了。

 1         void ThreadedPostorderTraverse(ThreadedNodeWorker<T> worker)
 2         {
 3             var node = GetLeftMostNode(this);
 4             while (node != null)
 5             {
 6                 worker.DoActionOnNode(node);
 7                 if (node.Point2NextNode)
 8                 {
 9                     node = node.Right;
10                 }
11                 else
12                 {
13                     var parent = node.Parent;
14                     if (parent != null)// node is NOT root
15                     {
16                         if (parent.Right == node)// node is right sub tree of parent
17                         {
18                             node = parent;
19                         }
20                         else// node is left sub tree of parent
21                         {
22                             node = GetLeftMostNode(parent.Right);//parent.Right will never be null because the tree is threaded.
23                         }
24                     }
25                     else// node is root of whole tree
26                     {
27                         node = parent;
28                     }
29                 }
30             }
31         }

层次遍历

线索二叉树对于层次遍历似乎没有优化的能力,其层次遍历仍旧用普通二叉树的遍历方法好了。

 1         void ThreadedLayerTraverse(ThreadedNodeWorker<T> worker)
 2         {
 3             NormalLayerTraverse(worker);
 4         }
 5         void NormalLayerTraverse(ThreadedNodeWorker<T> worker)
 6         {
 7             var queue = new Queue<ThreadedBinaryTreeNode<T>>();
 8             queue.Enqueue(this);
 9             while (queue.Count > 0)
10             {
11                 var element = queue.Dequeue();
12                 if (element != null)
13                 {
14                     worker.DoActionOnNode(element);
15                     var left = element.Left;
16                     var right = element.Right;
17                     if (left != null) { queue.Enqueue(left); }
18                     if (right != null) { queue.Enqueue(right); }
19                 }
20             }
21         }

总结

学习了线索二叉树后,我对算法思维有了新的认识。在想一个子问题的时候,以不同的角度思考,得到不同的结论,将其连接起来,竟然能够得到整个问题的解。

重拾算法(2)——线索二叉树

时间: 2024-10-10 00:31:45

重拾算法(2)——线索二叉树的相关文章

重拾算法(1)——优雅地非递归遍历二叉树及其它

重拾算法(1)——优雅地非递归遍历二叉树及其它 本文中非递归遍历二叉树的思想和代码都来自这里(http://jianshu.io/p/49c8cfd07410#).我认为其思想和代码都足够优雅动人了,于是稍作整理,得到如下的程序. 前中后序遍历二叉树 1 public class BinaryTreeNode<T> 2 { 3 public T Value { get;set; } 4 public BinaryTreeNode<T> Parent { get;set; } 5 p

重拾算法(0)——目录

现在到了重拾基础算法,掌握算法思维的时候.暂定要学习的算法如下表. 1 算法 2 KMP 3 树 4 遍历二叉树 5 线索二叉树 6 霍夫曼树 7 图 8 深度优先搜索 9 广度优先搜索 10 最小生成树 11 最短路径 12 拓扑排序 13 关键路径 14 查找 15 线性表的查找 16 折半查找 17 树的查找 18 二叉排序树 19 平衡二叉树 20 B-树 21 B+树 22 散列表的查找 23 构造方法 24 处理冲突的方法 25 查找 26 排序 27 插入排序 28 直接插入排序

重拾算法(5)——最小生成树的两种算法及其对比测试

重拾算法(5)——最小生成树的两种算法及其对比测试 什么是最小生成树 求解最小生成树(Minimum Cost Spanning Tree,以下简写做MST)是图相关的算法中常见的一个,用于解决类似如下的问题: 假设要在N个城市之间建立通信联络网,那么连通N个城市只需N-1条线路.这时自然会考虑这样一个问题:如何在最节省经费的前提下建立这个通信网. 在任意两个城市间都可以设置一条线路,相应地都要付出一定的经济代价.N个城市之间最多可能设置N(N-1)/2条线路,那么如何在这些线路中选择N-1条,

重拾算法之路——前言

话说,自从大二下学期从ACM集训队退出后, 对于算法, 真的放下了很多. 曾经说过,离开不是句号, 现在的情况,越来越趋近叹号了 o(╯□╰)o.. 其实,算法的用处,对于目前状态的我, 真的非常非常...不重要 ( ⊙ o ⊙ )!, 在做项目的时候,会经历这样的过程: ①算法NB论,但自己只是知道NB,用不上 ②算法无用论,做了一段时间项目,发现算法这东西,在我们项目中根本用不上,要么就是包装好的东西,直接调用过来,无须我们担心. ③算法还是很NB,刚开始接触的项目,只是初步应用,大多处于初

算法学习 - 线索二叉树

线索二叉树 线索二叉树就是在通用的二叉树里多了点东西,多了什么呢? 前驱和后继,把二叉树变成一个链式的结构.解释下:通常我们的二叉树里,叶子节点是没有孩子,所以指向空也就是NULL,在线索二叉树里,叶子节点的左右孩子分别指向它自己的前驱和后继,而前驱和后继是哪个节点呢? 就是树遍历过程的前一个节点和后一个节点.所以第一个遍历的节点是没有前驱的,最后一个节点是没有后继的.这里一般都是中序线索二叉树,当然也有先序线索二叉树和后序线索二叉树. []a[] / []b[] []c[] / \ []d[]

重拾算法之路——二分搜索

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 隶属于--递归与分治 描述: 给定 已排好序 的n个元素a[0;n-1],现在要在这n个元素中找出一特定元素x. 朴素法: 当然这是好听的说法,明白点叫最笨的方法,就是顺序搜索,逐个比较0到n-1中元素,直至找出元素x或搜索遍所有元素,确定x不在元素中,这个方法

重拾算法之路——递归与分治基础

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 这个周末家里有点事,回了趟家,就断了一些学习计划.. 抓紧补上! 第一个算法--递归与分治 都知道,分治算法基本思想是 将一个难以直接解决的问题,分割成一些规模小的相同问题,以便各个击破,分而治之, 这样,我们就可以将一个复杂的算法,类型不变,规模越来越小,最终

重拾算法之路——线性时间选择

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 第一章:分治与递归 线性时间选择 算法描述: 给定线性序集中n个元素和一个整数k,1 ≤ k ≤ n,要求找出这n个元素中第k小的元素.即如果将这n个元素依其线性序排列时,排在第k个位置的元素即为要找的元素.当k=1时,就是找最小元素,k=n时,就是找最大元素,

重拾算法之路——算法概述及NP完全性理论

***************************************转载请注明出处:http://blog.csdn.net/lttree******************************************** 我的这系列文章参考的书是: 王晓东 的 <计算机算法设计与分析>(第4版) 首先,当然是第一章节的概述,这些东西简要说一下概念: 1.算法与程序的区别? --算法 指 解决问题的一种方法 或 一个过程 (严格的讲,算法是由若干条指令组成的有穷序列) --程序