您的位置:首页 > 其它

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

2017-11-01 13:36 267 查看
先前我们讲的都是“线性结构”,他的特征就是“一个节点最多有一个”前驱“和一个”后继“。那么我们今天讲的树会是怎样的呢?

我们可以对”线性结构“改造一下,变为”一个节点最多有一个"前驱“和”多个后继“。哈哈,这就是我们今天说的”树“。

 

一: 树

      我们思维中的”树“就是一种枝繁叶茂的形象,那么数据结构中的”树“该是怎么样呢?对的,他是一种现实中倒立的树。



1:术语

     其实树中有很多术语的,这个是我们学习树形结构必须掌握的。

     <1>  父节点,子节点,兄弟节点

                  这个就比较简单了,B和C的父节点就是A,反过来说就是B和C是A的子节点。B和C就是兄弟节点。

     <2>  结点的度

                 其实”度“就是”分支数“,比如A的分支数有两个“B和C",那么A的度为2。

     <3> 树的度

                看似比较莫名其妙吧,他和”结点的度“的区别就是,树的度讲究大局观,乃树中最大的结点度,其实也就是2。

     <4> 叶结点,分支结点

                叶结点就是既没有左孩子也没有右孩子结点,也就是结点度为0。分支节点也就是if的else的条件咯。

    <5> 结点的层数

               这个很简单,也就是树有几层。

   <6> 有序树,无序树

               有序树我们先前也用过,比如“堆”和“二叉排序树”,说明这种树是按照一定的规则进行排序的,else条件就是无序树。

   <7>  森林

               现实中,很多的树形成了森林,那在数据结构中,我们把上图的“A”节点砍掉,那么B,C子树合一起就是森林咯。

 

2: 树的表示

     树这个结构的表示其实有很多种,常用的也就是“括号”表示法。

     比如上面的树就可以表示为:(A(B(D),(E)),(C(F),(G)))

 

二: 二叉树

         在我们项目开发中,很多地方都会用到树,但是多叉树的处理还是比较纠结的,所以俺们本着“大事化小,小事化了“的原则

      把”多叉树“转化为”二叉树“,那么问题就简化了很多。

 

1: ”二叉树“和”树“有什么差异呢?

         第一点:  树的度没有限制,而“二叉树”最多只能有两个,不然也就不叫二叉树了,哈哈。

         第二点:树中的子树没有左右划分,很简单啊,找不到参照点,二叉树就有参照物咯。

 

2: 二叉树的类型

       二叉树中有两种比较完美的类型,“完全二叉树”和“满二叉树”。

          <1>  满二叉树    

                       除叶子节点外,所有节点的度都为2,文章开头处的树就是这里的“满二叉树”。

          <2>  完全二叉树

                      必须要满足两个条件就即可:  干掉最后一层,二叉树变为“满二叉树”。

                                                              最后一层的叶节点必须是“从左到右”依次排开。

                     我们干掉文章开头处的节点“F和”G",此时还是“完全二叉树”,但已经不是“满二叉树”了,你懂的。

 

3: 二叉树的性质

         二叉树中有5点性质非常重要,也是俺们必须要记住的。

     <1>  二叉树中,第i层的节点最多有2(i-1)个。

     <2>  深度为k的二叉树最多有2k-1个节点。

     <3>  二叉树中,叶子节点树为N1个,度为2的节点有N2个,那么N1=N2+1。

     <4>  具有N个结点的二叉树深度为(Log2 N)+1层。

     <5>  N个结点的完全二叉树如何用顺序存储,对于其中的一个结点i,存在以下关系,

              2*i是结点i的父结点。

              i/2是结点i的左孩子。

              (i/2)+1是结点i的右孩子。

 

4: 二叉树的顺序存储

      同样的存储方式也有两种,“顺序存储”和“链式存储”。

       <1> 顺序存储

                 说实话,树的存储用顺序结构比较少,因为从性质定理中我们都可以看出只限定为“完全二叉树”,那么如果二叉树不是

              “完全二叉树”,那我们就麻烦了,必须将其转化为“完全二叉树”,将空的节点可以用“#”代替,图中也可看出,为了维护

              性质定理5的要求,我们牺牲了两个”资源“的空间。



     <2> 链式存储

               上面也说了,顺序存储会造成资源的浪费,所以嘛,我们开发中用的比较多的还是“链式存储”,同样“链式存储”

            也非常的形象,非常的合理。

               一个结点存放着一个“左指针”和一个“右指针”,这就是二叉链表。

               如何方便的查找到该结点的父结点,可以采用三叉链表。

 

5: 常用操作

      一般也就是“添加结点“,“查找节点”,“计算深度”,“遍历结点”,“清空结点”

   

<1> 这里我们就用二叉链表来定义链式存储模型
1 #region 二叉链表存储结构
2     /// <summary>
3 /// 二叉链表存储结构
4 /// </summary>
5 /// <typeparam name="T"></typeparam>
6     public class ChainTree<T>
7     {
8         public T data;
9
10         public ChainTree<T> left;
11
12         public ChainTree<T> right;
13     }
14     #endregion


 

<2> 添加结点

             要添加结点,我们就要找到添加结点的父结点,并且根据指示插入到父结点中指定左结点或者右结点。
1 #region 将指定节点插入到二叉树中
2         /// <summary>
3 /// 将指定节点插入到二叉树中
4 /// </summary>
5 /// <typeparam name="T"></typeparam>
6 /// <param name="tree"></param>
7 /// <param name="node"></param>
8 /// <param name="direction">插入做左是右</param>
9 /// <returns></returns>
10         public ChainTree<T> BinTreeAddNode<T>(ChainTree<T> tree, ChainTree<T> node, T data, Direction direction)
11         {
12             if (tree == null)
13                 return null;
14
15             if (tree.data.Equals(data))
16             {
17                 switch (direction)
18                 {
19                     case Direction.Left:
20                         if (tree.left != null)
21                             throw new Exception("树的左节点不为空,不能插入");
22                         else
23                             tree.left = node;
24
25                         break;
26                     case Direction.Right:
27                         if (tree.right != null)
28                             throw new Exception("树的右节点不为空,不能插入");
29                         else
30                             tree.right = node;
31
32                         break;
33                 }
34             }
35
36             BinTreeAddNode(tree.left, node, data, direction);
37             BinTreeAddNode(tree.right, node, data, direction);
38
39             return tree;
40         }
41         #endregion     


     

<3>  查找节点  

                 二叉树中到处都散发着递归思想,很能锻炼一下我们对递归的认识,同样查找也是用到了递归思想。
1         #region 在二叉树中查找指定的key
2         /// <summary>
3 ///在二叉树中查找指定的key
4 /// </summary>
5 /// <typeparam name="T"></typeparam>
6 /// <param name="tree"></param>
7 /// <param name="data"></param>
8 /// <returns></returns>
9         public ChainTree<T> BinTreeFind<T>(ChainTree<T> tree, T data)
10         {
11             if (tree == null)
12                 return null;
13
14             if (tree.data.Equals(data))
15                 return tree;
16
17             return BinTreeFind(tree, data);
18         }
19         #endregion


     

<4> 计算深度

          这个问题纠结了我二个多小时,原因在于没有深刻的体会到递归,其实主要思想就是递归左子树和右子树,然后得出较大的一个。
1 #region 获取二叉树的深度
2         /// <summary>
3 /// 获取二叉树的深度
4 /// </summary>
5 /// <typeparam name="T"></typeparam>
6 /// <param name="tree"></param>
7 /// <returns></returns>
8         public int BinTreeLen<T>(ChainTree<T> tree)
9         {
10             int leftLength;
11             int rightLength;
12
13             if (tree == null)
14                 return 0;
15
16             //递归左子树的深度
17             leftLength = BinTreeLen(tree.left);
18
19             //递归右子书的深度
20             rightLength = BinTreeLen(tree.right);
21
22             if (leftLength > rightLength)
23                 return leftLength + 1;
24             else
25                 return rightLength + 1;
26         }
27         #endregion


 

<5>  遍历结点

             二叉树中遍历节点的方法还是比较多的,有“先序”,“中序”,“后序”,“按层”,其实这些东西只可意会,不可言传,真的很难在口头

        上说清楚,需要反复的体会递归思想。

            先序:先访问根,然后递归访问左子树,最后递归右子树。(DLR模式)

            中序:先递归访问左子树,在访问根,最后递归右子树。(LDR模式)

            后序:先递归访问左子树,然后递归访问右子树,最后访问根。(LRD模式)

            按层:这个比较简单,从上到下,从左到右的遍历节点。
1  #region 二叉树的先序遍历
2         /// <summary>
3 /// 二叉树的先序遍历
4 /// </summary>
5 /// <typeparam name="T"></typeparam>
6 /// <param name="tree"></param>
7         public void BinTree_DLR<T>(ChainTree<T> tree)
8         {
9             if (tree == null)
10                 return;
11
12             //先输出根元素
13             Console.Write(tree.data + "\t");
14
15             //然后遍历左子树
16             BinTree_DLR(tree.left);
17
18             //最后遍历右子树
19             BinTree_DLR(tree.right);
20         }
21         #endregion
22
23         #region 二叉树的中序遍历
24         /// <summary>
25 /// 二叉树的中序遍历
26 /// </summary>
27 /// <typeparam name="T"></typeparam>
28 /// <param name="tree"></param>
29         public void BinTree_LDR<T>(ChainTree<T> tree)
30         {
31             if (tree == null)
32                 return;
33
34             //优先遍历左子树
35             BinTree_LDR(tree.left);
36
37             //然后输出节点
38             Console.Write(tree.data + "\t");
39
40             //最后遍历右子树
41             BinTree_LDR(tree.right);
42         }
43         #endregion
44
45         #region 二叉树的后序遍历
46         /// <summary>
47 /// 二叉树的后序遍历
48 /// </summary>
49 /// <typeparam name="T"></typeparam>
50 /// <param name="tree"></param>
51         public void BinTree_LRD<T>(ChainTree<T> tree)
52         {
53             if (tree == null)
54                 return;
55
56             //优先遍历左子树
57             BinTree_LRD(tree.left);
58
59             //然后遍历右子树
60             BinTree_LRD(tree.right);
61
62             //最后输出节点元素
63             Console.Write(tree.data + "\t");
64         }
65         #endregion
66
67         #region 二叉树的按层遍历
68         /// <summary>
69 /// 二叉树的按层遍历
70 /// </summary>
71 /// <typeparam name="T"></typeparam>
72 /// <param name="tree"></param>
73         public void BinTree_Level<T>(ChainTree<T> tree)
74         {
75             if (tree == null)
76                 return;
77
78             //申请保存空间
79             ChainTree<T>[] treeList = new ChainTree<T>[Length];
80
81             int head = 0;
82             int tail = 0;
83
84             //存放数组
85             treeList[tail] = tree;
86
87             //循环链中计算tail位置
88             tail = (tail + 1) % Length;
89
90             while (head != tail)
91             {
92                 var tempNode = treeList[head];
93
94                 head = (head + 1) % Length;
95
96                 //输出节点
97                 Console.Write(tempNode.data + "\t");
98
99                 //如果左子树不为空,则将左子树存于数组的tail位置
100                 if (tempNode.left != null)
101                 {
102                     treeList[tail] = tempNode.left;
103
104                     tail = (tail + 1) % Length;
105                 }
106
107                 //如果右子树不为空,则将右子树存于数组的tail位置
108                 if (tempNode.right != null)
109                 {
110                     treeList[tail] = tempNode.right;
111
112                     tail = (tail + 1) % Length;
113                 }
114             }
115         }
116         #endregion


 

<6> 清空二叉树

           虽然C#里面有GC,但是我们能自己释放的就不麻烦GC了,同样清空二叉树节点,我们用到了递归,说实话,这次练习让我喜欢

       上的递归,虽然XXX的情况下,递归的不是很好,但是递归还是很强大的。
1 #region 清空二叉树
2         /// <summary>
3 /// 清空二叉树
4 /// </summary>
5 /// <typeparam name="T"></typeparam>
6 /// <param name="tree"></param>
7         public void BinTreeClear<T>(ChainTree<T> tree)
8         {
9             //递的结束点,归的起始点
10             if (tree == null)
11                 return;
12
13             BinTreeClear(tree.left);
14             BinTreeClear(tree.right);
15
16             //在归的过程中,释放当前节点的数据空间
17             tree = null;
18         }
19         #endregion


 

最后上一下总的代码


View
Code

 

我们把文章开头的“二叉树”的节点输入到我们的结构中,看看遍历效果咋样。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: