前言:
哈夫曼树实际是一种编码方式,主要用在压缩数据,其本质是求解带权路径的最小值的编排方式。
哈夫曼树:
1、定义:是一种特殊的二叉树,被称为“最优二叉树”。即带权路径长度最短。带权路径可以理解为到这些叶子结点的耗费之和
2、权:赋予某个实体的一个量,也就是这个结点出现的频率
3、结点的带权路径长度:从该结点到树根之间的路径长度与结点上权的乘积
4、树的带权路径长度:树中所有叶子结点的带权路径长度之和,就是走完这些结点所需要的总耗费
图解如下:
5、带权路径长度:也等于所有根节点之和, 如上图19=3+6+10。这个证明过程这里就不具体展开了。简单来说就是这个3本身是a、b走一个路径所耗费的,6和10内除了包含新增的c、d路径耗费以外,也有a,b的3,即ab的3加了三次也就是ab的路径长度3乘上其权重都考虑了。
哈夫曼树的应用:
哈夫曼树最见的应用就是哈夫曼编码。一般的字母二进制编码的位数是一样的,我们想要让总体编码少的话就要减少每个字母的编码位数,但是如果都相同减少的话,会发生问题。例如3位二进制就无法表示15。而哈夫曼编码就是给不同频率出现的字母不同的长度的编码,在保证不会冲突的情况下,越常出现的字母给它越短的编码,从而让整体运用中所需要的编码总数最少。这个思想实际上就和我们带权路径和最短是相同的。
所以如果我们构造出一个哈夫曼树,他的带权路径和是最短的,也就是说他总体的编码总数也是最少的。这里假如一个路径连线是一个编码,那么按照上面这个图来说,ab离根节点的路径最长所以编码也最长,同时ab出现的频率即权重又是最小的,这就符合我们上一段说的,出现频率越少给他的编码越长的原则了。
下面我们再给这段分析一个图示:
到这里问题就转变为构造哈夫曼树了 !
构造哈夫曼树:
步骤:
1、选取所有结点中权值最小的两个,将其合并为树,并将根节点变为其权值之和。并且在所有节点中删除这两个结点
2、将上一步新生成的树看作一个新的结点,其权值就是之前两个结点的和。并将这个新结点放到所有结点的集合中
3、重复第一步第二步操作,直到只剩下一个新结点(即一个新树)。
见图示:
方法证明: (下面两个方法都可以证明)
1、反证法:假设找两个最小结点合并得到的带权路径和并不是最小的,即存在其他合并方法更小。假设B不与D合并而是和另外一个X合并,那么
“合并后的树到根节点的权重和减去原本的合并树到根节点的权重和”=“X到根节点的权重和减去之前D到根节点的和”。由于之前D和X到根节点路径长度相同,所以问题就变为两个权重谁大。权重越小则合并后的树到根节点的权重和越小,由于D最小所以 “合并后的树到根节点的权重和减去原本的合并树到根节点的权重和”为正数,即对任意X,合并后的树到根节点的权重和更大,故原假设不成立,不存在其他合并方法更小。
2、正面推理:读者自己证明
题目要求:
构造哈夫曼树,并求解哈夫曼树的最短路径之和。
题目分析:
构造哈夫曼树的难点在于每次都要找整个数组中最小的两个结点进行合并。所以我们要对整个数组进行排序,得到最小的结点,然后构造新的数组再排序。如果用一般的排序效率太低,所以一般采用堆排序,且是小根堆。(这里说一个点,实际上我们实现哈夫曼树也可以用其他的排序方式,只是对于树的问题用堆排序多好,树对树的逻辑结构实现多统一,嘻嘻,并且堆排序的各方面指标都很好)
而对于堆排序我们即可以采用priority_queue函数,也可以自己来实现这一个函数
代码展示:
一、利用stl函数
#include<bits/stdc++.h>
using namespace std;
const int N=1001;
int num[N];
int main(){
int n=0;
priority_queue<int,vector<int>,greater<int>> q;//小根堆
cin>>n;
for(int i=0;i<n;i++){
int t=0;
cin>>t;
q.push(t);
}
int res=0;
while(--n){
int a1=q.top();q.pop();
int a2=q.top();q.pop();
res+=a1+a2;//每个根结点的值相加就是最终路径的总长度
q.push(a1+a2);
}
cout<<res<<endl;
return 0;
}
二、完全自己实现
#include <iostream>
using namespace std;
const int N = 1001;
int num[N];
template<class T>
class priority_greater_queue {
private:
int size; // 用来存储队列的元素个数
T heap[N]; // 用来存储队列的元素,用数组来存储
public:
priority_greater_queue() {
size = 0;
}
void up(int n) { // 把一个元素上浮到合理位置,n就是修改点目前的位置
int t = n; // 修改的点目前所应该在的位置
while (n / 2 > 0 && heap[n / 2] > heap[n]) {
t = n / 2; // 改变修改点目前应该在的位置
swap(heap[t], heap[n]); // 真正把点移动到t这个位置
n = t; // 转移指针指向,进入下一轮上浮
}
}
void down(int n) { // 把一个元素下沉到应该在的位置
int t = n;
if (n * 2 <= size && heap[n * 2] < heap[t]) t = n * 2;
if (n * 2 + 1 <= size && heap[n * 2 + 1] < heap[t]) t = n * 2 + 1; // 在左右子树中找到应该在的位置
if (n != t) {
swap(heap[t], heap[n]);
down(t);
}
}
void push(int x) {
heap[++size] = x;
up(size);
}
int top() {
return heap[1];
}
void pop() {
heap[1] = heap[size];
size--;
down(1);
}
};
int main() {
int n = 0;
priority_greater_queue<int> q;
cin >> n;
for (int i = 1; i <= n; i++) {
int t = 0;
cin >> t;
q.push(t);
}
int res = 0;
while (--n) {
int a1 = q.top(); q.pop();
int a2 = q.top(); q.pop();
res += a1 + a2;
q.push(a1 + a2);
}
cout << res << endl;
return 0;
}