数据结构之树(四)

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=1nwi×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中只剩下一棵树为止.
所以从上述构建哈夫曼树的过程可以看出来,哈夫曼树的有以下的几个特点:

  1. 每个初始结点最终都成为叶结点,并且权值越小的结点到根结点的路径长度越大;
  2. 构建过程中共新建了 N − 1 N-1 N1个结点(双分支节点),因此哈夫曼树中结点总数为 2 N − 1 2N-1 2N1;
  3. 每次构造都选择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表示左子树还是右子树并没有明确的规定,它仅仅是标记的一种的符号表示而已.因此,左结点、右结点的顺序是任意的,所以构造出的哈夫曼树并不是唯一的,但是各个哈夫曼的带权路径长度相同并且为最优的。

小结

本小结以哈夫曼树编码问题为应用来了解树在实际问题中的基本应用,接下来的博文会在其他方面进一步解决实际问题.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树。在哈夫曼树中,每个叶子节点都有一个权值,而非叶子节点没有权值。哈夫曼树的构造过程是通过贪心算法实现的,即每次选择权值最小的两个节点合并成一个新节点,直到最后只剩下一个节点为止。哈夫曼树的应用非常广泛,其中最常见的应用是数据压缩。 以下是构造哈夫曼树和哈夫曼编码的步骤: 1. 统计每个字符出现的频率,并将它们作为叶子节点的权值。 2. 将所有的叶子节点按照权值从小到大排序。 3. 选择权值最小的两个节点合并成一个新节点,新节点的权值为这两个节点的权值之和。 4. 将新节点插入到原来的节点序列中,并重新按照权值从小到大排序。 5. 重复步骤3和步骤4,直到只剩下一个节点为止,这个节点就是哈夫曼树的根节点。 6. 对于每个叶子节点,从它到根节点的路径上标记0或1,得到每个字符的哈夫曼编码。 以下是Python实现哈夫曼树和哈夫曼编码的代码: ```python import heapq from collections import defaultdict def huffman_encoding(data): # 统计每个字符出现的频率 freq = defaultdict(int) for c in data: freq[c] += 1 # 将每个字符作为一个叶子节点,并将它们加入到优先队列中 heap = [[weight, [char, ""]] for char, weight in freq.items()] heapq.heapify(heap) # 合并节点,构造哈夫曼树 while len(heap) > 1: left = heapq.heappop(heap) right = heapq.heappop(heap) for pair in left[1:]: pair[1] = '0' + pair[1] for pair in right[1:]: pair[1] = '1' + pair[1] heapq.heappush(heap, [left[0] + right[0]] + left[1:] + right[1:]) # 得到每个字符的哈夫曼编码 huffman_code = dict(heapq.heappop(heap)[1:]) return huffman_code def huffman_decoding(data, huffman_code): # 将哈夫曼编码转换为反向字典 reverse_code = {v: k for k, v in huffman_code.items()} # 解码数据 current_code = "" decoded_data = "" for bit in data: current_code += bit if current_code in reverse_code: decoded_data += reverse_code[current_code] current_code = "" return decoded_data ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值