树形结构-- 一对多的关系
数据结构中,使用树形结构表示数据表素之间一对多的关系,树形结构是一种非线型结构.
定义:
树(Tree)是n(n≥0)个相同数据类型的数据元素的集合.树中的数据元素称为节点(Node).。n=0的树称为空树(Empty Tree);对于n>0的任意非空树T有:
(1)有且仅有一个特殊的结点称为树的根(Root)结点,根没有前驱结点;
(2)若n>1,则除根结点外,其余结点被分成了m(m>0)个互不相交的集合T1,T2,…,Tm,其中每一个集合Ti(1≤i≤m)本身又是一棵树。树T1,T2,…,Tm称为这棵树的子树(Subtree)。
由树的定义可知,树的定义是递归的,用树来定义树。因此,树(以及二叉树)的许多算法都使用了递归。
树的形式定义为:树(Tree)简记为T,是一个二元组,
T = (D, R)
其中:D是结点的有限集合;
R是结点之间关系的有限集合。
树具有下面两个特点:
(1)树的根结点没有前驱结点,除根结点之外的所有结点有且只有一个前驱结点。
(2)树中的所有结点都可以有零个或多个后继结点。
实际上,第(1)个特点表示的就是树形结构的“一对多关系”中的“一”,第(2)特点表示的是“多”。
树的相关术语:
1、结点(Node):表示树中的数据元素,由数据项和数据元素之间的关系组成。在图中,共有10个结点。
2、结点的度(Degree of Node):结点所拥有的子树的个数,在图中,结点A的度为3。
3、树的度(Degree of Tree):树中各结点度的最大值。在图5.1中,树的度为3。
4、叶子结点(Leaf Node):度为0的结点,也叫终端结点。在图5.1中,结点E、F、G、H、I、J都是叶子结点。
5、分支结点(Branch Node):度不为0的结点,也叫非终端结点或内部结点。在图5.1中,结点A、B、C、D是分支结点。
6、孩子(Child):结点子树的根。在图中,结点B、C、D是结点A的孩子。
7、双亲(Parent):结点的上层结点叫该结点的双亲。在图中,结点B、C、D的双亲是结点A。
8、祖先(Ancestor):从根到该结点所经分支上的所有结点。在图中,结点E的祖先是A和B。
9、子孙(Descendant):以某结点为根的子树中的任一结点。在图中,除A之外的所有结点都是A的子孙。
10、兄弟(Brother):同一双亲的孩子。在图5.1中,结点B、C、D互为兄弟。
11、结点的层次(Level of Node):从根结点到树中某结点所经路径上的分支数称为该结点的层次。根结点的层次规定为1,其余结点的层次等于其双亲结点的层次加1。
12、堂兄弟(Sibling):同一层的双亲不同的结点。在图中,G和H互为堂兄弟。
13、树的深度(Depth of Tree):树中结点的最大层次数。在图5.1中,树的深度为3。
14、无序树(Unordered Tree):树中任意一个结点的各孩子结点之间的次序构成无关紧要的树。通常树指无序树。
15、有序树(Ordered Tree):树中任意一个结点的各孩子结点有严格排列次序的树。二叉树是有序树,因为二叉树中每个孩子结点都确切定义为是该结点的左孩子结点还是右孩子结点。
16、森林(Forest):m(m≥0)棵树的集合。自然界中的树和森林的概念差别很大,但在数据结构中树和森林的概念差别很小。从定义可知,一棵树有根结点和m个子树构成,若把树的根结点删除,则树变成了包含m棵树的森林。当然,根据定义,一棵树也可以称为森林。
树的逻辑表示
二叉树的定义(BinaryTree):
每个节点至多有两个子树。
二叉树有左右之分,不可颠倒。如果把左右子树顺序颠倒则变了另一个全新的二叉树。
二叉树具有5种形态:
1)满二叉树(Full Binary Tree):如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树,如图所示。
由定义可知,对于深度为k的满二叉树的结点个数为2k-1。(24-1=15)
(2)完全二叉树(Complete Binary Tree):深度为k,有n个结点的二叉树当且仅当其每一个结点都与深度为k,有n个结点的满二叉树中编号从1到n的结点一一对应时,称为完全二叉树,如图所示。完全二叉树的特点是叶子结点只可能出现在层次最大的两层上,并且某个结点的左分支下子孙的最大层次与右分支下子孙的最大层次相等或大1。
二叉树的性质
性质1 一棵非空二叉树的第i层上最多有2i-1个结点(i≥1)。
性质2 若规定空树的深度为0,则深度为k的二叉树最多有2k-1个结点(k≥0)。
性质3 具有n个结点的完全二叉树的深度k为log2n+1。
性质4 对于一棵非空二叉树,如果度为0的结点数目为n0,度为2的结点数目为n2,则有n0= n2+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,则该结点无右孩子。
二叉树的存储结构--顺序存储
顺序存储:
1 把一个满二叉树从上到下,从左到右顺序编号,依次放到数组中,可得到如图示结果。
2 设满二叉树节点在数组中的索引为i,那么有如下性质:
(1)如果i=0,此节点为根节点,无双亲。
(2)如果i>0,则其双亲为(i-1)/2。(结果取整)
(3)结点i的左孩子为2i+1,右孩子为2i+2.
(4) 当i>0时,当i为奇数时,它是双亲节点的左孩子,它的兄弟为i+1;当i为偶数时,它是双亲节点的右孩子,它的兄弟为i-1.
(5) 深度为K的满二叉树需要长度为2k-1的数组存储。
二叉树以及其节点的实现
1. 二叉树节点的定义及实现
根据二叉树的特点可知,二叉树的子节点最多有两个子树,分别是左子树和右子树。所以,一个二叉树的节点应有三个部分组成:
² 当前节点数据<T>;
² 左子节点的引用;
² 右子节点的引用。
如下图左部分的Node类定义。
Node类的实现:
/// <summary>
/// 二叉树节点代码
/// </summary>
/// <typeparam name="T"></typeparam>
public class Node<T>
{
private T _data;
private Node<T> _lChild;
private Node<T> _rChild;
/// <summary>
/// 二叉树当前节点的数据
/// </summary>
public T Data
{
get { return _data; }
set { _data = value; }
}
/// <summary>
/// 二叉树左子节点的引用
/// </summary>
public Node<T> LChild
{
get { return _lChild; }
set { _lChild = value; }
}
/// <summary>
/// 二叉树右子节点的引用
/// </summary>
public Node<T> RChild
{
get { return _rChild; }
set { _rChild = value; }
}
public Node()
{
_data = default(T);
_lChild = null;
_rChild = null;
}
public Node(T _data)
{
this._data = _data;
_lChild = null;
_rChild = null;
}
public Node(T _data, Node<T> _lChild, Node<T> _rChild)
{
this._data = _data;
this._lChild = _lChild;
this._rChild = _rChild;
}
public Node(Node<T> _lChild, Node<T> _rChild)
{
this._lChild = _lChild;
this._rChild = _rChild;
}
}
2 二叉树的实现
二叉树的实现主要有能下几个功能:
Ø 构造一个新的二叉树
Ø 插入指定节点的左子节点;
Ø 插入指定节点的右子节点;
Ø 删除指定节点的左子节点;
Ø 删除指定节点的右子节点;
Ø 判断二叉树是否为空;
Ø 判断节点是否为叶子节点。
代码实现如下:
1 /// <summary>
2 /// 二叉树的操作实现
3 /// </summary>
4 /// <typeparam name="T"></typeparam>
5 public class BinaryTree<T>
6 {
7 private Node<T> _header;
8
9
10 ..构造函数..
30
31 /// <summary>
32 /// 二叉树根节点(头节点)
33 /// </summary>
34 public Node<T> Header
35 {
36 get { return _header; }
37 set { _header = value; }
38 }
39
40 /// <summary>
41 /// 插入左子节点
42 /// </summary>
43 /// <param name="_fNode"></param>
44 /// <param name="_data"></param>
45 public void InsertL(Node<T> _fNode, T _data)
46 {
47 /*
48 * 步骤:
49 * 1 New新的节点
50 * 2 新节点引用要原父节点的左子节点
51 * 3 父节点的左子节点引用新节点
52 */
53 Node<T> _node = new Node<T>(_data);
54 _node.LChild = _fNode.LChild;
55 _fNode.LChild = _node;
56 }
57
58 /// <summary>
59 /// 插入右子节点
60 /// </summary>
61 /// <param name="_fNode"></param>
62 /// <param name="_data"></param>
63 public void InsertR(Node<T> _fNode, T _data)
64 {
65 // 思路同插入左子节点
66 Node<T> _node = new Node<T>(_data);
67 _node.LChild = _fNode.RChild;
68 _fNode.RChild = _node;
69 }
70
71
72 /// <summary>
73 /// 删除指定节点的左子节点
74 /// </summary>
75 /// <param name="node"></param>
76 public Node<T> DeleteL(Node<T> node)
77 {
78 if (node == null || node.LChild == null)
79 {
80 return null;
81 }
82 /*
83 * 步骤:
84 * 1 获取左子节点的引用
85 * 2 把指定节点的左子节点引用设为NULL
86 * 3 返回左子节点的引用
87 */
88 Node<T> lChild = node.LChild;
89 node.LChild = null;
90 return lChild;
91 }
92
93 /// <summary>
94 /// 删除指定节点的右子节点
95 /// </summary>
96 /// <param name="node"></param>
97 /// <returns></returns>
98 public Node<T> DeleteR(Node<T> node)
99 {
100 if (node == null || node.RChild == null)
101 {
102 return null;
103 }
104 // 步骤同删除指定节点的左子节点相同,只不过是首先获取的是右子节点的引用
105 Node<T> rChild = node.RChild;
106 node.RChild = null;
107 return rChild;
108 }
109
110 /// <summary>
111 /// 判断是否为空
112 /// </summary>
113 /// <returns></returns>
114 public bool IsEmpty()
115 {
116 return _header == null;
117 }
118
119 /// <summary>
120 /// 判断指定的节点是否为叶子节点
121 /// </summary>
122 /// <param name="_node"></param>
123 /// <returns></returns>
124 public bool IsLeaf(Node<T> _node)
125 {
126 // 叶子结点(Leaf Node):度为0的结点,也叫终端结点
127 return (_node != null && _node.LChild == null && _node.RChild == null);
128 }
129 }
3 二叉树的遍历
二叉树的遍历是按照某种顺序对树中的每个节点访问且只能访问一次的过程,包括查询、修改、计算等。
二叉树的遍历实际是将非线性结构线性化,它是二叉树的各种计算和操作的基础。
由二叉树的定义可知,一棵二叉树由根结点、左子树和右子树三部分组成,若规定D、L、R分别代表遍历根结点、遍历左子树、遍历右子树,则二叉树的遍历方式有6种:DLR、DRL、LDR、LRD、RDL、RLD。由于先遍历左子树和先遍历右子树在算法设计上没有本质区别,所以,只讨论三种方式:DLR(先序遍历)、LDR(中序遍历)和LRD(后序遍历)。
先序遍历:先访问根节点,再访问左子树,再访问右子树。
中序遍历:先访问左子树,再访问根节点,最后访问右子树。
后序遍历:先访问左子以,再访问右子树,最后访问根节点。
3.1 先序遍历(DLR)
先序遍历的基本思想是:首先访问根结点,然后先序遍历其左子树,最后先序遍历其右子树。先序遍历的递归算法实现如下。(这里只是获取节点的值)
/// <summary>
/// 先序遍历之递归
/// </summary>
/// <param name="node"></param>
/// <returns></returns>
public string perOrderNode(Node< char> node)
{
if (node == null)
return null;
StringBuilder nodeValues = new StringBuilder();
nodeValues.Append(node.Data);
// 递归调用
nodeValues.Append(perOrderNode(node.LChild));
nodeValues.Append(perOrderNode(node.RChild));
return nodeValues.ToString();
}
public string perOrderRecursion(BinaryTree.BinaryTree< char> tree)
{
return perOrderNode(tree.Header);
}
非递归算法实现:
/// <summary>
/// 二叉树先序遍历之非递归算法
/// 利用栈实现二叉树的遍历.
/// </summary>
/// <param name="tree"></param>
/// <returns></returns>
public string perOrderNoRecursion(BinaryTree.BinaryTree< char> tree)
{
StringBuilder result = new StringBuilder();
Stack<Node< char>> rightStack = new Stack<Node< char>>();
Node< char> curNode = tree.Header;
while (curNode != null || rightStack.Count != 0)
{
if (curNode != null)
{
result.Append(curNode.Data);
if (curNode.RChild != null)
{
// 如果右子节点不为NULL,则先放入Stack中.
rightStack.Push(curNode.RChild);
}
curNode = curNode.LChild;
}
else
{
curNode = rightStack.Pop();
}
}
return result.ToString();
}
3.2 中序遍历(LDR)
中序遍历的基本思想是:首先中序遍历根结点的左子树,然后访问根结点,最后中序遍历其右子树。中序遍历的递归算法实现如下: (附件)
3.3 后序遍历(LRD)
后序遍历的基本思想是:首先后序遍历根结点的左子树,然后后序遍历根结点的右子树,最后访问根结点。后序遍历的递归算法实现如下: (附件)
<a title="代码下载" href="http://files.cnblogs.com/wservices/BinaryTree.rar">代码下载</a>