这节,我们说一说二叉树常见的应用的场景。呵呵。。。。。。。。。。。。。。
定义一个哈夫曼树,首先,要高清楚什么是哈夫曼树。所谓哈夫曼树是又叫最优二叉树,指的是对于一组具有确定权值的叶子结点的具有最小带权路径长度的二叉树。
介绍哈夫曼树的一些基本概念。
(1)路径(Path):从树中的一个结点到另一个结点之间的分支构成这两个结点间的路径。
(2)路径长度(Path Length):路径上的分支数。
(3)树的路径长度(Path Length of Tree):从树的根结点到每个结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
(4)结点的权(Weight of Node):在一些应用中,赋予树中结点的一个有实际意义的数。
(5)结点的带权路径长度(Weight Path Length of Node):从该结点到树的根结点的路径长度与该结点的权的乘积。
(6)树的带权路径长度(WPL) :树中所有叶子结点的带权路径长度之和,记为 ∑==n1kkkWPLLW
具体情况,如图a所示:
在下图所示的的四棵二叉树, 都有 4个叶子结点, 其权值分别为 1、 2、 3、4,它们的带权路径长度分别为:
wl=(1+2+3+4)*2=20 如图所示:
wl=(3+4)*3+2*2+1=26 如图所示:
WPL=1×3+2×3+3×2+4×1=19 如图所示:
WPL=2×1+1×2+3×3+4×3=25 如图所示:
那么, 如何构造一棵哈夫曼树呢?哈夫曼最早给出了一个带有一般规律的算法,俗称哈夫曼算法。现叙述如下:
(1)根据给定的n个权值{w1,w2,…,wn},构造n棵只有根结点的二叉树集合F={T1,T2,…,Tn};
(2)从集合 F 中选取两棵根结点的权最小的二叉树作为左右子树,构造一棵新的二叉树, 且置新的二叉树的根结点的权值为其左、 右子树根结点权值之和。
(3)在集合 F 中删除这两棵树,并把新得到的二叉树加入到集合 F 中;
(4)重复上述步骤,直到集合中只有一棵二叉树为止,这棵二叉树就是哈夫曼树。他的步骤如下图所示:
由哈夫曼树的构造算法可知, 用一个数组存放原来的 n个叶子结点和构造过程中临时生成的结点,数组的大小为 2n-1。所以,哈夫曼树类 HuffmanTree中有两个成员字段: data数组用于存放结点, leafNum表示哈夫曼树叶子结点的数目。结点有四个域,一个域 weight,用于存放该结点的权值;一个域 lChild,用于存放该结点的左孩子结点在数组中的序号;一个域 rChild,用于存放该结点的右孩子结点在数组中的序号; 一个域 parent, 用于判定该结点是否已加入哈夫曼树中。哈夫曼树结点的结构为,如图所示:
所以,结点类 Node有 4 个成员字段,weight 表示该结点的权值,lChild和rChild分别表示左、右孩子结点在数组中的序号,parent 表示该结点是否已加入哈夫曼树中,如果 parent的值为-1,表示该结点未加入到哈夫曼树中。当该结点已加入到哈夫曼树中时,parent的值为其双亲结点在数组中的序号。
结点类 Node的定义如下:
public class Node
{
private int weight; //结点权值
private int lChild; //左孩子结点
private int rChild; //右孩子结点
private int parent; //父结点
//结点权值属性
public int Weight
{
get
{
return weight;
}
set
{
weight = value;
}
}
//左孩子结点属性
public int LChild
{
get
{
return lChild;
}
set
{
lChild = value;
}
}
//右孩子结点属性
public int RChild
{
get
{
return rChild;
}
set
{
rChild = value;
}
}
//父结点属性
public int Parent
{
get
{
return parent;
}
set
{
parent = value;
}
}
//构造器
//默认为空 就是空值了
public Node()
{
weight = 0;
lChild = -1;
rChild = -1;
parent = -1;
}
//构造器
//为权重,做结点,有结点都赋值的吧
public Node(int w, int lc, int rc, int p)
{
weight = w;
lChild = lc;
rChild = rc;
parent = p;
}
}
哈夫曼树类 HuffmanTree中只有一个成员方法 Create,它的功能是输入 n个叶子结点的权值,创建一棵哈夫曼树。哈夫曼树类 HuffmanTree的实现如下。 如图所示:
public class HuffmanTree
{
private Node[] data; //结点数组
private int leafNum; //叶子结点数目
//索引器 运用当前的索引器来遍历的吧
public Node this[int index]
{
get
{
return data[index];
}
set
{
data[index] = value;
}
}
//叶子结点数目属性
public int LeafNum
{
get
{
return leafNum;
}
set
{
leafNum = value;
}
}
//构造器 创建一个2*n-1二叉树 带权叶子的结点为n
public HuffmanTree (int n)
{
data = new Node[2*n-1];
leafNum = n;
}
//创建哈夫曼树 在创建哈夫曼树 就开始了
public void Create()
{
int m1;
int m2;
int x1;
int x2;
//输入 n个叶子结点的权值
for (int i = 0; i < this.leafNum; ++i)
{
data[i].Weight = Console.Read();
}
//处理 n 个叶子结点,建立哈夫曼树
for (int i = 0; i < this.leafNum - 1; ++i)
{
max1 = max2 = Int32.MaxValue;
tmp1 = tmp2 = 0;
//找权重的 最小的结点加入到哈夫曼树。
//在全部结点中找权值最小的两个结点
for (int j = 0; j < this.leafNum + i; ++j)
{
if ((data[i].Weight < max1)
&& (data[i].Parent == -1))
{
max2 = max1;
tmp2 = tmp1;
tmp1 = j;
max1 = data[j].Weight;
}
else if ((data[i].Weight < max2)
&& (data[i].Parent == -1))
{
max2 = data[j].Weight;
tmp2 = j;
}
}
data[tmp1].Parent = this.leafNum + i; //加入其中
data[this.leafNum + i].Weight = data[tmp1].Weight
+ data[tmp2].Weight;
data[this.leafNum + i].LChild = tmp1;
data[this.leafNum + i].RChild = tmp2;
}
}
}
//这个算法的时间的复杂度是O(n²) 具体的情况如上图所示。
我们花了这么大的篇幅,介绍了哈夫曼树,他能做什么,就是哈夫曼编码。哈夫曼就是把互相的路径写成01,这个应用最常用的应用,就是想win-zip,win-rar就是基于这个基础。
应用举例二。编写算法,在二叉树中查找值为 value的结点。
算法实现如下:
Node<T> Search(Node<T> root, T value)
{
Node<T> p = root;
if (p == null)
{
return null;
}
if (p.Data.Equals(value))
{
return p;
}
if (p.LChild != null)
{
return Search(p.LChild, value);
}
if (p.RChild != null)
{
return Search(p.RChild, value);
}
return null; 由于用到了递归,这个算法的时间的复杂度是O(n²) 如图所示:
}
统计出二叉树中叶子结点的数目。
就是 统计其中二叉树的没有子节点的数目。通过递归来查找的。递归实现该算法。如果二叉树为空,则返回 0;如果二叉树只有一个结点,则根结点就是叶子结点,返回 1,否则返回根结点的左分支的叶子结点数目和右分支的叶子结点数目的和。
int CountLeafNode(Node<T> root)
{
if (root == null)
{
return 0;
}
else if (root.LChild == null && root.RChild == null)
{
return 1;
}
else
{
return (CountLeafNode(root.LChild) +
CountLeafNode(root.RChild));
}
由于用到了递归算法的实现,其时间复杂度是O(n²),其中的算法的实现,如图所示:
}
最后一个实例,我们来聊聊一下实际的商业的项目中用到到的树的数据结构,就是比如你构建一个父子级的菜单,这个就是树的典型的应用。这在做数据库设计时候,一个字段ParentCode指向了富集。他最终的效果,如图所示:
这就是树形数据结构的,典型应用。下篇文章,我们来讨论一下图形结构。