Huffman编码的原理为:每次选择数据集datas中当前第1小和第2小的2个数来构建左右子树,而这2个子树的父节点则是这2个子树的和,然后将该和放入父节点的datas中,再在新的数据集中选择两个子树继续构建,直到数据集中只剩下一个数据为止。总共构建次数为n-1次。
而Huffman的greedy策略点为每次选择数据值倒数2个进行构建,因为Huffman的原则是频率越大的数越最先编码,即频率越大的数越靠近树根,这样的话,需要的编码字符越少,那么我们应该如何选择构建Huffman树呢?根据数据越大越靠近树根,数据越小则越靠近底层,本文可以将数据越小的先构建树,然后再大一点的数A与先构建的树根一起再构建树,这样再大一点的这个数A一定比前面两个最小的数就越靠近树根,那么在再大一点的数A到root的路径(length)就更短一点,每个叶子节点到root的路径长度就是coding长度。
所以greedy策略点最终还是回归到问题本身,即回归到了问题的求解目的(如何给字符编码使得总的code最短)。
那么如何实现该算法呢?
(1)其实按照直接每次提取最后的2个数进行构建,则每次提取前进行一下排序就行,这个可通过合并排序解决,但是前面已排过一次,基本有序,每次对基本有序的数据在排序,也比较耗时,而每次得到最小的数,存在一个更好的数据结构:最小堆,即优先队列。只要我们连续取2次即可,因为最小堆在每次取走首元素后,随即进行堆的调整,使第二小的数顶替刚被取走的最小元素的位置,本文基于最小堆构建Huffman树的时间复杂度为O(nlogn),而如何采用其他的排序快速排序也需要O(n*nlogn)。主体步骤如下:
Step1. 将全部的节点以结构体或类的形式进行封装,都包含左右子树节点的指针,方便后面进行greedy合并,然后每次取出最小的两个节点进行合并;
Step2. 若全部直接一次排序,但每次都有新合并的节点需要进行全部排序,并删除原合并的节点,所以采用堆排序可以避免重复排序;
Step3. 每次构建结构体或类的树形结构。原则是:新构建的节点为堆中pop的两个较小小节点,而父节点则是这两个节点的和,然后将该节点入堆,这样的话:堆中的元素类型为节点类型,(可利用模板结构),先构建堆。
Step4. 最后通过树的节点的遍历,并深层次的递归传递路径0||1的code。
Step5. 由于最开始采用的vector来构建堆,在每次弹出最小元素时,vector容器会自动delete地址所指的节点,所以最后只能得到一个根节点,其他的节点都随着pop_back()被删除了。因此,重新设计一个vector,即本文定义的listVector,仅仅将minHeap中存储节点指针的数组减1,而不删除该数组位置的指针所指的节点,因为节点保存在内存的其他地方,所以仍然存在,而listvector中的数组删除的只是数组中存放的节点的地址,而不会连带的delete指针所指的内容。最后返回的是整个已经构建好的Huffman树。其主体框架算法如下:
vector<Point> GreedyStrategy::huffmanCoding(map<string, double > num)
{
ListVector minHeap=*new ListVector(); //通过普通方法来建立vector堆 ,全局变量
Point *p = new Point(); //定义一个对象,初始化时,树形结构都为空
p->setId("start"); //编码名称
p->setData(0); //编码使用频率
insertHeap(minHeap, p); //为什么第0个元素不能利用起来了
map<string, double>::iterator iter = num.begin();
while (iter!=num.end()) //将map中的元素全部入堆
{
Point *point = new Point(); //定义一个对象指针,初始化时,树形结构都为空
point->setId(iter->first); //编码名称
point->setData(iter->second); //编码使用频率
insertHeap(minHeap,point);
iter++;
}
//**********************************************************
Point *root; //数组中的元素个数
while (minHeap.GetLength()>=3) //为什么要去掉第一个元素,第一个元素为什么要设置为0 至少包含两个节点才可以构建
{
Point *leftPoint = new Point(); //覆盖的只是地址变量,而实际的地址指向的块内容则没有被覆盖
Point *rightPoint = new Point();
Point *leftMinPoint = deleteGetMin(minHeap); //左第一小,右为第二小
Point *rightMinPoint = deleteGetMin(minHeap); //提取出minHeap中最小的两个节点
leftPoint = leftMinPoint;
rightPoint = rightMinPoint;
Point *fatherPoint = new Point(); //无地址说明对象没有初始化,切记?不然就是乱地址,这样他不为空,但是无值
fatherPoint->setData(leftMinPoint->getData() + rightMinPoint->getData());
fatherPoint->setId(leftMinPoint->getId() + rightMinPoint->getId());
fatherPoint->setLeft(leftPoint); //只要是地址,就会被删除
fatherPoint->setRight(rightPoint);
leftPoint->setParent(fatherPoint);
rightPoint->setParent(fatherPoint);
root = fatherPoint; //保持root一直指向父节点
insertHeap(minHeap, root);
}
vector<Point>coding; //traversalCoding
root = deleteGetMin(minHeap); //重先赋值给根节点,并进行遍历
string code = "";
traversalCoding(coding, root, code);
return coding;
}
(2)优先队列的算法实现采用的是数组数据结构进行存储,由于vector在堆的删除顶部元素后,delete释放该元素空间,所以在构建以指针为节点连接方式的二叉树中,子树节点会被删除,而无法最后构建出huffman树,因此本文痛定思痛,决定自己写一个vector,即listVector,只弹出元素而不删除被弹出的元素。而算法中最小堆的实现结构和算法是自己以前编写minHeap模板类结构,这次只做了将vector换成listVector结构的相应修改。具体的实现原理为:每次插入和删除堆顶时,都不断的调整堆,具体的调整过程应该是左旋或者右旋的二次平衡树原理,具体实现原理后文待写,实现算法如下:
/************************************************************************/
//优先小堆的实现
/************************************************************************/
void GreedyStrategy::insertHeap(ListVector &minHeap, Point *point)
{
if (minHeap.GetLength()>1) //堆中存在元素,至少1个
{
minHeap.push_tail(point);
//minHeap.push_back(point); //先置于堆的最后
moveUp(minHeap, point); //然后在对该元素指向上移操作至合适位置
}
else
{
minHeap.push_tail(point); //否则直接放在最后
}
}
void GreedyStrategy::moveUp(ListVector &minHeap, Point *point)
{
int n = minHeap.GetLength()-1; //减1的目的是除去首元素
int i = n / 2;
while (minHeap.sq.date[i]->getData() > point->getData() && i) //注意:error:表达式必须包含类类型和表达式必须包含指针类型,原因:.和->没有用对
{
minHeap.sq.date[n] = minHeap.sq.date[i]; //切记:直接get得到的是形参而不是实参
n = i;
i = i / 2;
}
minHeap.sq.date[n] = point;
/*while (minHeap[i].getData()>point.getData()&&i)
{
minHeap[n] = minHeap[i];
n = i;
i = i / 2;
}
minHeap[n] = point;*/
}
Point * GreedyStrategy::deleteGetMin(ListVector &minHeap)
{
//堆排序中较小的元素是不是已按序排序,或者仅仅符合堆的结构:子树<父节点
if (minHeap.GetLength()>1)
{
Point *minPoint = new Point();
minPoint = minHeap.sq.date[1]; //这里为什么是1,不是0???????
downMove(minHeap); //每次下移都是从下标1开始,所以直接在函数中实现
return minPoint;
}
else
{
return minHeap.sq.date[0];
}
}
void GreedyStrategy::downMove(ListVector &minHeap)
{
int i = 1;
int n = minHeap.GetLength();
Point *point = minHeap.sq.date[n - 1];
minHeap.pop_tail();
//minHeap.pop_back(); //这里将元素直接删除了,后面的子树也被删除了
//以下是实现下移操作
while (2*i<n) //这里是下标0不用的原因
{
if (i*2+1<n)
{
if (minHeap.sq.date[i*2]->getData() < minHeap.sq.date[i*2+1]->getData())
{
if (point->getData()>minHeap.sq.date[i * 2]->getData())
{
minHeap.sq.date[i] = move(minHeap.sq.date[i * 2]);
i = i * 2;
}
else
{
minHeap.sq.date[i] = point;
break;
}
}//the second if
else
{
if (point->getData()>minHeap.sq.date[i * 2 + 1]->getData())
{
minHeap.sq.date[i] = minHeap.sq.date[i * 2 + 1];
i = i * 2 + 1;
}
else
{
minHeap.sq.date[i] = point;
break;
}
}
}
else //最外层 i=2*n; 说明只有一个左子树, 这里这样设计是因为需要进行左右子树的判断
{
if (point->getData()>minHeap.sq.date[i * 2]->getData())
{
minHeap.sq.date[i] = minHeap.sq.date[i * 2];
i = i * 2;
}
else
{
minHeap.sq.date[i] = point;
break;
}
}
}//while
if (minHeap.GetLength() > 1)
{
minHeap.sq.date[i] = point;
}
}
(3)应用情况,如果能够用STL则可用,若实际情况不能使用vector,或者vector在设计算法中,相反使算法变得简单的复杂化,则可以根据实际应用情况自己设计一个vector,并修改其中相关的功能,同时可以编译成一个工具类.lib文件或.dll文件,因为vector本身也是.dll文件,思维一定要开阔,数据善置,算法自成。listVector数据结构如下:
/************************************************************************/
/* 功能:数组实现的线性表
与vecto不同的是:弹出元素时不销毁*/
以下是listVector.h文件
/************************************************************************/
#include"Point.h"
#include <vector>
#pragma once;
#define MAX 1000
typedef struct
{
Point *date[MAX]; //这里元素的类型需要改变成point类型的地址
int len;
}Sqlist;
//先具体指明变量类型,后改用模板
class ListVector
{
public:
ListVector();
~ListVector();
void InitSqlist(Sqlist &sq);
Sqlist getSq(); //类的成员函数可以直接访问类的私有变量;类的对象不可以直接访问类的私有变量,只能通过成员方法进行访问。
void setSq(Sqlist sq);
int GetLength();
Point* getElem(int i); //获取第i个位置的元素
void pop_tail(); //仅仅将某个对象的地址删除,弹出但不销毁
void push_tail(Point* point); //压入某个对象的地址
void push_back(Point* point);
void pop_back();
Sqlist sq;
private:
vector<Point*>data;
};
//////////////////////////////以下是listVector.cpp文件///////////////////////////////////////////
#include "ListVector.h"
ListVector::ListVector()
{
}
ListVector::~ListVector()
{
}
void ListVector::InitSqlist(Sqlist &sq)
{
sq.len = 0;
}
Sqlist ListVector::getSq()
{
return sq;
}
void ListVector::setSq(Sqlist sq)
{
this->sq = sq;
}
int ListVector::GetLength()
{
return sq.len;
}
Point* ListVector::getElem(int i)
{
if (i<0||i>sq.len)
{
return 0;
}
else
{
return sq.date[i]; //元素从第0个开始,对象的地址
}
}
//以下实现2个重要的vector功能:放入和弹出最后的2个元素
//push_back
//pop_back
void ListVector::pop_tail() //核心功能
{
sq.len--; //直接将尾部元素拿掉,但并delete point对象
}
void ListVector::push_tail(Point* point)
{
sq.date[sq.len] = point; //在尾部增加一个元素
sq.len++; //元素压入数组中,则总的长度增加
}
void ListVector::push_back(Point*point)
{
data.push_back(point);
}
void ListVector::pop_back()
{
//vector 不弹出来,长度无法改变,故还是要重写
}
(4)如何遍历上面构建的huffman树,实现对叶子节点的字符进行coding呢?很显然可以采用递归思想,但是如何记录每个叶子节点的code成了一大难题。因为在不断的递归过程中,虽然递归函数相同,但传递的数据是在不断的变化,可以根据数据的变化(增加或减小)来结束或跳转递归。那到底如何记录呢?本文想到一个方法就是在外层通过vector<Point>&coding别名传递(注意这里的Point只是表示字符和code),这样传递的就是同一个数组变量,而且所有的递归都是在这个别名变量上操作的,且别名变量可声明在调用递归函数的最外面。这样不管递归多少次,别名变量都统管所有的子递归。
那么如何记录code?当递归到叶子节点时,停止递归,并将该叶子节点的name和其对应的code构建成一个对象压入coding中。那么如何来表示其code呢?
是向上增还是向下增,最开始想到的是在每次遍历左子树之前新建一个字符变量code加”0”;在每次递归遍历右子树时则code+”1”。好像可以,但是其实逻辑上一想是不行的,因为根的右子树叶为1,但是好像又可以,通过向左时加0,然后向右时将刚才的0减去,在加1,这样好像是可以的,但是字符只能相加而不能相减,不过可以通过一个vector类型变量codes来存放这个code,应该可以实现。论证如下:
存在2个方法可以实现所有的子递归中codes共用:方法1是将codes定义成一个const类型,即静态类型;方法2是采用函数传值的方法,将codes进行递归参数传递。而不管是方法1还是方法2,都要从最外层递归将参数传递进去。所以本文最后直接进行参数传递,而如果进行参数传递的话,就不用code来记录参数,因为当向当前左子树进行遍历时,可直接将当前参数+0“”传递到下一层左子树递归中。那这样的递归到底正不正确呢?
证明:当树只有3个节点时,将参数变量code定义在最外层,并设为空。然后当向左子树传递时,则在函数中直接将code+”0”进行传递;当向右传递时,则直接在递归右函数中将code+”1”进行传递。当递归到叶子节点时,即左右子树为空时。在判断条件else中记录当前节点的值和当前递归层的code参数到对象point中,并push_back到vector类型的 coding中,为了保证在递归的过程中,是保存编码的coding参数的不变性,则可将参数以别名的形式进行传递,这样就是对同一片地址的变量进行操作,所以当记每个叶子节点时,都是记录在同一个coding中,且coding又没有因为每次的递归而重复定义变量。因为如果在递归函数内部定义变量,则该变量就在每次递归中发生存储地改变,导致变量没有连续的传递性,即所有的子递归不能共用一个变量(虽然名字相同,但每次定义的变量都是不同的,都会开辟新的存储地址,只有当存储地址相同,变量无论名字是否相同,则2变量都是相同的)。所以,当需要所有的子递归共用一个变量时,可以在调用递归函数的外面定义一个变量,并进行别名传递到递归函数中;若在递归的过程中参数需要不断的改变,可以在递归中定义一个临时变量来保存当前的传递参数,特别注意在向下一层传递参数时,左子树和右子树的参数一定要保证是当前递归的参数,而不能是左传递后修改过的参数,得证。
具体的遍历算法如下:注:递归的算法一定是最简洁的。还是那句话:数据善置,算法自成。
void GreedyStrategy::traversalCoding(vector<Point>&coding, Point * root,string code)
{
//注:任何递归一定是最简洁的代码,递归部分都不要超过3行代码 //定义一个叶子节点
if (root->getLeft() != nullptr&&root->getRight() != nullptr) //当非叶子节点时,继续路径编码
{
traversalCoding(coding, root->getLeft(), code + "0");
traversalCoding(coding, root->getRight(), code + "1");
}
else
{
Point leafNode = *new Point(); //如果是叶子节点,则定义一个存储coding对象
leafNode.setId(root->getId());
leafNode.setCode(code);
leafNode.setData(root->getData());
coding.push_back(leafNode);
}
}
来源:CSDN
作者:tanshemeng9736
链接:https://blog.csdn.net/tanshemeng9736/article/details/103963434