【C#数据结构系列】树和二叉树

  线性结构中的数据元素是一对一的关系,树形结构是一对多的非线性结构,很是相似于天然界中的树,数据元素之间既有分支关系,又有层次关系。树形结构在现实世界中普遍存在,如家族的家谱、一个单位的行政机构组织等均可以用树形结构来形象地表示。树形结构在计算机领域中也有着很是普遍的应用,如 Windows 操做系统中对磁盘文件的管理、编译程序中对源程序的语法结构的表示等都采用树形结构。在数据库系统中,树形结构也是数据的重要组织形式之一。树形结构有树和二叉树两种,树的操做实现比较复杂,但树能够转换为二叉树进行处理,因此,咱们主要讨论二叉树。node

一:树

1.1 树的定义

  树(Tree)是 n(n≥0)个相同类型的数据元素的有限集合。树中的数据元素叫结点(Node)。n=0 的树称为空树(Empty Tree);对于 n>0 的任意非空树 T 有:算法

  (1)有且仅有一个特殊的结点称为树的根(Root)结点,根没有前驱结点;  数据库

  (2)若n>1,则除根结点外,其他结点被分红了m(m>0)个互不相交的集合T 1 ,T 2 ,…,T m ,其中每个集合T i (1≤i≤m)自己又是一棵树。树T 1 ,T 2 ,…,T m数组

称为这棵树的子树(Subtree)。数据结构

  由树的定义可知,树的定义是递归的,用树来定义树。所以,树(以及二叉树)的许多算法都使用了递归ide

  树的形式定义为:树(Tree)简记为 T,是一个二元组,
          T = (D, R)
  其中:D 是结点的有限集合;
  R 是结点之间关系的有限集合。优化

                          图 1.1  动画

  从树的定义和上图的示例能够看出,树具备下面两个特色:  this

  (1)树的根结点没有前驱结点,除根结点以外的全部结点有且只有一个前驱结点。编码

  (2)树中的全部结点均可以有零个或多个后继结点。

  实际上,第(1)个特色表示的就是树形结构的“一对多关系”中的“一”,第(2)特色表示的是“多”。

  由此特色可知,下图 所示的都不是树。

1.2 树的相关术语

一、结点(Node):表示树中的数据元素,由数据项和数据元素之间的关系组成。在图 1.1中,共有 10 个结点。

二、结点的度(Degree of Node):结点所拥有的子树的个数,在图 1.1 中,结点 A 的度为 3。

三、树的度(Degree of Tree):树中各结点度的最大值。在图 1.1 中,树的度为3。

四、叶子结点(Leaf Node):度为 0 的结点,也叫终端结点。在图 1.1 中,结点 E、F、G、H、I、J 都是叶子结点。

五、分支结点(Branch Node):度不为 0 的结点,也叫非终端结点或内部结点。在图 1.1 中,结点 A、B、C、D 是分支结点。

六、孩子(Child):结点子树的根。在图 1.1 中,结点 B、C、D 是结点 A 的孩子。

七、双亲(Parent):结点的上层结点叫该结点的双亲。在图 1.1 中,结点 B、C、D 的双亲是结点 A。

八、祖先(Ancestor):从根到该结点所经分支上的全部结点。在图 1.1 中,结点 E 的祖先是 A 和 B。

九、子孙(Descendant):以某结点为根的子树中的任一结点。在图 1.1 中,除A 以外的全部结点都是 A 的子孙。

十、兄弟(Brother):同一双亲的孩子。在图 1.1 中,结点 B、C、D 互为兄弟。

十一、结点的层次(Level of Node):从根结点到树中某结点所经路径上的分支数称为该结点的层次。根结点的层次规定为 1,其他结点的层次等于其双亲结点的层次加 1。

十二、堂兄弟(Sibling):同一层的双亲不一样的结点。在图 1.1 中,G 和 H 互为堂兄弟。

1三、树的深度(Depth of Tree):树中结点的最大层次数。在图 1.1 中,树的深度为 3。

1四、无序树(Unordered Tree):树中任意一个结点的各孩子结点之间的次序构成可有可无的树。一般树指无序树。

1五、有序树(Ordered Tree):树中任意一个结点的各孩子结点有严格排列次序的树。二叉树是有序树,由于二叉树中每一个孩子结点都确切定义为是该结点的左孩子结点仍是右孩子结点。

1六、森林(Forest):m(m≥0)棵树的集合。天然界中的树和森林的概念差异很大,但在数据结构中树和森林的概念差异很小。从定义可知,一棵树有根结点和m 个子树构成,若把树的根结点删除,则树变成了包含 m 棵树的森林。固然,根据定义,一棵树也能够称为森林。

 

1.3 树的逻辑表示

树的逻辑表示方法不少,下面是常见的表示方法。
一、直观表示法

它象平常生活中的树木同样。整个图就象一棵倒立的树,从根结点出发不断扩展,根结点在最上层,叶子结点在最下面,如图 1.1 所示。

二、凹入表示法
每一个结点对应一个矩形,全部结点的矩形都右对齐,根结点用最长的矩形表示,同一层的结点的矩形长度相同,层次越高,矩形长度越短,图 1.1 中的树的凹入表示法以下

三、广义表表示法

用广义表的形式表示根结点排在最前面,用一对圆括号把它的子树结点括起来,子树结点用逗号隔开。图 1.1 的树的广义表表示以下:

          (A(B(E,F,G),C(H),D(I,J)))

四、嵌套表示法

相似数学中所说的文氏图表示法,以下图 所示。

 

二:二叉树

2.1 二叉树的定义

  二叉树(Binary Tree)是 n(n≥0)个相同类型的结点的有限集合。n=0 的二叉树称为空二叉树(Empty Binary Tree);对于 n>0 的任意非空二叉树有:

  (1)有且仅有一个特殊的结点称为二叉树的根(Root)结点,根没有前驱结点;

  (2)若n>1,则除根结点外,其他结点被分红了 2 个互不相交的集合T L ,T R ,而T L 、T R 自己又是一棵二叉树,分别称为这棵二叉树的左子树(Left Subtree)和右子树(Right Subtree)。

  二叉树的形式定义为:二叉树(Binary Tree)简记为 BT,是一个二元组,

              BT = (D, R)

  其中:D 是结点的有限集合;
     R 是结点之间关系的有限集合。

 

  由树的定义可知,二叉树是另一种树形结构,而且是有序树,它的左子树和右子树有严格的次序,若将其左、右子树颠倒,就成为另一棵不一样的二叉树。所以,图 (a)和图 (b)所示是不一样的二叉树

  

  二叉树的形态共有 5 种:空二叉树、只有根结点的二叉树、右子树为空的二叉树、左子树为空的二叉树和左、右子树非空的二叉树。二叉树的 5 种形态以下图所示。

 

 

 

  三种特殊的二叉树:

  (1)完美二叉树(Perfect Binary Tree):Every node except the leaf nodes have two children and every level (last level too) is completely filled. 除了叶子结点以外的每个结点都有两个孩子,每一层(固然包含最后一层)都被彻底填充。

 

  (2)彻底二叉树(Complete Binary Tree):Every level except the last level is completely filled and all the nodes are left justified. 除了最后一层以外的其余每一层都被彻底填充,而且全部结点都保持向左对齐。

                       (若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层全部的结点都连续集中在最左边,这就是彻底二叉树。【来源百度百科】)

    这是一种有些难以理解的特殊二叉树,首先从字面上要区分,“彻底”和“满”的差别,满二叉树必定是一棵彻底二叉树,但彻底二叉树不必定是满的。

 

   (3)完满二叉树(Full Binary Tree):Every node except the leaf nodes have two children. 除了叶子结点以外的每个结点都有两个孩子结点。

                  

 

 

    完满(Full)二叉树 v.s. 彻底(Complete)二叉树 v.s. 完美(Perfect)二叉树

    

 

 

    

2.2 二叉树的特性

  性质 1 :版本一:若二叉树的层次从0开始,则在二叉树的第i层至多有2^i个结点(i>=0)。【Thomas和Charles等人写的《算法导论》和 Robert Sedgewick所著的《算法》从 level 0 开始定义】

      版本二:若二叉树的层次从1开始,则在二叉树的第i层至多有2^(i-1)个结点(i>=1)。【严蔚敏老师的《数据结构》则是从level 1开始定义的】

  性质 2: 若规定空树的深度为 0,则深度为k的二叉树最多有 2^k -1 个结点(满二叉树)(k≥0)。

     性质 3 :具备n个结点的彻底二叉树的深度k为log 2 n+1。

  性质 4: 对于一棵非空二叉树,若是叶子结点(度为0)数目为m ,度为 2 的结点数目为n,则有m= n +1。

  性质 5: 对于具备 n 个结点的彻底二叉树,若是按照从上到下和从左到右的顺序对全部结点从 1 开始编号,则对于序号为 i 的结点,有:

  (1)若是 i>1,则序号为 i 的结点的双亲结点的序号为 i/2(“/”表示整除);若是 i=1,则该结点是根结点,无双亲结点。

  (2)若是 2i≤n,则该结点的左孩子结点的序号为 2i;若 2i>n,则该结点无左孩子。  

  (3)若是 2i+1≤n,则该结点的右孩子结点的序号为 2i+1;若 2i+1>n,则该结点无右孩子

 

2.3 二叉树的存储结构

  二叉树的存储结构主要有三种:顺序存储结构、二叉链表存储结构和三叉链表存储结构

  2.3.1:二叉树的顺序存储结构

  对于一棵彻底二叉树,由性质 5 可计算获得任意结点 i 的双亲结点序号、左孩子结点序号和右孩子结点序号。因此,彻底二叉树的结点可按从上到下和从左到右的顺序存储在一维数组中,其结点间的关系可由性质 5 计算获得,这就是二叉树的顺序存储结构。图 (a)所示的二叉树的顺序存储结构为:

  

  可是,对于一棵非彻底二叉树,不能简单地按照从上到下和从左到右的顺序存放在一维数组中,由于数组下标之间的关系不能反映二叉树中结点之间的逻辑关系。因此,应该对一棵非彻底二叉树进行改造,增长空结点(并不存在的结点)使之成为一棵彻底二叉树,而后顺序存储在一维数组中。图 (b)是图 (a)的顺序存储示意图

  

  显然,顺序存储对于需增长不少空结点才能改造为一棵彻底二叉树的二叉树不适合,由于会形成空间的大量浪费。实际上,采用顺序存储结构,是对非线性的数据结构线性化,用线性结构来表示二叉树的结点之间的逻辑关系,因此,须要增长空间。通常来讲,有大约一半的空间被浪费。最差的状况是右单支树,以下图 所示,一棵深度为k的右单支树,只有k个结点,却须要分配 2 k -1 个存储单元。

  

  2.3.2:二叉树的二叉链表存储结构

  二叉树的二叉链表存储结构是指二叉树的结点有三个域:一个数据域和两个引用域,数据域存储数据,两个引用域分别存放其左、右孩子结点的地址。当左孩子或右孩子不存在时,相应域为空,用符号 NULL 或∧表示。结点的存储结构以下所示:

  

  

  下图是图2.3.1(a)所示的二叉树的二叉链表示意图。图 (a)是不带头结点的二叉链表,图 (b)是带头结点的二叉链表。

  

  由上图所示的二叉树有 4 个结点,每一个结点中有两个引用,共有 8 个引用,其中 3 个引用被使用,5 个引用是空的。由性质 4 可知:由 n 个结点构成的二叉链表中,只有 n-1 个引用域被使用,还有 n+1 个引用域是空的

  2.3.3:二叉树的三叉链表存储结构

  使用二叉链表,能够很是方便地访问一个结点的子孙结点,但要访问祖先结点很是困难。能够考虑在每一个结点中再增长一个引用域存放其双亲结点的地址信息,这样就能够经过该引用域很是方便地访问其祖先结点。这就是下面要介绍的三叉链表。

  二叉树的三叉链表存储结构是指二叉树的结点有四个域:一个数据域和三个引用域,数据域存储数据,三个引用域分别存放其左、右孩子结点和双亲结点的地址。当左、右孩子或双亲结点不存在时,相应域为空,用符号 NULL 或∧表示。结点的存储结构以下所示:

  

  下图 (a)是不带头结点的三叉链表,图 (b)是带头结点的三叉链表。

  

 

2.4 二叉链表存储结构的类实现

  二叉树的二叉链表的结点类有 3 个成员字段:数据域字段 data、左孩子引用域字段 lChild 和右孩子引用域字段 rChild。二叉树的二叉链表的结点类的实现以下所示。

 

 1 public class Node<T>
 2  {  3         public T Data { get; set; }  4         public Node<T> LChild { get; set; }  5         public Node<T> RChild { get; set; }  6 
 7         public Node(T data, Node<T> lp, Node<T> rp)  8  {  9             Data = data; 10             LChild = lp; 11             RChild = rp; 12  } 13 
14         public Node(Node<T> lp, Node<T> rp) 15  { 16             Data = default(T); 17             LChild = lp; 18             RChild = rp; 19  } 20 
21         public Node(T data) 22  { 23             Data = data; 24             LChild = null; 25             RChild = null; 26  } 27 
28         public Node() 29  { 30             Data = default(T); 31             LChild = null; 32             RChild = null; 33  } 34     }
View Code

 

   不带头结点的二叉树的二叉链表比带头结点的二叉树的二叉链表的区别与不带头结点的单链表与带头结点的单链表的区别同样。下面只介绍不带头结点的二叉树的二叉链表的类 BiTree<T>。BiTree<T>类只有一个成员字段 head 表示头引用。如下是 BiTree<T>类的实现。

 1 public class BiTree<T>
 2  {  3         //头引用属性
 4         public Node<T> Head { get; set; }  5 
 6         //构造器
 7         public BiTree()  8  {  9             Head = null;  10  }  11 
 12         //构造器
 13         public BiTree(T val)  14  {  15             Node<T> p = new Node<T>(val);  16             Head = p;  17  }  18 
 19         //构造器
 20         public BiTree(Node<T> lp, Node<T> rp)  21  {  22             Node<T> p = new Node<T>(lp, rp);  23             Head = p;  24  }  25 
 26         //构造器
 27         public BiTree(T val, Node<T> lp, Node<T> rp)  28  {  29             Node<T> p = new Node<T>(val, lp, rp);  30             Head = p;  31  }  32 
 33         //判断是不是空二叉树
 34         public bool IsEmpty()  35  {  36             if (Head == null)  37  {  38                 return true;  39  }  40             else
 41  {  42                 return false;  43  }  44  }  45 
 46         //获取根结点
 47         public Node<T> Root()  48  {  49             return Head;  50  }  51 
 52         //获取结点的左孩子结点
 53         public Node<T> GetLChild(Node<T> p)  54  {  55             return p.LChild;  56  }  57 
 58         //获取结点的右孩子结点
 59         public Node<T> GetRChild(Node<T> p)  60  {  61             return p.RChild;  62  }  63 
 64         //将结点p的左子树插入值为val的新结点,  65         //原来的左子树成为新结点的左子树
 66         public void InsertL(T val, Node<T> p)  67  {  68             Node<T> tmp = new Node<T>(val);  69             tmp.LChild = p.LChild;  70             p.LChild = tmp;  71  }  72 
 73         //将结点p的右子树插入值为val的新结点,  74         //原来的右子树成为新结点的右子树
 75         public void InsertR(T val, Node<T> p)  76  {  77             Node<T> tmp = new Node<T>(val);  78             tmp.RChild = p.RChild;  79             p.RChild = tmp;  80  }  81 
 82         //若p非空,删除p的左子树
 83         public Node<T> DeleteL(Node<T> p)  84  {  85             if ((p == null) || (p.LChild == null))  86  {  87                 return null;  88  }  89             Node<T> tmp = p.LChild;  90             p.LChild = null;  91             return tmp;  92  }  93 
 94         //若p非空,删除p的右子树
 95         public Node<T> DeleteR(Node<T> p)  96  {  97             if ((p == null) || (p.RChild == null))  98  {  99                 return null; 100  } 101             Node<T> tmp = p.RChild; 102             p.RChild = null; 103             return tmp; 104  } 105 
106         //判断是不是叶子结点
107         public bool IsLeaf(Node<T> p) 108  { 109             if ((p != null) && (p.LChild == null) && (p.RChild == null)) 110  { 111                 return true; 112  } 113             else
114  { 115                 return false; 116  } 117  } 118     }
View Code

 

2.5 二叉树的遍历

  实际上,遍历是将二叉树中的结点信息由非线性排列变为某种意义上的线性排列。也就是说,遍历操做使非线性结构线性化

  由二叉树的定义可知,一棵二叉树由根结点、左子树和右子树三部分组成,若规定 D、L、R 分别表明遍历根结点、遍历左子树、遍历右子树,则二叉树的遍历方式有 6 种:DLR、DRL、LDR、LRD、RDL、RLD。因为先遍历左子树和先遍历右子树在算法设计上没有本质区别,因此,只讨论三种方式:DLR(先序遍历)、LDR(中序遍历)和 LRD(后序遍历)。

  除了这三种遍历方式外,还有一种方式:层序遍历(Level Order)。层序遍历是从根结点开始,按照从上到下、从左到右的顺序依次访问每一个结点一次仅一次。

  一、先序遍历(DLR)

  先序遍历的基本思想是:首先访问根结点,而后先序遍历其左子树,最后先序遍历其右子树。先序遍历的递归算法实现以下,注意:这里的访问根结点是把根结点的值输出到控制台上。固然,也能够对根结点做其它处理。

          彻底二叉树

 1 public static void PreOrder<T>(Node<T> root)  2  {  3             //根结点为空
 4             if (root == null)  5  {  6                 return;  7  }  8 
 9             //处理根结点
10             Console.WriteLine("{0}", root.Data); 11 
12             //先序遍历左子树
13  PreOrder(root.LChild); 14 
15             //先序遍历右子树
16  PreOrder(root.RChild); 17         }
View Code

  对于上图所示的彻底二叉树,按先序遍历所获得的结点序列为:A B D H I E J C F G

  二、中序遍历(LDR)

  中序遍历的基本思想是:首先中序遍历根结点的左子树,而后访问根结点,最后中序遍历其右子树。中序遍历的递归算法实现以下:

  

 1 public static void InOrder<T>(Node<T> root)  2  {  3             //根结点为空
 4             if (root == null)  5  {  6                 return;  7  }  8             //中序遍历左子树
 9  InOrder(root.LChild); 10             //处理根结点
11             Console.WriteLine("{0}", root.Data); 12             //中序遍历右子树
13  InOrder(root.RChild); 14         }
View Code

  对于上图所示的彻底二叉树,按中序遍历所获得的结点序列为:H D I B J E A F C G

  3、后序遍历(LRD)

  后序遍历的基本思想是:首前后序遍历根结点的左子树,而后后序遍历根结点的右子树,最后访问根结点。后序遍历的递归算法实现以下:

 

 1 public void PostOrder<T>(Node<T> root)  2  {  3             //根结点为空
 4             if (root == null)  5  {  6                 return;  7  }  8 
 9             //先序遍历左子树
10  PostOrder(root.LChild); 11 
12             //先序遍历右子树
13  PostOrder(root.RChild); 14 
15             //处理根结点
16             Console.Write("{0} ", root.Data); 17         }
View Code

 

  对于上图所示的二叉树,按后序遍历所获得的结点序列为:H I D J E B F G C A

  四、层序遍历(Level Order)

  层序遍历的基本思想是:因为层序遍历结点的顺序是先遇到的结点先访问,与队列操做的顺序相同。因此,在进行层序遍历时,设置一个队列,将根结点引用入队,当队列非空时,循环执行如下三步:

(1) 从队列中取出一个结点引用,并访问该结点;
(2) 若该结点的左子树非空,将该结点的左子树引用入队;
(3) 若该结点的右子树非空,将该结点的右子树引用入队;

  层序遍历的算法实现以下:

 1 public static void LevelOrder<T>(Node<T> root)  2  {  3             //根结点为空
 4             if (root == null)  5  {  6                 return;  7  }  8 
 9             //设置一个队列保存层序遍历的结点
10             CSeqQueue<Node<T>> sq = new CSeqQueue<Node<T>>(50); 11 
12             //根结点入队
13  sq.In(root); 14 
15             //队列非空,结点没有处理完
16             while (!sq.IsEmpty()) 17  { 18                 //结点出队
19                 Node<T> tmp = sq.Out(); 20                 //处理当前结点
21                 Console.WriteLine("{0}", tmp.Data); 22                 //将当前结点的左孩子结点入队
23                 if (tmp.LChild != null) 24  { 25  sq.In(tmp.LChild); 26  } 27                 //将当前结点的右孩子结点入队
28                 if (tmp.RChild != null) 29  { 30  sq.In(tmp.RChild); 31  } 32  } 33         }
View Code

  对于上图所示的二叉树,按层次遍历所获得的结点序列为:A B C D E F G H I J

2.6 二叉树的应用

  实际场景使用上,用的最多的是二叉平衡树,有种特殊的二叉平衡树就是红黑树,Java集合中的TreeSet和TreeMap,C++STL中的set,map以及LInux虚拟内存的管理,都是经过红黑树去实现的,还有哈弗曼树编码方面的应用,以及B-Tree,B+-Tree在文件系统中的应用。固然二叉查找树能够用来查找和排序。

  二叉树在搜索上的优点

   数组的搜索比较方便,能够直接使用下标,但删除或者插入就比较麻烦了,而链表与之相反,删除和插入都比较简单,可是查找很慢,这天然也与这两种数据结构的存储方式有关,数组是取一段相连的空间,而链表是每建立一个节点便取一个节点所需的空间,只是使用指针进行链接,空间上并非连续的。而二叉树就既有链表的好处,又有数组的优势。

  2.6.1 二叉查找树

  二叉查找树具备很高的灵活性,对其优化能够生成平衡二叉树,红黑树等高效的查找和插入数据结构,后文会介绍。

   1: 定义

 

  二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具备下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上全部结点的值均小于它的根结点的值;

  2. 若任意节点的右子树不空,则右子树上全部结点的值均大于它的根结点的值;

  3. 任意节点的左、右子树也分别为二叉查找树。

  4. 没有键值相等的节点(no duplicate nodes)。

  以下图,在二叉树的基础上,加上节点之间的大小关系,就是二叉查找树

  

  从图中能够看出,二叉查找树中,最左和最右节点即为最小值和最大值

  2: 查找

  查找操做和二分查找相似,将key和节点的key比较,若是小于,那么就在左节点查找,若是大于,则在右节点查找,若是相等,直接返回Value。

  C# 迭代实现

 1 /// <summary>
 2         /// 二叉查找树查找  3         /// </summary>
 4         /// <param name="bt">二叉树</param>
 5         /// <param name="key">目标值</param>
 6         /// <returns>0:查找成功,1:查找失败</returns>
 7         public int Search(BiTree<int> bt, int key)  8  {  9             Node<int> p; 10             //二叉排序树为空
11             if (bt.IsEmpty() == true) 12  { 13                 Console.WriteLine("The Binary Sorting Tree is empty!"); 14                 return 1; 15  } 16             p = bt.Head; 17             //二叉排序树非空
18             while (p != null) 19  { 20                 //存在要查找的记录
21                 if (p.Data == key) 22  { 23                     Console.WriteLine("Search is Successful!"); 24                     return 0; 25  } 26                 //待查找记录的关键码大于结点的关键码
27                 else if (p.Data < key) 28  { 29                     p = p.RChild; 30  } 31                 //待查找记录的关键码小于结点的关键码
32                 else
33  { 34                     p = p.LChild; 35  } 36  } 37 
38             return 1; 39         }
View Code

 

  3: 插入

  插入和查找相似,首先查找有没有和key相同的,若是有,更新;若是没有找到,那么建立新的节点。并更新每一个节点的Number值,代码实现以下:

  C#实现

 1 /// <summary>
 2         /// 二叉查找树插入  3         /// </summary>
 4         /// <param name="bt">二叉树</param>
 5         /// <param name="key">目标值</param>
 6         /// <returns>0:查找成功,1:查找失败</returns>
 7         public int Insert(BiTree<int> bt, int key)  8  {  9             Node<int> p; 10             Node<int> parent = new Node<int>();//插入节点的父级
11             p = bt.Head; 12             while (p != null) 13  { 14                 //存在关键码等于key的结点
15                 if (p.Data == key) 16  { 17                     Console.WriteLine("Record is exist!"); 18                     return 1; 19  } 20                 parent = p; 21                 //记录的关键码大于结点的关键码
22                 if (p.Data < key) 23  { 24                     p = p.RChild; 25  } 26                 //记录的关键码小于结点的关键码
27                 else
28  { 29                     p = p.LChild; 30  } 31  } 32 
33             p = new Node<int>(key); 34             //二叉查找树为空
35             if (parent == null) 36  { 37                 bt.Head = parent; 38  } 39             //待插入记录的关键码小于结点的关键码
40             else if (p.Data < parent.Data) 41  { 42                 parent.LChild = p; 43  } 44             //待插入记录的关键码大于结点的关键码
45             else
46  { 47                 parent.RChild = p; 48  } 49             return 0; 50         }
View Code

  随机插入造成树的动画以下,能够看到,插入的时候树仍是可以保持近似平衡状态:

  

  4: 删除

  二叉排序树的删除状况以下图所示。

  

    

       

  C# 实现

 1  /// <summary>
 2         /// 二叉查找树删除  3         /// </summary>
 4         /// <param name="bt"></param>
 5         /// <param name="key"></param>
 6         /// <returns></returns>
 7         public int Delete(BiTree<int> bt, int key)  8  {  9             Node<int> p;  10             Node<int> parent = new Node<int>();  11             Node<int> s = new Node<int>();  12             Node<int> q = new Node<int>();  13             //二叉排序树为空
 14             if (bt.IsEmpty() == true)  15  {  16                 Console.WriteLine("The Binary Sorting is empty!");  17                 return 1;  18  }  19             p = bt.Head;  20             parent = p;  21             //二叉排序树非空
 22             while (p != null)  23  {  24                 //存在关键码等于key的结点
 25                 if (p.Data == key)  26  {  27                     //结点为叶子结点
 28                     if (bt.IsLeaf(p))  29  {  30                         if (p == bt.Head)  31  {  32                             bt.Head = null;  33  }  34                         else if (p == parent.LChild)  35  {  36                             parent.LChild = null;  37  }  38                         else
 39  {  40                             parent.RChild = null;  41  }  42  }  43                     //结点的右子结点为空而左子结点非空
 44                     else if ((p.RChild == null) && (p.LChild != null))  45  {  46                         if (p == parent.LChild)  47  {  48                             parent.LChild = p.LChild;  49  }  50                         else
 51  {  52                             parent.RChild = p.LChild;  53  }  54  }  55                     //结点的左子结点为空而右子结点非空
 56                     else if ((p.LChild == null) && (p.RChild != null))  57  {  58                         if (p == parent.LChild)  59  {  60                             parent.LChild = p.RChild;  61  }  62                         else
 63  {  64                             parent.RChild = p.RChild;  65  }  66  }  67                     //结点的左右子结点均非空
 68                     else
 69  {  70                         q = p;  71                         s = p.RChild;  72                         while (s.LChild != null)  73  {  74                             q = s;  75                             s = s.LChild;  76  }  77                         p.Data = s.Data;  78                         if (q != p)  79  {  80                             q.LChild = s.RChild;  81  }  82                         else
 83  {  84                             q.RChild = s.RChild;  85  }  86  }  87                     return 0;  88  }  89                 //待删除记录的关键码大于结点的关键码
 90                 else if (p.Data < key)  91  {  92                     parent = p;  93                     p = p.RChild;  94  }  95                 else
 96  {  97                     parent = p;  98                     p = p.LChild;  99  } 100  } 101             return -1; 102         }
View Code

  

  以上二叉查找树的删除节点的算法不是完美的,由于随着删除的进行,二叉树会变得不太平衡,下面是动画演示。

  

  二叉查找树和二分查找同样,插入和查找的时间复杂度均为lgN,可是在最坏的状况下仍然会有N的时间复杂度。缘由在于插入和删除元素的时候,树没有保持平衡。咱们追求的是在最坏的状况下仍然有较好的时间复杂度,这就是平衡查找树了。

 

2.7 树与森林

  2.7.1树的存储

  树的存储结构包括顺序存储结构和链式存储结构但不管采用哪一种存储结构,都要求存储结构不但能存储结点自己的信息,还能存储树中各结点之间的逻辑关系。

  一、双亲表示法

  从树的定义可知,除根结点外,树中的每一个结点都有惟一的一个双亲结点。根据这一特性,可用一组连续的存储空间(一维数组)存储树中的各结点。树中的结点除保存结点自己的信息以外,还要保存其双亲结点在数组中的位置(数组的下标),树的这种表示法称为双亲表示法。

  因为树的结点只保存两个信息,因此树的结点用结构体 PNode<T>来表示。结构中有两个字段:数据字段 data 和双亲位置字段 pPos。而树类 PTree<T>只有一个成员数组字段 nodes,用于保存结点。

  树的双亲表示法的结点的结构以下所示:

  树的双亲表示法的结点的结构体 PNode<T>和树类 PTree<T>的定义以下:

 1 public struct PNode<T>
 2 {  3 public T data;  4 public int pPos;  5  6 }  7 public class PTree<T>
 8 {  9 private PNode<T>[] nodes; 10 11 }
View Code

  下图分别为树结构和树双亲表示法

 

  树的双亲表示法对于实现 Parent(t)操做和 Root()操做很是方便。Parent(t)操做能够在常量时间内实现,反复调用 Parent(t)操做,直到遇到无双亲的结点(其 pPos值为-1)时,便找到了树的根,这就是 Root()操做的执行过程。但要实现查找孩子结点和兄弟结点等操做很是困难,由于这须要查询整个数组。要实现这些操做,须要在结点结构中增设存放第1个孩子在数组中的序号的域和存放第1个兄弟在数组中的序号的域。

  二、孩子链表表示法

  孩子链表表示法也是用一维数组来存储树中各结点的信息。但结点的结构与双亲表示法中结点的结构不一样,孩子链表表示法中的结点除保存自己的信息外,不是保存其双亲结点在数组中的序号,而是保存一个链表的第一个结点的地址信息。这个链表是由该结点的全部孩子结点组成。每一个孩子结点保存有两个信息,一个是每一个孩子结点在一维数组中的序号,另外一个是下一个孩子结点的地址信息。

  孩子结点的结构以下所示:

 

  孩子结点类 ChildNode 的定义以下

1 public class ChildNode 2 { 3 private int index; 4 private ChildNode nextChild; 5 6 }
View Code

  树结构和树孩子链表表示法以下图:

  树的孩子链表表示法对于实现查找孩子结点等操做很是方便,但对于实现查找双亲结点、兄弟结点等操做则比较困难。

  三、孩子兄弟表示法

  这是一种经常使用的数据结构,又称二叉树表示法,或二叉链表表示法,即以二叉链表做为树的存储结构。每一个结点除存储自己的信息外,还有两个引用域分别存储该结点第一个孩子的地址信息和下一个兄弟的地址信息。树类 CSTree<T>只有一个成员字段 head,表示头引用。

   树的孩子兄弟表示法的结点的结构以下所示:

  

  树的孩子兄弟表示法的结点类 CSNode<T>的定义以下:

1 public class CSNode<T>
2 { 3 private T data; 4 private CSNode<T> firstChild; 5 private CSNode<T> nextSibling; 6 7 }
View Code

  树类 CSTree<T>的定义以下:

1 public class CSTree<T>
2 { 3 private CSNode<T> head;; 4 5 }
View Code

  树的孩子兄弟表示法以下

  

  树的孩子兄弟表示法对于实现查找孩子、兄弟等操做很是方便,但对于实现查找双亲结点等操做则很是困难。若是在树的结点中再增长一个域来存储孩子的双亲结点的地址信息,则就能够较方便地实现上述操做了。

  2.7.2 树、森林与二叉树的转换 

  从树的孩子兄弟表示法可知,树能够用二叉链表进行存储,因此,二叉链表能够做为树和二叉树之间的媒介。也就是说,借助二叉链表,树和二叉树能够相互进行转换。从物理结构来看,它们的二叉链表是相同的,只是解释不一样而已。而且,若是设定必定的规则,就可用二叉树来表示森林,森林和二叉树也能够相互进行转换。

  一、树转换为二叉树

  因为二叉树是有序的,为了不混淆,对于无序树,咱们约定树中的每一个结点的孩子结点按从左到右的顺序进行编号。如上图所示的树,根结点 A 有三个孩子 B、C、D,规定结点 B 是结点 A 的第一个孩子,结点 C 是结点 A 的第 2个孩子,结点 D 是结点 A 的第 3 个孩子。

  将树转换成二叉树的步骤是: 

(1)加线。就是在全部兄弟结点之间加一条连线;
(2)抹线。就是对树中的每一个结点,只保留他与第一个孩子结点之间的连
线,删除它与其它孩子结点之间的连线;
(3)旋转。就是以树的根结点为轴心,将整棵树顺时针旋转必定角度,使
之结构井井有条。

  下图是树转换为二叉树的转换过程示意图。

  

  二、森林转换为二叉树

   森林是由若干棵树组成,能够将森林中的每棵树的根结点看做是兄弟,因为每棵树均可以转换为二叉树,因此森林也能够转换为二叉树。

  将森林转换为二叉树的步骤是: 

  (1)先把每棵树转换为二叉树;  

  (2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点做为前一棵二叉树的根结点的右孩子结点,用线链接起来。当全部的二叉树链接起来后获得的二叉树就是由森林转换获得的二叉树。

   森林转换为二叉树的转换过程示意图以下:

  

  三、二叉树转换为树

  二叉树转换为树是树转换为二叉树的逆过程,其步骤是:  

(1)若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结
点的右孩子结点……都做为该结点的孩子结点,将该结点与这些右孩子结点用线
链接起来;
(2)删除原二叉树中全部结点与其右孩子结点的连线;
(3)整理(1)和(2)两步获得的树,使之结构井井有条。

   二叉树转换为树的过程示意图以下:

  四、二叉树转换为森林

  二叉树转换为森林比较简单,其步骤以下:

(1)先把每一个结点与右孩子结点的连线删除,获得分离的二叉树;
(2)把分离后的每棵二叉树转换为树;
(3)整理第(2)步获得的树,使之规范,这样获得森林。

  2.7.3 树和森林的遍历

  一、树的遍历

  树的遍历一般有两种方式:

  (1)先序遍历,即先访问树的根结点,而后依次先序遍历树中的每棵子树。

  (2)后序遍历,即先依次后序遍历树中的每棵子树,而后访问根结点。

  对上图中的树所示的树进行先序遍历所获得的结点序列为:A B E F G C H D I J 

    对此树进行后序遍历获得的结点序列为:E F G B H C I J D A

  根据树与二叉树的转换关系以及二叉树的遍历定义能够推知,树的先序遍历与其转换的相应的二叉树的先序遍历的结果序列相同;树的后序遍历与其转换的二叉树的中序遍历的结果序列相同;树的层序遍历与其转换的二叉树的后序遍历的结果序列相同。所以,树的遍历算法能够采用相应的二叉树的遍历算法来实现。

  二、森林的遍历  

  森林的遍历有两种方式。  

  (1)先序遍历,即先访问森林中第一棵树的根结点,而后先序遍历第一棵树中的每棵子树,最后先序遍历除第一棵树以后剩余的子树森林。  

  (2)中序遍历,即先中序遍历森林中第一棵树的根结点的全部子树,而后访问第一棵树的根结点,最后中序遍历除第一棵树以后剩余的子树森林。

  

  上图所示的森林的先序遍历的结点序列为:A B C D E F G H J I  

  此森林的中序遍历的结点序列为:B C D A F E J H I G

  由森林与二叉树的转换关系以及森林与二叉树的遍历定义可知,森林的先序遍历和中序遍历与所转换获得的二叉树的先序遍历和中序遍历的结果序列相同

 

2.8 哈夫曼树

  2.8.1 哈夫曼树的基本概念

  首先给出定义哈夫曼树所要用到的几个基本概念。

(1)路径(Path):从树中的一个结点到另外一个结点之间的分支构成这两个结点间的路径。

(2)路径长度(Path Length):路径上的分支数。
(3)树的路径长度(Path Length of Tree):从树的根结点到每一个结点的路径长度之和。在结点数目相同的二叉树中,彻底二叉树的路径长度最短。

(4)结点的权(Weight of Node):在一些应用中,赋予树中结点的一个有实际意义的数。

(5)结点的带权路径长度(Weight Path Length of Node):从该结点到树的根结点的路径长度与该结点的权的乘积。

(6)树的带权路径长度(WPL):树中全部叶子结点的带权路径长度之和记为

  

  其中,W k 为第k个叶子结点的权值,L k 为第k个叶子结点的路径长度。在下图所示的二叉树中,结点B的路径长度为 1,结点C和D的路径长度为 2,结点E、F和G的路径长度为 3,结点H的路径长度为 4,结点I的路径长度为 5。该树的路径长度为:1+2*2+3*3+4+5=23。若是结点B、C、D、E、F、G、H、I的权分别是 一、二、三、四、五、六、七、8,则这些结点的带权路径长度分别是 1*一、2*二、2*三、3*四、3*五、3*六、4*七、5*8,该树的带权路径长度为 3*5+3*6+5*8=73。

 

  

  2.8.2 :哈夫曼树(Huffman Tree)

  哈夫曼树(Huffman Tree),又叫最优二叉树,指的是对于一组具备肯定权值的叶子结点的具备最小带权路径长度的二叉树。在下图所示的的四棵二叉树,都有 4 个叶子结点,其权值分别为 一、二、三、4,它们的带权路径长度分别为: 

(a)WPL=1×2+2×2+3×2+4×2=20
(b)WPL=1×1+2×2+3×3+4×3=28
(c)WPL=1×3+2×3+3×2+4×1=19
(d)WPL=2×1+1×2+3×3+4×3=29

  其中,图 (c)所示的二叉树的带权路径长度最小,这棵树就是哈夫曼树。能够验证,哈夫曼树的带权路径长度最小。

 

  那么,如何构造一棵哈夫曼树呢?哈夫曼最先给出了一个带有通常规律的算法,俗称哈夫曼算法。现叙述以下: 

  (1)根据给定的n个权值{w 1 ,w 2 ,…,w n },构造n棵只有根结点的二叉树集合F={T 1 ,T 2 ,…,T n };

  (2)从集合 F 中选取两棵根结点的权最小的二叉树做为左右子树,构造一棵新的二叉树,且置新的二叉树的根结点的权值为其左、右子树根结点权值之和。

  (3)在集合 F 中删除这两棵树,并把新获得的二叉树加入到集合 F 中;
  (4)重复上述步骤,直到集合中只有一棵二叉树为止,这棵二叉树就是哈夫曼树。

  由二叉树的性质 4 和哈夫曼树的特色可知,一棵有 n 个叶子结点构造的哈夫曼树共有 2n-1 个结点。

  哈夫曼树的构造过程:

  

  2.8.3 哈夫曼树类的实现  

   由哈夫曼树的构造算法可知,用一个数组存放原来的 n 个叶子结点和构造过程当中临时生成的结点,数组的大小为 2n-1。因此,哈夫曼树类 HuffmanTree 中有两个成员字段:data 数组用于存放结点,leafNum 表示哈夫曼树叶子结点的数目。结点有四个域,一个域 weight,用于存放该结点的权值;一个域 lChild,用于存放该结点的左孩子结点在数组中的序号;一个域 rChild,用于存放该结点的右孩子结点在数组中的序号;一个域 parent,用于断定该结点是否已加入哈夫曼树中。哈夫曼树结点的结构为。

  

  因此,结点类 Node 有 4 个成员字段,weight 表示该结点的权值,lChild 和rChild 分别表示左、右孩子结点在数组中的序号,parent 表示该结点是否已加入哈夫曼树中,若是 parent 的值为-1,表示该结点未加入到哈夫曼树中。当该结点已加入到哈夫曼树中时,parent 的值为其双亲结点在数组中的序号。

  结点类 Node 的定义以下:

 

 1 public class HuffmanNode  2  {  3         private int weight;//结点权值
 4         private int lChild;///左孩子结点
 5         private int rChild; //右孩子结点
 6         private int parent; //父结点
 7 
 8         public int Weight { get; set; }  9         public int LChild { get; set; } 10         public int RChild { get; set; } 11         public int Parent { get; set; } 12 
13         //构造器
14         public HuffmanNode() 15  { 16             weight = 0; 17             lChild = -1; 18             rChild = -1; 19             parent = -1; 20  } 21 
22         //构造器
23         public HuffmanNode(int w, int lc, int rc, int p) 24  { 25             weight = w; 26             lChild = lc; 27             rChild = rc; 28             parent = p; 29  } 30     }
View Code

 

  哈夫曼树类 HuffmanTree 中只有一个成员方法 Create,它的功能是输入 n 个叶子结点的权值,建立一棵哈夫曼树。哈夫曼树类 HuffmanTree 的实现以下。

 1 public class HuffmanTree  2  {  3         private HuffmanNode[] data;//结点数组
 4         private int leafNum;//叶子结点数目  5 
 6         //索引器
 7         public HuffmanNode this[int index]  8  {  9             get
10  { 11                 return data[index]; 12  } 13             set
14  { 15                 data[index] = value; 16  } 17  } 18 
19         //叶子结点数目属性
20         public int LeafNum { get; set; } 21 
22         public HuffmanTree(int n) 23  { 24             data = new HuffmanNode[2 * n - 1]; 25             for (int i = 0; i < 2 * n - 1; i++) 26  { 27                 data[i] = new HuffmanNode(); 28  } 29             leafNum = n; 30  } 31 
32         //建立哈夫曼树
33         public HuffmanNode[] Create(List<int> list) 34  { 35             int max1; 36             int max2; 37             int tmp1; 38             int tmp2; 39             // 输入 n 个叶子结点的权值
40             for (int i = 0; i < this.leafNum; ++i) 41  { 42                 data[i].Weight = list[i]; 43  } 44 
45             //处理 n 个叶子结点,创建哈夫曼树
46             for (int i = 0; i < this.leafNum - 1; ++i) 47  { 48                 max1 = max2 = Int32.MaxValue; 49                 tmp1 = tmp2 = 0; 50                 //在所有结点中找权值最小的两个结点
51                 for (int j = 0; j < this.leafNum + i; ++j) 52  { 53                     if ((data[j].Weight < max1) && (data[j].Parent == -1)) 54  { 55                         max2 = max1; 56                         tmp2 = tmp1; 57                         tmp1 = j; 58                         max1 = data[j].Weight; 59  } 60                     else if ((data[j].Weight < max2) && (data[j].Parent == -1)) 61  { 62                         max2 = data[j].Weight; 63                         tmp2 = j; 64  } 65  } 66                 data[tmp1].Parent = this.leafNum + i; 67                 data[this.leafNum + i].Weight = data[tmp1].Weight + data[tmp2].Weight; 68                 data[this.leafNum + i].LChild = tmp1; 69                 data[this.leafNum + i].RChild = tmp2; 70  } 71             return data; 72 
73  } 74     }
View Code 

  

  2.8.4 哈夫曼编码

  在数据通讯中,一般须要把要传送的文字转换为由二进制字符 0 和 1 组成的二进制串,这个过程被称之为编码(Encoding)。例如,假设要传送的电文为DCBBADD,电文中只有 A、B、C、D 四种字符,若这四个字符采用表 下图(a)所示的编码方案,则电文的代码为 11100101001111,代码总长度为 14。若采用表 5-1(b) 所示的编码方案,则电文的代码为 0110101011100,代码总长度为 13。

  

  哈夫曼树可用于构造总长度最短的编码方案。具体构造方法以下:设须要编码的字符集为{d 1 ,d 2 ,…,d n },各个字符在电文中出现的次数或频率集合为{w 1 ,w 2 ,…,w n }。以d 1 ,d 2 ,…,d n 做为叶子结点,以w 1 ,w 2 ,…,w n 做为相应叶子结点的权值来构造一棵哈夫曼树。规定哈夫曼树中的左分支表明 0,右分支表明 1,则从根结点到叶子结点所通过的路径分支组成的0和1的序列便为该结点对应字符的编码就是哈夫曼编码(Huffman Encoding)。

  下图 就是电文 DCBBADD 的哈夫曼树,其编码就是表 (b)。在创建不等长编码中,必须使任何一个字符的编码都不是另外一个编码的前缀,这样才能保证译码的惟一性。例如,若字符 A 的编码是 00,字符 B 的编码是 001,那么字符 A 的编码就成了字符 B 的编码的后缀。这样,对于代码串001001,在译码时就没法断定是将前两位码 00 译成字符 A 仍是将前三位码 001译成 B。这样的编码被称之为具备二义性的编码,二义性编码是不惟一的。而在哈夫曼树中,每一个字符结点都是叶子结点,它们不可能在根结点到其它字符结点的路径上,因此一个字符的哈夫曼编码不多是另外一个字符的哈夫曼编码的前缀,从而保证了译码的非二义性。

   

2.9 C#中的树

  C#中的树不少。好比,Windows Form 程序设计和 Web 程序设计中都有一种被称为 TreeView 的控件。TreeView 控件是一个显示树形结构的控件,此树形结构与 Windows 资源管理器中的树形结构很是相似。不一样的是,TreeView 能够由任意多个节点对象组成。每一个节点对象均可以关联文本和图像。另外,Web 程序设计中的 TreeView 的节点还能够显示为超连接并与某个 URL 相关联。每一个节点还能够包括任意多个子节点对象。包含节点及其子节点的层次结构构成了TreeView 控件所呈现的树形结构。

  DOM(Document Object Model)是 C#中树形结构的另外一个例子。文档对象模型 DOM 不是 C#中独有的,它是 W3C 提供的可以让程序和脚本动态访问和更新文档内容、结构和样式的语言平台。DOM 被分为不一样的部分(Core DOM,XML DOM和 HTML DOM)和不一样的版本(DOM 1/2/3),Core DOM 定义了任意结构文档的标准对象集合,XML DOM 定义了针对 XML 文档的标准对象集合,而 HTML DOM 定义了针对 HTML 文档的标准对象集合。C#提供了一个标准的接口来访问并操做 HTML和 XML 对象集。后面将以 XML 对象集为例进行说明,对 HTML 对象集的操做相似。DOM 容许将 XML 文档的结构加载到内存中,由此能够得到在 XML 文档中执行更新、插入和删除操做的能力。DOM 是一个树形结构,文件中的每一项都是树中的一个结点。每一个结点下面还有子结点。还能够用结点表示数据,而且数据和元素是不一样的。在 C#中使用不少类来访问 DOM,主要的类见下表所示。

  

本章小结 

   树形结构是一种很是重要的非线性结构,树形结构中的数据元素称为结点,它们之间是一对多的关系,既有层次关系,又有分支关系。树形结构有树和二叉树两种。

  树是递归定义的,树由一个根结点和若干棵互不相交的子树构成,每棵子树的结构与树相同,一般树指无序树。树的逻辑表示一般有四种方法,即直观表示法、凹入表示法、广义表表示法和嵌套表示法。树的存储方式有 3 种,即双亲表示法、孩子链表表示法和孩子兄弟表示法。

  二叉树的定义也是递归的,二叉树由一个根结点和两棵互不相交的子树构成,每棵子树的结构与二叉树相同,一般二叉树指有序树。重要的二叉树有满二叉树和彻底二叉树。二叉树的性质主要有 5 条。二叉树的的存储结构主要有三种:顺序存储结构、二叉链表存储结构和三叉链表存储结构,本书给出了二叉链表存储结构的 C#实现。二叉树的遍历方式一般有四种:先序遍历(DLR)、中序遍历(LDR)、后序遍历(LRD)和层序遍历(Level Order)。

  森林是 m(m≥0)棵树的集合。树、森林与二叉树的之间能够进行相互转换。树的遍历方式有先序遍历和后序遍历两种,森林的遍历方式有先序遍历和中序遍历两种。

  哈夫曼树是一组具备肯定权值的叶子结点的具备最小带权路径长度的二叉树。哈夫曼树可用于解决最优化问题,在数据通讯等领域应用很广。

相关文章
相关标签/搜索