算法系列15天速成——第十二天 树操作【中】

原文:算法系列15天速成——第十二天 树操作【中】

先前说了树的基本操作,我们采用的是二叉链表来保存树形结构,当然二叉有二叉的困扰之处,比如我想找到当前结点

的“前驱”和“后继”,那么我们就必须要遍历一下树,然后才能定位到该“节点”的“前驱”和“后继”,每次定位都是O(n),这

不是我们想看到的,那么有什么办法来解决呢?

(1) 在节点域中增加二个指针域,分别保存“前驱”和“后继”,那么就是四叉链表了,哈哈,还是有点浪费空间啊。

(2) 看下面的这个二叉树,我们知道每个结点有2个指针域,4个节点就有8个指针域,其实真正保存节点的指针

仅有3个,还有5个是空闲的,那么为什么我们不用那些空闲的指针域呢,达到资源的合理充分的利用。

一: 线索二叉树

1  概念

刚才所说的在空闲的指针域里面存放“前驱”和“后继”就是所谓的线索。

<1>  左线索:   在空闲的左指针域中存放该“结点”的“前驱”被认为是左线索。

<2>  右线索:   在空闲的右指针域中存放该“结点“的”后继“被认为是右线索。

当“二叉链表”被套上这种线索,就被认为是线索链表,当“二叉树”被套上这种线索就被认为是线索二叉树,当然线索根据

二叉树的遍历形式不同被分为“先序线索”,“中序线索”,“后序线索”。

2  结构图

说了这么多,我们还是上图说话,就拿下面的二叉树,我们构建一个中序线索二叉树,需要多动动脑子哟。

<1> 首先要找到“中序遍历”中的首结点D,因为“D结点”是首节点,所以不存在“前驱”,左指针自然是空,

”D节点”的右指针存放的是“后继”,那么根据“中序遍历”的规则应该是B,所以D的右指针存放着B节点。

<2>  接着就是“B节点”,他的左指针不为空,所以就不管了,但是他的“右指针”空闲,根据规则“B结点“的右

指针存放的是"A结点“。

<3>  然后就是“A节点”,他已经被塞的满满的,所以就没有“线索”可言了。

<4>  最后就是“C节点”,根据规则,他的“左指针”存放着就是“A节点“,”C节点“是最后一个节点,右指针自然就是空的,你懂的。

3 基本操作

常用的操作一般有“创建线索二叉树”,”查找后继节点“,”查找前驱节点“,”遍历线索二叉树“,下面的操作我们就以”中序遍历“

来创建中序线索二叉树。

<1>  线索二叉树结构

从“结构图”中可以看到,现在结点的指针域中要么是”子节点(SubTree)“或者是”线索(Thread)“,此时就要设立标志位来表示指针域

存放的是哪一种。

 1     #region 节点标识(用于判断孩子是节点还是线索) 2     /// <summary> 3 /// 节点标识(用于判断孩子是节点还是线索) 4 /// </summary> 5     public enum NodeFlag 6     { 7         SubTree = 1, 8         Thread = 2 9     }10     #endregion11 12     #region 线索二叉树的结构13     /// <summary>14 /// 线索二叉树的结构15 /// </summary>16 /// <typeparam name="T"></typeparam>17     public class ThreadTree<T>18     {19         public T data;20         public ThreadTree<T> left;21         public ThreadTree<T> right;22         public NodeFlag leftFlag;23         public NodeFlag rightFlag;24     }25     #endregion

<2>  创建线索二叉树

刚才也说了如何构建中序线索二叉树,在代码实现中,我们需要定义一个节点来保存当前节点的前驱,我练习的时候迫不得已,只能使用两个

ref来实现地址操作,达到一个Tree能够让两个变量来操作。

 1 #region 中序遍历构建线索二叉树 2         /// <summary> 3 /// 中序遍历构建线索二叉树 4 /// </summary> 5 /// <typeparam name="T"></typeparam> 6 /// <param name="tree"></param> 7         public void BinTreeThreadingCreate_LDR<T>(ref ThreadTree<T> tree, ref ThreadTree<T> prevNode) 8         { 9             if (tree == null)10                 return;11 12             //先左子树遍历,寻找起始点13             BinTreeThreadingCreate_LDR(ref tree.left, ref prevNode);14 15             //如果left为空,则说明该节点可以放“线索”16             tree.leftFlag = (tree.left == null) ? NodeFlag.Thread : NodeFlag.SubTree;17 18             //如果right为空,则说明该节点可以放“线索”19             tree.rightFlag = (tree.right == null) ? NodeFlag.Thread : NodeFlag.SubTree;20 21             if (prevNode != null)22             {23                 if (tree.leftFlag == NodeFlag.Thread)24                     tree.left = prevNode;25                 if (prevNode.rightFlag == NodeFlag.Thread)26                     prevNode.right = tree;27             }28 29             //保存前驱节点30             prevNode = tree;31 32             BinTreeThreadingCreate_LDR(ref tree.right, ref prevNode);33         }34         #endregion

<3> 查找后继结点

现在大家都知道,后继结点都是保存在“结点“的右指针域中,那么就存在”两种情况“。

《1》 拿“B节点“来说,他没有右孩子,则肯定存放着线索(Thread),所以我们直接O(1)的返回他的线索即可。

《2》 拿“A节点”来说,他有右孩子,即右指针域存放的是SubTree,悲哀啊,如何才能得到“A节点“的后继呢?其实也很简单,

根据”中序“的定义,”A节点“的后继必定是”A节点“的右子树往左链找的第一个没有左孩子的节点(只可意会,不可言传,嘻嘻)。

 1 #region 查找指定节点的后继 2         /// <summary> 3 /// 查找指定节点的后继 4 /// </summary> 5 /// <typeparam name="T"></typeparam> 6 /// <param name="tree"></param> 7         public ThreadTree<T> BinTreeThreadNext_LDR<T>(ThreadTree<T> tree) 8         { 9             if (tree == null)10                 return null;11 12             //如果查找节点的标志域中是Thread,则直接获取13             if (tree.rightFlag == NodeFlag.Thread)14                 return tree.right;15             else16             {17                 //根据中序遍历的规则是寻找右子树中中序遍历的第一个节点18                 var rightNode = tree.right;19 20                 //如果该节点是subTree就需要循环遍历21                 while (rightNode.leftFlag == NodeFlag.SubTree)22                 {23                     rightNode = rightNode.left;24                 }25                 return rightNode;26             }27         }28         #endregion

<4> 查找前驱节点

这个跟(3)的操作很类似,同样也具有两个情况。

《1》  拿“C结点”来说,他没有“左子树”,则说明“C节点”的左指针为Thread,此时,我们只要返回左指针域即可得到前驱结点。

《2》  拿"A节点“来说,他有”左子树“,则说明”A节点“的左指针为SubTree,那么怎么找的到”A节点“的前驱呢?同样啊,根据

”中序遍历“的性质,我们可以得知在”A节点“的左子树中往”右链“中找到第一个没有”右孩子“的节点。

 1 #region 查找指定节点的前驱 2         /// <summary> 3 /// 查找指定节点的前驱 4 /// </summary> 5 /// <typeparam name="T"></typeparam> 6 /// <param name="tree"></param> 7 /// <returns></returns> 8         public ThreadTree<T> BinTreeThreadPrev_LDR<T>(ThreadTree<T> tree) 9         {10             if (tree == null)11                 return null;12 13             //如果标志域中存放的是线索,那么可以直接找出来14             if (tree.leftFlag == NodeFlag.Thread)15                 return tree.left;16             else17             {18                 //根据”中序“的规则可知,如果不为Thread,则要找出左子树的最后节点19 //也就是左子树中最后输出的元素20                 var leftNode = tree.left;21 22                 while (leftNode.rightFlag == NodeFlag.SubTree)23                     leftNode = leftNode.right;24 25                 return leftNode;26             }27         }28         #endregion

<5> 遍历线索二叉树

因为我们构建线索的时候采用的是“中序”,那么我们遍历同样采用“中序”,大家是否看到了“线索”的好处,此时我们找某个节点的时间复杂度变为了

O(1) ~0(n)的时间段,比不是线索的时候查找“前驱"和“后继”效率要高很多。

 1 #region 遍历线索二叉树 2         /// <summary> 3 /// 遍历线索二叉树 4 /// </summary> 5 /// <typeparam name="T"></typeparam> 6 /// <param name="tree"></param> 7         public void BinTreeThread_LDR<T>(ThreadTree<T> tree) 8         { 9             if (tree == null)10                 return;11 12             while (tree.leftFlag == NodeFlag.SubTree)13                 tree = tree.left;14 15             do16             {17                 Console.Write(tree.data + "\t");18 19                 tree = BinTreeThreadNext_LDR(tree);20 21             } while (tree != null);22 23         }24         #endregion

最后上一下总的运行代码

  1 using System;  2 using System.Collections.Generic;  3 using System.Linq;  4 using System.Text;  5   6 namespace ThreadChainTree  7 {  8     class Program  9     { 10         static void Main(string[] args) 11         { 12             ThreadTreeManager manager = new ThreadTreeManager(); 13  14             //生成根节点 15             ThreadTree<string> tree = CreateRoot(); 16  17             //生成节点 18             AddNode(tree); 19  20             ThreadTree<string> prevNode = null; 21  22             //构建线索二叉树 23             manager.BinTreeThreadingCreate_LDR(ref tree, ref prevNode); 24  25             Console.WriteLine("\n线索二叉树的遍历结果为:\n"); 26             //中序遍历线索二叉树 27             manager.BinTreeThread_LDR(tree); 28         } 29  30         #region 生成根节点 31         /// <summary> 32 /// 生成根节点 33 /// </summary> 34 /// <returns></returns> 35         static ThreadTree<string> CreateRoot() 36         { 37             ThreadTree<string> tree = new ThreadTree<string>(); 38  39             Console.WriteLine("请输入根节点,方便我们生成树\n"); 40  41             tree.data = Console.ReadLine(); 42  43             Console.WriteLine("根节点生成已经生成\n"); 44  45             return tree; 46         } 47         #endregion 48  49         #region 插入节点操作 50         /// <summary> 51 /// 插入节点操作 52 /// </summary> 53 /// <param name="tree"></param> 54         static ThreadTree<string> AddNode(ThreadTree<string> tree) 55         { 56             ThreadTreeManager mananger = new ThreadTreeManager(); 57  58             while (true) 59             { 60                 ThreadTree<string> node = new ThreadTree<string>(); 61  62                 Console.WriteLine("请输入要插入节点的数据:\n"); 63  64                 node.data = Console.ReadLine(); 65  66                 Console.WriteLine("请输入要查找的父节点数据:\n"); 67  68                 var parentData = Console.ReadLine(); 69  70                 if (tree == null) 71                 { 72                     Console.WriteLine("未找到您输入的父节点,请重新输入。"); 73                     continue; 74                 } 75  76                 Console.WriteLine("请确定要插入到父节点的:1 左侧,2 右侧"); 77  78                 Direction direction = (Direction)Enum.Parse(typeof(Direction), Console.ReadLine()); 79  80                 tree = mananger.BinTreeThreadAddNode(tree, node, parentData, direction); 81  82                 Console.WriteLine("插入成功,是否继续?  1 继续, 2 退出"); 83  84                 if (int.Parse(Console.ReadLine()) == 1) 85                     continue; 86                 else 87                     break; 88             } 89  90             return tree; 91         } 92         #endregion 93     } 94  95     #region 节点标识(用于判断孩子是节点还是线索) 96     /// <summary> 97 /// 节点标识(用于判断孩子是节点还是线索) 98 /// </summary> 99     public enum NodeFlag100     {101         SubTree = 1,102         Thread = 2103     }104     #endregion105 106     #region 线索二叉树的结构107     /// <summary>108 /// 线索二叉树的结构109 /// </summary>110 /// <typeparam name="T"></typeparam>111     public class ThreadTree<T>112     {113         public T data;114         public ThreadTree<T> left;115         public ThreadTree<T> right;116         public NodeFlag leftFlag;117         public NodeFlag rightFlag;118     }119     #endregion120 121     #region 插入左节点或者右节点122     /// <summary>123 /// 插入左节点或者右节点124 /// </summary>125     public enum Direction { Left = 1, Right = 2 }126     #endregion127 128     #region 线索二叉树的基本操作129     /// <summary>130 /// 线索二叉树的基本操作131 /// </summary>132     public class ThreadTreeManager133     {134         #region 将指定节点插入到二叉树中135         /// <summary>136 /// 将指定节点插入到二叉树中137 /// </summary>138 /// <typeparam name="T"></typeparam>139 /// <param name="tree"></param>140 /// <param name="node"></param>141 /// <param name="direction">插入做左是右</param>142 /// <returns></returns>143         public ThreadTree<T> BinTreeThreadAddNode<T>(ThreadTree<T> tree, ThreadTree<T> node, T data, Direction direction)144         {145             if (tree == null)146                 return null;147 148             if (tree.data.Equals(data))149             {150                 switch (direction)151                 {152                     case Direction.Left:153                         if (tree.left != null)154                             throw new Exception("树的左节点不为空,不能插入");155                         else156                             tree.left = node;157 158                         break;159                     case Direction.Right:160                         if (tree.right != null)161                             throw new Exception("树的右节点不为空,不能插入");162                         else163                             tree.right = node;164 165                         break;166                 }167             }168 169             BinTreeThreadAddNode(tree.left, node, data, direction);170             BinTreeThreadAddNode(tree.right, node, data, direction);171 172             return tree;173         }174         #endregion175 176         #region 中序遍历构建线索二叉树177         /// <summary>178 /// 中序遍历构建线索二叉树179 /// </summary>180 /// <typeparam name="T"></typeparam>181 /// <param name="tree"></param>182         public void BinTreeThreadingCreate_LDR<T>(ref ThreadTree<T> tree, ref ThreadTree<T> prevNode)183         {184             if (tree == null)185                 return;186 187             //先左子树遍历,寻找起始点188             BinTreeThreadingCreate_LDR(ref tree.left, ref prevNode);189 190             //如果left为空,则说明该节点可以放“线索”191             tree.leftFlag = (tree.left == null) ? NodeFlag.Thread : NodeFlag.SubTree;192 193             //如果right为空,则说明该节点可以放“线索”194             tree.rightFlag = (tree.right == null) ? NodeFlag.Thread : NodeFlag.SubTree;195 196             if (prevNode != null)197             {198                 if (tree.leftFlag == NodeFlag.Thread)199                     tree.left = prevNode;200                 if (prevNode.rightFlag == NodeFlag.Thread)201                     prevNode.right = tree;202             }203 204             //保存前驱节点205             prevNode = tree;206 207             BinTreeThreadingCreate_LDR(ref tree.right, ref prevNode);208         }209         #endregion210 211         #region 查找指定节点的后继212         /// <summary>213 /// 查找指定节点的后继214 /// </summary>215 /// <typeparam name="T"></typeparam>216 /// <param name="tree"></param>217         public ThreadTree<T> BinTreeThreadNext_LDR<T>(ThreadTree<T> tree)218         {219             if (tree == null)220                 return null;221 222             //如果查找节点的标志域中是Thread,则直接获取223             if (tree.rightFlag == NodeFlag.Thread)224                 return tree.right;225             else226             {227                 //根据中序遍历的规则是寻找右子树中中序遍历的第一个节点228                 var rightNode = tree.right;229 230                 //如果该节点是subTree就需要循环遍历231                 while (rightNode.leftFlag == NodeFlag.SubTree)232                 {233                     rightNode = rightNode.left;234                 }235                 return rightNode;236             }237         }238         #endregion239 240         #region 查找指定节点的前驱241         /// <summary>242 /// 查找指定节点的前驱243 /// </summary>244 /// <typeparam name="T"></typeparam>245 /// <param name="tree"></param>246 /// <returns></returns>247         public ThreadTree<T> BinTreeThreadPrev_LDR<T>(ThreadTree<T> tree)248         {249             if (tree == null)250                 return null;251 252             //如果标志域中存放的是线索,那么可以直接找出来253             if (tree.leftFlag == NodeFlag.Thread)254                 return tree.left;255             else256             {257                 //根据”中序“的规则可知,如果不为Thread,则要找出左子树的最后节点258 //也就是左子树中最后输出的元素259                 var leftNode = tree.left;260 261                 while (leftNode.rightFlag == NodeFlag.SubTree)262                     leftNode = leftNode.right;263 264                 return leftNode;265             }266         }267         #endregion268 269         #region 遍历线索二叉树270         /// <summary>271 /// 遍历线索二叉树272 /// </summary>273 /// <typeparam name="T"></typeparam>274 /// <param name="tree"></param>275         public void BinTreeThread_LDR<T>(ThreadTree<T> tree)276         {277             if (tree == null)278                 return;279 280             while (tree.leftFlag == NodeFlag.SubTree)281                 tree = tree.left;282 283             do284             {285                 Console.Write(tree.data + "\t");286 287                 tree = BinTreeThreadNext_LDR(tree);288 289             } while (tree != null);290 291         }292         #endregion293     }294     #endregion295 }

将文章开头处的数据输入到存储结构中

时间: 2024-10-10 18:03:01

算法系列15天速成——第十二天 树操作【中】的相关文章

算法系列15天速成——第十一天 树操作(上)

原文:算法系列15天速成--第十一天 树操作(上) 最近项目赶的紧,歇了一个星期没写博客了,趁周末继续写这个系列. 先前我们讲的都是“线性结构”,他的特征就是“一个节点最多有一个”前驱“和一个”后继“.那么我们今天讲的树会是怎样的呢? 我们可以对”线性结构“改造一下,变为”一个节点最多有一个"前驱“和”多个后继“.哈哈,这就是我们今天说的”树“. 一: 树 我们思维中的”树“就是一种枝繁叶茂的形象,那么数据结构中的”树“该是怎么样呢?对的,他是一种现实中倒立的树. 1:术语 其实树中有很多术语的

算法系列15天速成——第六天 五大经典查找【下】

原文:算法系列15天速成--第六天 五大经典查找[下] 大家是否感觉到,树在数据结构中大行其道,什么领域都要沾一沾,碰一碰. 就拿我们前几天学过的排序就用到了堆和今天讲的”二叉排序树“,所以偏激的说,掌握的树你就是牛人了. 今天就聊聊这个”五大经典查找“中的最后一个”二叉排序树“. 1. 概念: <1> 其实很简单,若根节点有左子树,则左子树的所有节点都比根节点小. 若根节点有右子树,则右子树的所有节点都比根节点大. <2> 如图就是一个”二叉排序树“,然后对照概念一比较比较. 2

算法系列15天速成——第三天 七大经典排序【下】

原文:算法系列15天速成--第三天 七大经典排序[下] 今天跟大家聊聊最后三种排序: 直接插入排序,希尔排序和归并排序. 直接插入排序: 这种排序其实蛮好理解的,很现实的例子就是俺们斗地主,当我们抓到一手乱牌时,我们就要按照大小梳理扑克,30秒后, 扑克梳理完毕,4条3,5条s,哇塞......  回忆一下,俺们当时是怎么梳理的. 最左一张牌是3,第二张牌是5,第三张牌又是3,赶紧插到第一张牌后面去,第四张牌又是3,大喜,赶紧插到第二张后面去, 第五张牌又是3,狂喜,哈哈,一门炮就这样产生了.

算法系列15天速成——第四天 五大经典查找【上】

原文:算法系列15天速成--第四天 五大经典查找[上] 在我们的生活中,无处不存在着查找,比如找一下班里哪个mm最pl,猜一猜mm的芳龄....... 对的这些都是查找. 在我们的算法中,有一种叫做线性查找. 分为:顺序查找. 折半查找. 查找有两种形态: 分为:破坏性查找,   比如有一群mm,我猜她们的年龄,第一位猜到了是23+,此时这位mm已经从我脑海里面的mmlist中remove掉了. 哥不找23+的,所以此种查找破坏了原来的结构. 非破坏性查找, 这种就反之了,不破坏结构. 顺序查找

算法系列15天速成——第一天 七大经典排序【上】

原文:算法系列15天速成--第一天 七大经典排序[上] 今天是开篇,得要吹一下算法,算法就好比程序开发中的利剑,所到之处,刀起头落. 针对现实中的排序问题,算法有七把利剑可以助你马道成功. 首先排序分为四种: 交换排序: 包括冒泡排序,快速排序. 选择排序: 包括直接选择排序,堆排序. 插入排序: 包括直接插入排序,希尔排序. 合并排序: 合并排序. 那么今天我们讲的就是交换排序,我们都知道,C#类库提供的排序是快排,为了让今天玩的有意思点, 我们设计算法来跟类库提供的快排较量较量.争取KO对手

算法系列15天速成——第七天 线性表【上】

原文:算法系列15天速成--第七天 线性表[上] 人活在社会上不可能孤立,比如跟美女有着千丝万缕的关系,有的是一对一,有的是一对多,有的是多对多. 哈哈,我们的数据也一样,存在这三种基本关系,用术语来说就是: <1>  线性关系. <2>  树形关系. <3>  网状关系. 一: 线性表 1 概念: 线性表也就是关系户中最简单的一种关系,一对一. 如:学生学号的集合就是一个线性表. 2 特征: ① 有且只有一个“首元素“. ② 有且只有一个“末元素”. ③ 除“末元素”

算法系列15天速成——第十天 栈

原文:算法系列15天速成--第十天 栈 今天跟大家聊聊栈,在程序设计中,栈的使用还是非常广泛的,比如有“括号匹配问题“,”html结构匹配问题“. 所以说掌握了”栈“的使用,对我们学习算法还是很有帮助的. 一: 概念 栈,同样是一种特殊的线性表,是一种Last In First Out(LIFO)的形式,现实中有很多这样的例子, 比如:食堂中的一叠盘子,我们只能从顶端一个一个的取. 二:存储结构 ”栈“不像”队列“,需要两个指针来维护,栈只需要一个指针就够了,这得益于栈是一种一端受限的线性表.

算法系列15天速成——第五天 五大经典查找【中】

原文:算法系列15天速成--第五天 五大经典查找[中] 大家可否知道,其实查找中有一种O(1)的查找,即所谓的秒杀. 哈希查找: 对的,他就是哈希查找,说到哈希,大家肯定要提到哈希函数,呵呵,这东西已经在我们脑子里面形成 固有思维了.大家一定要知道“哈希“中的对应关系. 比如说: ”5“是一个要保存的数,然后我丢给哈希函数,哈希函数给我返回一个”2",那么此时的”5“ 和“2”就建立一种对应关系,这种关系就是所谓的“哈希关系”,在实际应用中也就形成了”2“是key,”5“是value. 那么有的

算法系列15天速成——第二天 七大经典排序【中】

原文:算法系列15天速成--第二天 七大经典排序[中] 首先感谢朋友们对第一篇文章的鼎力支持,感动中.......  今天说的是选择排序,包括“直接选择排序”和“堆排序”. 话说上次“冒泡排序”被快排虐了,而且“快排”赢得了内库的重用,众兄弟自然眼红,非要找快排一比高下. 这不今天就来了两兄弟找快排算账. 1.直接选择排序: 先上图: 说实话,直接选择排序最类似于人的本能思想,比如把大小不一的玩具让三岁小毛孩对大小排个序, 那小孩首先会在这么多玩具中找到最小的放在第一位,然后找到次小的放在第二位