greedy策略求解Huffman编码

         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);
	}
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值