5. 树与二叉树的应用
树在实际问题解决中有很多方面,例如在编码方面的哈夫曼树,在排序方面的AVL树、红黑树等等。从本小结开始实际应用到树在解决实际问题中的应用.
5.1 哈夫曼树
5.1.1 哈夫曼树的定义
在实际应用当中,有些问题可以将树中的结点赋予一定的数值以表示某种意义上的权值,这个数值一般地称为该结点的权值.从树根结点到任意结点的路径长度(经过的边数量)与该结点上权值的乘积称为该结点的带权路径长度.树中所有叶结点的带权路径长度之和称为该树的带权路径长度,记为
W
P
L
=
∑
i
=
1
n
w
i
×
l
i
WPL=\sum\limits_{i=1}^{n}w_{i}\times{l_{i}}
WPL=i=1∑nwi×li
上式中,
w
i
w_{i}
wi是第
i
i
i个叶结点所带的权值;
l
i
l_{i}
li是该叶结点到根结点的路径长度.
在含有
N
N
N个带权叶子结点的二叉树中,其中带权路径长度(
W
P
L
WPL
WPL)最小的二叉树称为哈夫曼树,也称为最优二叉树.这样构造的树一般地称为哈夫曼树.
5.1.2 哈夫曼树的构造
给定
N
N
N个权值分别为
w
1
,
w
2
,
…
,
w
n
w_{1},w_{2},\dots,w_{n}
w1,w2,…,wn的结点.通过哈夫曼算法可以构造出最优二叉树,算法的描述如下所示:
① 将这
N
N
N个结点分别作为
N
N
N棵树仅含有一个结点的二叉树,构成森林
F
F
F;
② 构造一个新结点,并从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和;
③ 从
F
F
F中删除刚才选出的两棵树,同时将新得到的树加入到森林
F
F
F当中;
④ 重复步骤①、②、③,直到
F
F
F中只剩下一棵树为止.
所以从上述构建哈夫曼树的过程可以看出来,哈夫曼树的有以下的几个特点:
- 每个初始结点最终都成为叶结点,并且权值越小的结点到根结点的路径长度越大;
- 构建过程中共新建了 N − 1 N-1 N−1个结点(双分支节点),因此哈夫曼树中结点总数为 2 N − 1 2N-1 2N−1;
- 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点.
哈夫曼树构造的算法如下所示
template<typename ElementType>
void CreateHuffmanTree(ElementType arr[],unsigned int n){
Node<BiNode<ElementType>*>* list = NULL;
for(unsigned int k=0;k<n;k++){
BiNode<ElementType>* ptr = new BiNode<ElementType>(arr[k]);
Node<BiNode<ElementType>*>* tmpptr = new Node<BiNode<ElementType>*>(ptr,list);;
list = tmpptr;
}
//排序
Node<BiNode<ElementType>*>*ptr = NULL;
QuickSort(list,ptr);
while(list->GetNode()!=NULL){
Node<BiNode<ElementType>*>*ptr1 = list;
Node<BiNode<ElementType>*>*ptr2 = list->GetNode();
list = ptr2->GetNode();
ElementType weight = ptr1->GetData()->GetData() + ptr2->GetData()->GetData();
BiNode<ElementType>* biptr = new BiNode<ElementType>(weight,ptr1->GetData(),ptr2->GetData());
ptr = new Node<BiNode<ElementType>*>(biptr);
ptr->SetNode(list);
list = ptr;
ptr = NULL;
QuickSort(list,ptr);
delete ptr1;
delete ptr2;
}
ptr = list->GetData();
delete list;
return ptr;
}
这里的排序算法使用到了链表中的快速排序算法
template<typename ElementType>
void QuickSort(Node<BiNode<ElementType>*>* begin ,Node<BiNode<ElementType>*>* end){
if (begin!=end && begin->GetNode()!=end){
Node<BiNode<ElementType>*>* ptr=begin;
while(ptr->GetNode()!=end) ptr=ptr->GetNode();
BiNode<ElementType>* pivot= ptr->GetData();
Node<BiNode<ElementType>*>* p1=begin;
Node<BiNode<ElementType>*>* p2=begin;
while(p2->GetNode()!=end){
if (p2->GetData()->GetData()<pivot->GetData()){
BiNode<ElementType>* tmp= p2->GetData();
p2->SetData(p1->GetData());
p1->SetData(tmp);
p1=p1->GetNode();
}
p2=p2->GetNode();
}
p2->SetData(p1->GetData());
p1->SetData(pivot);
QuickSort(begin,p1);
QuickSort(p1->GetNode(), end);
}
}
5.1.3 哈夫曼编码
对于待处理的一个字符串序列,如果对于每个字符用同样长度的二进制位来表示,则称这种编码方式为固定场编码.若允许对不字符串用不等长的二进制位表示,则这种方式称为可变长度编码.特别地.可变长度的编码可以高效地对频率较高的字符赋予短编码,而对频率较低的字符则赋予较长的编码,这样就会使得字符平均编码长度减短,起到数据压缩的效果.哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码.
若没有一个编码是另外一个编码的前缀,这样的编码为前缀编码,对这样编码解码也是非常容易的.
由哈夫曼树得到哈夫曼编码是非常自然的过程.首先,将每个出现的字符当做一个独立的结点,其权值为它出现的频率(或次数),构造出对应的哈夫曼树.显然所有字符结点都出现在叶结点中.我们可以将字符的编码解释为从根结点到该字符的路径上边标记的序列,其中边标记为0表示"转向左孩子",标记为1表示"转向右孩子".
举个非常简单的例子,例如对下列字符进行编码(数字代表的是字符出现的频率大小):
A:3
B:5
C:12
D:15
E:18
F:56
G:74
H:78
I:86
J:89
K:98
L:99
通过进行哈夫曼编码计算,可以得到哈夫曼编码树如下图所示
计算哈夫曼编码如下所示
template<typename ElementType>
void GetCodesWithSt(BiNode<ElementType>* root){//获取哈夫曼编码
if(root==NULL) return ;
Stack<BiNode<ElementType>*>st;
BiNode<ElementType>*prev=NULL;
BiNode<ElementType>*ptr = root;
while(!st.IsEmpty()||ptr!=NULL){
while(ptr!=NULL){
st.Push(ptr);
ptr=ptr->GetLeft();
}
ptr = st.Top();
if(ptr->GetRight()==NULL||ptr->GetRight()==prev){
if(ptr->GetLeft()==NULL && ptr->GetRight()==NULL){
Stack<BiNode<ElementType>*>tmpst;
BiNode<ElementType>*nextptr = NULL;
std::string tmpcodes = "";
while(!st.IsEmpty()){
BiNode<ElementType>*tmpptr = st.Pop();
if(nextptr!=NULL){
if(tmpptr->GetLeft()==nextptr) tmpcodes="0"+tmpcodes;
if(tmpptr->GetRight()==nextptr) tmpcodes="1"+tmpcodes;
}
nextptr = tmpptr;
tmpst.Push(tmpptr);
}
std::cout<<ptr->GetData()<<": "<<tmpcodes<<std::endl;
while(!tmpst.IsEmpty()) st.Push(tmpst.Pop());
}
prev = ptr;
ptr = NULL;
st.Pop();
}else ptr= ptr->GetRight();
}
std::cout<<std::endl;
}
计算权重值如下所示
template<typename ElementType>
ElementType GetWeightsWithSt(BiNode<ElementType>*root){
if (root==NULL) return 0;
BiNode<ElementType>*ptr = root;
BiNode<ElementType>*prev = NULL;
Stack<BiNode<ElementType>*>st;
ElementType weights = 0;
while(ptr!=NULL||!st.IsEmpty()){
while(ptr!=NULL){
st.Push(ptr);
ptr=ptr->GetLeft();
}
ptr = st.Top();
if(ptr->GetRight()==NULL || ptr->GetRight()==prev){
if(ptr->GetRight()==NULL){
unsigned int Depth = st.Size();
weights = weights + (Depth-1)*(ptr->GetData());
}
prev = ptr;
ptr=NULL;
st.Pop();
}else ptr=ptr->GetRight();
}
return weights;
}
经过计算机的计算可以得到如下的结果
PreOrder: 633 261 109 53 20 8 3 5 12 33 15 18 56 152 74 78 372 175 86 89 197 98 99
InOrder: 3 8 5 20 12 53 15 33 18 109 56 261 74 152 78 633 86 175 89 372 98 197 99
Weights: 2013
Leaves: 3 5 12 15 18 56 74 78 86 89 98 99
All Paths:
3: 000000
5: 000001
12: 00001
15: 00010
18: 00011
56: 001
74: 010
78: 011
86: 100
89: 101
98: 110
99: 111
注意:究竟0和1表示左子树还是右子树并没有明确的规定,它仅仅是标记的一种的符号表示而已.因此,左结点、右结点的顺序是任意的,所以构造出的哈夫曼树并不是唯一的,但是各个哈夫曼的带权路径长度相同并且为最优的。
小结
本小结以哈夫曼树编码问题为应用来了解树在实际问题中的基本应用,接下来的博文会在其他方面进一步解决实际问题.