以一道题引入:
题目给出的样例和提示:
总结下题意:
利用哈夫曼编码给各个字符编码,求出最小的总字节数,即每个字符的编码数乘以字符的权重(这里是指字符出现的次数),得到每个字符的字节数,再对所有字符的字节数求和。
思路:
哈夫曼编码每次都是取权值最小的两个合并,因此每次都需要排序,这可以用到优先队列(priority_queue)。
提到哈夫曼编码,可能最先想到的最建一棵树,然后算出每个叶子的深度以得到每个字符的字节数,从而求出总字节数,但是针对这一题,这样做似乎复杂了。
可以这样想:一个叶子所在的树每被合并一次,叶子的深度就要加一,这个叶子的权重也要被加一次。因此,在优先队列中将权值最小结点的两个合并成一个新的结点,使新结点的权值等于两个被合并的结点的权值之和,并删除这两个结点。虽说这两个结点被删除了,但是实际上它们的权值以和的形式保留了下来。每次合并,都在现在总字节数的基础上加上新结点的权重,这样实际上就是对旧结点上所有里子结点的权值加了一次,最终得到的结果就是总字节数。
代码如下:
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
struct Node {
int weight;
friend bool operator <(Node a, Node b) {
return a.weight > b.weight;
//思考下这里为什么用'>'而不用'<'(后面解释)
}
};
main() {
int n, k;
cin >> n;
Node left, right;
priority_queue <Node> q;
int sum = 0;
int m = n;
while(m--) {
char ch;
Node x;
cin >> ch >> x.weight;
q.push(x);
}
while(q.size() != 1) { //q.size()=1时表明已处理完
//下面依次取出最小的两个结点
left = q.top();
q.pop();
right = q.top();
q.pop();
left.weight = left.weight + right.weight;
sum += left.weight; //每合并一次就要将结点的权值加一次
q.push(left); //要记得把新的结点加进队列
}
printf("%d\n", sum);
return 0;
}
看完了这道题,我们来了解一下优先队列的用法。
优先队列是队列的一种,不过它可以按照自定义的一种方式(数据的优先级)来对队列中的数据进行动态的排序。每次的push和pop操作,队列都会动态的调整,以达到我们预期的方式来存储。
priority_queue<Type, Container<Type>, Functional>
其中 Type 为数据类型, Container 为保存数据的容器,Functional 为元素比较方式。
Container 必须是用数组实现的容器,比如 vector, deque 但不能用 list,STL里面默认用的是 vector. 比较方式默认用 operator< , 所以如果把后面俩个参数缺省的话,优先队列就是大顶堆,队头元素最大。
例:
// q 是数据类型为 int 型的队列,队头元素[q.top()]最大
priority_queue<int> q;
// p 是数据类型为 int 型的队列,队头元素[p.top()]最小
priority_queue<int, vector<int>, greater<int> > p;
由上可知,如果要用到小顶堆,则要把模板的三个参数都带进去。
greater<> 是 STL里面定义的一个仿函数,对于基本类型可以用这个仿函数声明小顶堆。
对于自定义类型,则必须自己重载“ operator< ”(或者重载“>”)或者自己写仿函数。
struct node
{
friend bool operator< (node n1, node n2)
{
return n1.priority < n2.priority;
// 这里可根据需求改成: return n1.priority > n2.priority;
}
int priority;
int value;
};
到这里应该对优先队列的用法有了大致的了解,那么我们现在回过头来看看最前面题目代码里面的一个疑惑:
struct Node {
int weight;
friend bool operator <(Node a, Node b) {
return a.weight > b.weight;
//思考下这里为什么用'>'而不用'<'
}
};
其实我开始是想这样写的:
struct Node {
int weight;
friend bool operator <(Node a, Node b) {
return a.weight < b.weight;
}
};
// 然后这样使用:(但这种用法会报错)
priority_queue<Node, vector<Node>, greater<Node> > q;
自定义类型重载“operator <”后,声明对象时就可以只带一个模板参数。不能这样声明:priority_queue<Node, vector<Node>, greater<Node> >
,如果要这样使用,就要对“operator >”重载。针对前面的题目,代码可改成这样:
struct Node {
int weight;
friend bool operator > (Node a, Node b) {
return a.weight > b.weight;
}
};
// 然后这样使用(这种用法是可以的):
priority_queue<Node, vector<Node>, greater<Node> > q;
看过这些例子后,可得出如下结论:
对于自定义的类型,重载操作符“<”与只能和声明方法priority_queue<Node> q;
联合使用,而重载操作符“>”则只能对应声明方法priority_queue<Node, vector<Node>, greater<Node> > q;
上面讲到的都只是优先队列的用法,下面是用堆来实现队列(注:这份代码是在网上找的,来源:http://blog.csdn.net/zhang20072844/article/details/10286997,重点烛知道如何对最大最小堆调整):
#include <iostream>
#include <vector>
using namespace std;
class Heap {
public:
Heap(int iSize);
~Heap();
int Enqueue(int iVal);
int Dequeue(int &iVal);
int GetMin(int &iVal);
void printQueue();
protected:
int *m_pData;
int m_iSize;
int m_iAmount;
};
Heap::Heap(int iSize = 100) {
//注意这里是从0开始,所以如果根是i,那么左孩子是2*i+1,右孩子是2*i+2
m_pData = new int[iSize];
m_iSize = iSize;
m_iAmount = 0;
}
Heap::~Heap() {
delete []m_pData;
}
int Heap::Enqueue(int iVal) { //入堆 (入队列)
if (m_iAmount == m_iSize) return 0; //队列已满
m_pData[m_iAmount ++] = iVal;
int iIndex = m_iAmount - 1;
while (m_pData[iIndex] < m_pData[(iIndex - 1) /2]) { //上浮,直到满足最小堆
swap(m_pData[iIndex],m_pData[(iIndex - 1) /2]);
iIndex = (iIndex - 1) /2;
}
return 1;
}
int Heap::Dequeue(int &iVal) { //出堆 (出队列)
if (m_iAmount == 0) return 0;
iVal = m_pData[0];//出堆的数据
m_pData[0] = m_pData[m_iAmount - 1];//最后一个数据放到第一个根上面
-- m_iAmount;//总数减1
int rc = m_pData[0];
int s = 0;
for (int j = 2*s +1; j < m_iAmount; j *= 2)//最后一个数放到第一个位置以后,开始下沉,来维持堆的性质
{
if (j < m_iAmount - 1 && m_pData[j] > m_pData[j+1]) ++ j;
if (rc < m_pData[j]) break; //rc应该插入在s位置上
m_pData[s] = m_pData[j];
s = j;
}
m_pData[s] = rc;
return 1;
}
int Heap::GetMin(int &iVal) {
if (m_iAmount == 0) return 0;
iVal = m_pData[0];
return 1;
}
void Heap::printQueue() {
for (int i = 0; i < m_iAmount; ++ i) cout << m_pData[i] << " ";
cout << endl;
}
int main() {
Heap heap;
heap.Enqueue(4);
heap.Enqueue(1);
heap.Enqueue(3);
heap.Enqueue(2);
heap.Enqueue(6);
heap.Enqueue(5);
heap.printQueue();
int iVal;
heap.Dequeue(iVal);
heap.Dequeue(iVal);
heap.printQueue();
return 0;
}
OK,慢慢享用吧~