文章目录
一、概念:树(Tree)是n(n>=0)个结点的有限集。n= 0时称为空树。
在任意一颗非空树中:
(1)有且只有一个特定的称为根(Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、………、Tm,其中每个集合本身又是一棵树,并且称为根的子树(SubTree)。
注意:
1、n>0时根结点是唯一的,不能存在多个根结点;
2、m>0时,子树的个数没有限制,但他们一定是互不相交的。
二、结点
树的结点包含一个数据元素及若干指向其子树的分支。
结点拥有的子树的个数称为结点的度(Degree)。度为零的结点称为叶结点(Leaf)或终端结点;度不为零的结点称为非终端结点或分支结点。除根结点外,分支结点也称内部结点,树的度是树内个节点的度的最大值。
三、结点间关系:
结点的子树的根称为该结点的孩子(child),相应的,该结点称为孩子结点的双亲(parent)。
同一个双亲的孩子之间互称为兄弟(Sibling);
结点的祖先是从根到该结点所经分支上的所有结点。反之,某结点为根的子树中的任意结点都称为该结点的子孙。
结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。某结点在第i层,其孩子结点在第i+1层;
其双亲在同一层的结点互为堂兄弟
树中结点的最大层次称为树的深度(Depth)或这高度.
如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树;
森林(Forest)是m(m>=0)颗互不相交的树的集合。对树中的每个结点而言,其子树的集合即为森林。
线性结构与树结构对比:
线性结构 | 树结构 |
---|---|
第一个数据元素无前驱 | 根结点,无双亲,唯一 |
最后一个数据元素,无后继 | 叶结点,无孩子,可以多个 |
中间元素,一个前驱一个后继 | 中间结点,一个双亲多个孩子 |
四:树的存储结构(双亲表示法、孩子表示法、双亲孩子表示法)
1、双亲表示法:
假设以一组连续空间存储树的结点,同时每个结点中,附设一个指示器,指向其双亲系结点到链表中的位置。,
data是数据域,存储结点的数据信息,而parent是指针域,存储该结点的双亲在数组中的下标。
2、孩子表示法
由于树中每个结点可能有多颗子树,可以考虑用 多重链表,即每个结点有多个指针域,其中每个指针指向一颗子树的根结点,这种方法叫做多重链表表示法。
方案一:
指针域的个数就等于树的度。
其中data是数据域。child1到childd是指针域,用来指向该结点的孩子结点。
缺点:树中各结点的度各不相同,会浪费空间
方案二、
每个结点的指针域的个数等于该结点的度。专门取一个位置来存储该结点的指针域的个数。
其中data为数据域,degree为度域,也就是存储该结点的孩子结点的个数,child1到childd为指针域,指向该结点的各个孩子的结点。
空间效率高了,但是时间效率降低。
孩子表示法:把每个结点的孩子结点排列起来,以单链表作为存储结构,则n个结点有n个孩子的链表,如果是叶子结点则此单链表为空,然后n个头指针又组成一个线性表,采用顺序存储结构,存放进一个一维数组中。
3、孩子兄弟表示法:
任意一颗树,它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的.因此,设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
其中data为数据域,firstchild为指针域,存储该结点的第一个孩子结点的存储地址,rightsib是指针域,存储该结点的右兄弟结点的存储地址。
五、二叉树的定义:
1、二叉树(Binary Tree):二叉树是n(n>=0)个结点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树组成。
2、二叉树的特点:
○ 每个结点最多有两颗子树,所以二叉树中不存在度大于二的结点,注意不是只有两颗子树,而是最多有,没有子树或者只有一颗都是可以的。
○ 左子树和右子树是有顺序的,次序不能任意颠倒。
○ 即使树中某结点只有一个子树。也要区分它是左子树还是右子树。
3、二叉树的五种基本形态:
a. 空二叉树
b. 只有一个根结点
c. 根结点只有左子树
d. 根结点只有右子树
e. 根结点既有左子树又有右子树
3、特殊二叉树
a. 斜树:所有的结点都只有左子树的二叉树叫左斜树
所有结点都是只有右子树的二叉树叫右边斜树
b. 满二叉树:在一颗二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子结点都在同层上,这样的二叉树称为满二叉树
满二叉树的特点:
(1)叶子只能出现在最下层,
(2)非叶子结点的度一定是2,
(3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多
c. 完全二叉树:
对一颗具有n个结点的二叉树按层排序,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这颗二叉树成为完全二叉树
满二叉树一定是一颗完全二叉树,但是完全二叉树不一定是满的。
完全二叉树的特点:
§ 叶子结点只能出现在最下两层
§ 最下层的叶子一定集中在左部连续位置
§ 倒数二层,若有叶子结点,则一定都在右部连续位置
§ 如果结点度为1,则结点只有左孩子,即不存在只有右子树的情况
§ 同样结点数的二叉树,完全二叉树的深度最小
六、二叉树的性质:
1. 在二叉树的第i层上至多有2^(i-1)个结点(i>=1);
第一层2^0;
第二层2^1
第三层2^2……
2. 深度为K的二叉树至多有2^k -1个结点;
一层至多有2^1 -1 = 1;
二层至多有2^2 -1 = 3;
三层至多有2^3 -1 = 7……
3. 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0 = n2 +1;
4. 具有n个结点的完全二叉树的深度为
整数。
5. 如果对一棵有n个结点的完全二叉树(其深度为[log2n](向下取整)+1)的结点按层序编号(从第1层到第[log2n](向下取整)+1层,每层从左到右),对任一结点i(1<=i<=n)有:
a. 如果i=1,则结点i是二叉树的根,无双亲;如果i>1,其双亲是结点[i/2](向下取整)。
b. 如果2i>n,则结点i无左孩子(结点i为叶子结点);否则其左孩子是结点2i;
c. 如果2i+1 >n,则结点i无右孩子,否则其右孩子是结点2i+1;
七、二叉树的存储结构:
1、顺序结构一般只用于完全二叉树
2、二叉链表;
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,这样的链表称为二叉链表
Lchild | Data | RChild |
---|
其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。
namespace TreeNode
{
class TreeNode<T>
{
private T data; //数据域
private TreeNode<T> lChild; //左孩子
private TreeNode<T> rChild;
public TreeNode(T val, TreeNode<T> lp, TreeNode<T> rp)
{
data = val;
lChild = lp;
rChild = rp;
}
public TreeNode(TreeNode<T> lp, TreeNode<T> rp)
{
data = default(T);
lChild = lp;
rChild = rp;
}
public TreeNode(T val)
{
data = val;
lChild = null;
rChild = null;
}
public T Data
{
get { return data; }
set { data = value; }
}
public TreeNode<T> LChild
{
get { return lChild; }
set { lChild = value; }
}
public TreeNode<T> RChild
{
get { return lChild; }
set { rChild = value; }
}
}
}
3、二叉树的遍历方法:
二叉树的遍历(traversing binary tree)是指从结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
a. 前序遍历
若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树
b. 中序遍历
若树为空,则空操作返回,否则从跟结开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后访问根结点,最后中序遍历右子树。
c. 后序遍历:
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问在右子树,最后访问根结点。
d. 层序遍历:
若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
namespace TraversingBinaryTree
{
[DebuggerDisplay("Value = {value}")]
public class Tree
{
public string Value;
public Tree Left;
public Tree Right;
}
class Program
{
static void Main(string[] args)
{
Tree newTree = CreatFakeTree();
PreOrder(newTree);
PreOrderNoRecursion(newTree);
}
/// <summary>
/// 新建一个二叉树
/// </summary>
/// <returns></returns>
public static Tree CreatFakeTree()
{
Tree tree = new Tree() { Value = "A" };
tree.Left = new Tree()
{
Value = "B",
Left = new Tree() { Value = "D", Left = new Tree() { Value = "G" } },
Right = new Tree() { Value = "E", Right = new Tree() { Value = "H" } }
};
tree.Right = new Tree() { Value = "C", Right = new Tree() { Value = "F" } };
return tree;
}
#region 前序遍历的递归和非递归实现
/// <summary>
/// 递归实现二叉树的前序遍历
/// </summary>
/// <param name="tree"></param>
public static void PreOrder(Tree tree)
{
if (tree == null)
return;
Console.WriteLine(tree.Value);
PreOrder(tree.Left);
PreOrder(tree.Right);
}
/// <summary>
/// 非递归实现二叉树的前序遍历
/// </summary>
/// <param name="tree"></param>
public static void PreOrderNoRecursion(Tree tree)
{
if (tree ==null)
{
return;
}
Stack<Tree> stack = new Stack<Tree>();
Tree node = tree;
while (node!=null || stack.Any())
{
if (node!=null)
{
stack.Push(node);
Console.WriteLine(node.Value);
node = node.Left;
}
else
{
var item = stack.Pop();
node = item.Right;
}
}
}
#endregion
#region 中序遍历的递归和非递归实现
/// <summary>
/// 递归实现二叉树的中序遍历
/// </summary>
/// <param name="tree"></param>
public static void InOrder(Tree tree)
{
if (tree ==null)
{
return;
}
InOrder(tree);
Console.WriteLine(tree.Value);
InOrder(tree);
}
/// <summary>
/// 非递归实现二叉树的中序遍历
/// </summary>
/// <param name="tree"></param>
public static void InOrderNoRecursion(Tree tree)
{
if (tree ==null)
{
return;
}
Stack<Tree> stack = new Stack<Tree>();
Tree node = tree;
while (node != null || stack.Any())
{
if (tree != null)
{
stack.Push(node);
node = node.Left;
}
else
{
var item = stack.Pop();
Console.WriteLine(item.Value);
node = item.Right;
}
}
}
#endregion
#region 后序遍历的递归和非递归实现
/// <summary>
/// 后序遍历的递归实现
/// </summary>
/// <param name="tree"></param>
public static void PostOrder(Tree tree)
{
if (tree == null)
{
return;
}
PostOrder(tree.Left);
PostOrder(tree.Right);
Console.WriteLine(tree.Value);
}
/// <summary>
/// 后序遍历的非递归实现(一)
/// </summary>
/// <param name="tree"></param>
public static void PostOrderNoRecursion(Tree tree)
{
if (tree == null)
{
return;
}
Stack<Tree> stack = new Stack<Tree>();
Tree node = tree;
Tree pre = null;
stack.Push(node);
while (stack.Any())
{
node = stack.Peek();
if ((node.Left ==null&& node.Right == null)||(pre!=null
&&(pre == node.Left||pre==node.Right)))
{
Console.WriteLine(node.Value);
pre = node;
stack.Pop();
}
else
{
if (node.Right != null)
stack.Push(node.Right);
if (node.Left != null)
stack.Push(node.Left);
}
}
}
/// <summary>
/// 后序遍历的非递归实现(二)
/// </summary>
/// <param name="tree"></param>
public static void PostOrderNoRecursion2(Tree tree)
{
HashSet<Tree> visited = new HashSet<Tree>();
Stack<Tree> stack = new Stack<Tree>();
Tree node = tree;
while (node!=null || stack.Any())
{
if (node!=null)
{
stack.Push(node);
node = node.Left;
}
else
{
var item = stack.Peek();
if (item.Right!=null&&!visited.Contains(item.Right))
{
node = item.Right;
}
else
{
Console.WriteLine(item.Value);
visited.Add(item);
stack.Pop();
}
}
}
}
#endregion
#region 层序遍历的实现
/// <summary>
/// 层序遍历的实现
/// </summary>
/// <param name="tree"></param>
public static void LevelOrder(Tree tree)
{
if (tree == null)
{
return;
}
Queue<Tree> queue = new Queue<Tree>();
queue.Enqueue(tree);
while (queue.Any())
{
var item = queue.Dequeue();
Console.WriteLine(item.Value);
if (item.Left!= null)
{
queue.Enqueue(item.Left);
}
if (item.Right!=null)
{
queue.Enqueue(item.Right);
}
}
}
#endregion
}
}
八、树、森林与二叉树的转换
1、树转换为二叉树
a. 加线。在所有兄弟结点之间加一条线
b. 去线。对树中每个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线。
c. 层次调整。以树的跟结点为轴心,将正棵树旋转一定的角度,使之结构层次分明。注意第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子就是结点的右孩子。
2、森林转换为二叉树
森林是由若干树组成的,可以理解为,森林中的每一颗树都是兄弟,可以按照兄弟的处理办法来操作。
a. 把每个树转换为二叉树。
b. 第一颗二叉树不动,从第二棵树开始,依次把后一颗二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。当所有的二叉树连接起来后就得到由森林转换而来的二叉树。
3、二叉树转换为树
二叉树转换为树是树转换为二叉树的逆过程,
a、加线。若某结点的左孩子结点存在,则将这个左孩子的右孩子结点,右孩子的右孩子结点,右孩子的右孩子的右孩子结点 ……,左孩子的n右孩子都作为此结点的孩子。将该结点与这些右孩子结点用线连接起来。
b、去线。删除原二叉树中所有结点与其右孩子结点的连线。
c、层次调整。使之结构层次分明。
4、二叉树转换为森林:
如果一棵二叉树的根结点有右孩子,则能转换为森林,如果没有,则不能。
步骤:
a. 从跟结点开始,若右孩子存在,则把与右孩子结点的连线删除,再查看分离后的二叉树,若右孩子存在,则连线删除……,直到所有右孩子连线都删除为止,得到分离的二叉树。
b. 再将没棵分离后的二叉树转换为树即可。
九、树和森林的遍历:
1、树的遍历分两种方式:
a. 先根遍历树:先访问树的根结点,然后依次先根遍历根的没棵子树;
b. 后根遍历树:即先依次后根遍历每课子树,然后在访问根结点。
下图先根遍历序列为 ABEFCDG
下图先根遍历序列为 EFBCGDA
2、森林的遍历分两种方式:
a. 前序遍历:先访问森林中的第一颗树的根结点,然后在依次先根遍历每棵子树,再依次用同样的方式遍历除去第一棵树的剩余树构成的森林。
b. 后序遍历:先访问森林中的第一颗树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。
下图森林的前序遍历的结果是ABCDEFGHJI
前序遍历的结果是BCDAFEJHIG
九、赫夫曼树———赫夫曼编码(压缩编码方法)
路径长度:从树中一个结点到另一个结点之间的分支构成两个节点之间的路径,路径上的分支数目称作路径长度。
树的路径长度:树的路径长度就是从树根到每一个结点的路径长度之和。
赫夫曼树:假设有n个权值{w1,w2,……,wn},构造一颗有n个叶子结点的二叉树,每个叶子结点带权wk,每个叶子的路径长度为lk,则wk*lk称为称为叶子结点的带权路径长度,
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。其中带权路径WPL长度最小的的e二叉树称为赫夫曼树。