数算记录->二叉树->huffman树与穿线二叉树

Huffman树

1丶应用背景:在字符编码中,采取变长编码的方式能够比固定长度的编码更加省空间,为了解读的唯一性,要求任意一个字符的编码不能是另一个字符的编码的前缀。Huffman树给出了求解最优变长编码方式的方法。我们设字符 d 0 , d 1 , ⋯   , d n d_0,d_1,\cdots,d_n d0,d1,,dn在文本中出现的频率为 k 0 , k 1 , ⋯   , k n k_0,k_1,\cdots,k_n k0,k1,,kn,并设最终每个字符的编码长度分别为 s 0 , s 1 ⋯   , s n s_0,s_1\cdots,s_n s0,s1,sn,目标是在无前缀的条件下求出 ∑ i = 0 n k i s i \sum_{i=0}^{n}k_is_i i=0nkisi的最小值。

2丶算法流程:事实上,由于每个字符的编码都是01串,对于每种编码方式。我们可以建立一个满二叉树,每个叶节点对应一个字符 d i d_i di。从根节点出发,向左走表明编码多一个为0,向右走表明编码多一个1,一直走到叶节点,就得到一个01串,该01串即为字符 d i d_i di的编码,编码长度s i _i i即为该节点的路径长度。最优编码方式对应的满二叉树即为Huffman树。

首先建立Huffman树的节点:

template<class T>
class treenode {          //T表示待编码对象di,一般为char
	friend class huffmantree<T>;
	friend struct cmp<T>;
	int weight;  //对应该节点的权重si
	T* order;    //指向待编码对象di
	treenode<T>* left, *right;  
    public:
	treenode():left(NULL),right(NULL),order(NULL){}
	treenode(treenode<T>* a1,treenode<T>* a2):left(a1),right(a2){}
};

首先根据给出的字符及权重建立好所有的叶节点。并把这些节点放入优先队列中,每次取出权重最小的两个节点,然后建立一个父节点,把它的左右字节点设为这两个节点,并把父节点的权重设置为两个字节点的权重之和,再把父节点放入到优先队列中,n个待编码字符则上述过程重复n-1次,便得到了Huffman树的结构,并且最后一步得到的父节点即为树根。从构造过程可以看出这是一个满二叉树。它对应的编码方式即为最优解。

template<class T>
struct cmp {     //优先队列的比较函数,使得权重最小的节点在队首
	bool operator()(const treenode<T>* a1, const treenode<T>* a2) {
		return a1->weight > a2->weight;
	}
};

template <class T>
class huffmantree {
private:
	treenode<T>* root;  //对应构造好的Huffman树的树根
public: 
	huffmantree(const map<T,int>& data,map<T,string>& rel); //  构造Huffman tree,data给出每个待编码字符T对应的权重,我们将每个T对应的编码放入到rel中
	void travel(map<T, string>& rel, string* ptr, treenode<T>* now); //遍历构造好的Huffman tree从而得到对应的编码 
	void free_node(treenode<T>* now); //在析构函数中调用,用于释放树节点占用的空间
	void decode(const string& sour, string& des);  //给定采取该编码后得到的01字符串 sour,该函数将其解码并将结果存入des中
	virtual ~huffmantree(); 
};

完整的代码实现如下:

#include <iostream>
#include <queue>
#include <map>
#include <string>
using namespace std;
template<class T> class huffmantree;
template<class T> struct cmp;


template<class T>
class treenode {
	friend class huffmantree<T>;
	friend struct cmp<T>;
	int weight;
	T* order;
	treenode<T>* left, *right;
public:
	treenode():left(NULL),right(NULL),order(NULL){}
	treenode(treenode<T>* a1,treenode<T>* a2):left(a1),right(a2){}
};

template<class T>
struct cmp {
	bool operator()(const treenode<T>* a1, const treenode<T>* a2) {
		return a1->weight > a2->weight;
	}
};

template <class T>
class huffmantree {
private:
	treenode<T>* root;
public:
	huffmantree(const map<T,int>& data, map<T,string>& rel); // 构造Huffman tree
	void travel(map<T, string>& rel, string* ptr, treenode<T>* now);
	void free_node(treenode<T>* now);
	void decode(const string& sour, string& des);
	virtual ~huffmantree();
};
template<class T>
void huffmantree<T>::travel(map<T, string>& rel, string* ptr, treenode<T>* now) {
	if (!(now->left || now->right)) {
		rel[*(now->order)] = *ptr;
		return;
	}
	*ptr += '0';
	travel(rel, ptr, now->left);
	ptr->erase(ptr->size() - 1);
	*ptr += '1';
	travel(rel, ptr, now->right);
	ptr->erase(ptr->size() - 1);
}

template<class T>
huffmantree<T>::huffmantree(const map<T,int>& data, map<T,string>& rel) {
	priority_queue<treenode<T>*,vector<treenode<T>*>,cmp<T> > wet_order;
	for (const pair<const T, int>& k : data) { //这一段循环在构造叶节点
		treenode<T>* leaf = new treenode<T>;
		leaf->weight = k.second;
		leaf->order = const_cast<T*>(&(k.first)); //将const常量强转,不然没办法赋值了
		wet_order.push(leaf);
	}
	while (!wet_order.empty()) {   //这一段循环在构造Huffman树
		root = wet_order.top();
		wet_order.pop();
		if (wet_order.empty())break;
		treenode<T>* other = wet_order.top();
		wet_order.pop();
		treenode<T>* parent = new treenode<T>(root, other);
		parent->weight = root->weight + other->weight;
		wet_order.push(parent);
	}
	string s;
	travel(rel, &s, root);  //遍历Huffman树
}
template<class T>
void huffmantree<T>::free_node(treenode<T>* now) { //释放new出来的空间
	if (!(now->left || now->right)) {
		delete now;
		return;
	}
	free_node(now->left);
	free_node(now->right);
	delete now;
}
template<class T>
void huffmantree<T>::decode(const string& sour, string& des) {
	treenode<T>* now = root;
	for (char a : sour) {
		if (!(now->left || now->right)) {
			des += *(now->order);
			now = root;
		}
		if (a == '0')now = now->left;
		else now = now->right;
	}
	des += *(now->order);
}

template<class T>
huffmantree<T>::~huffmantree() {
	free_node(root);
}


int main() {
	/*map<char, int> a{ {'a',2},{'b',3},{'c',5},{'d',7},{'e',11},{'f',13},{'g',17},{'h',19},{'i',23},{'j',29},
	{'k',31},{'l',37},{'m',41} };*/
	map<char, int> a{ {'Z',2,},{'M',7},{'F',24},{'C',32},{'U',37},{'D',42},{'L',42},{'E',120} };
	map<char, string> rel;     //两组a为测试数据
	huffmantree<char> k(a, rel);
	for (auto& s : rel) {
		cout << s.first << ":  " << s.second << endl;
	}
	string sour("111101110"), des;
	k.decode(sour, des);
	cout << "des: " << des;
	return 0;
}

因为是自己写的,不能保证完全没问题。测试了两组数据都没问题,所以大概率是对的。

记录一下自己写的时候遇到的一些坑:

查看C++ reference上的优先队列的声明,注意比较函数类是在第三个位置(而不是第二个!)。因此在自定义优先队列的比较顺序时要记得把自定义比较函数放到第三个位置

另外,关于类和函数模板声明为友元的方式,其中之一是将整个模板类声明为该类的友元。

template<class Q> friend class a; //该条语句置于某个类的内部,则a的任何实例都是该类的友元

另一种方式是将模板的某个实例声明为该类的友元,不过这需要在在该类的定义之前声明需要作为友元的模板

在上面的Huffman树的实现中,采用的第二种方式。

3丶Huffman树的正确性(参考算法导论)

既然Huffman树是用合并的方法来构造的,我们希望说明合并前字符集的Huffman树和合并后对应的字符集Huffman树是具有一致性就可以了。

具体一点说,对于合并前的字符集A,它的最优编码对应一个Huffman树,然后我们把这个字符集的两个权重最小的节点合并了,并将其权重设为原来的两个权重相加,这是一个新的字符集。我们希望说明,把这个Huffman树的权重最小的两个叶节点收缩到父节点后得到的满二叉树对应的编码方式即为新字符集的最优编码(或者反过来)。

依次有下面三个断言成立:

①最优编码对应的二叉树一定是满二叉树(显然)

②一定存在某种最优编码方式对应的满二叉树,它的最小权重的两个叶节点是兄弟节点(注意到在最优编码对应的满二叉树中,权重越小的节点深度越大,把权重最小的两个叶节点与深度最大的两个兄弟节点互换后,新的编码方式一定不会变差,或者说代价 ∑ i = 0 n k i s i \sum_{i=0}^{n}k_is_i i=0nkisi不增)

③假设新的字符集的最优编码对应的Huffman树已找到,记代价为 M 1 M_1 M1。现在在该树上找到之前收缩的父节点,现在把它展开为原来的两个节点,则该树对应了原来字符集的一种编码方式,这是原来字符集的最优编码(如果我们记原来字符集中最小权重的两个节点的权重为 k 1 , k 2 k_1,k_2 k1,k2那这里的原来字符集的编码代价显然是 M 1 + k 1 + k 2 M_1+k_1+k2 M1+k1+k2,假设这不是最小的,根据断言2,我们可以找到原来字符集的最优编码对应的满二叉树,权重为 k 1 , k 2 k_1,k_2 k1,k2的两个节点为兄弟节点,且总代价小于 M 1 + k 1 + k 2 M_1+k_1+k2 M1+k1+k2,现在把权重最小的这两个节点收缩到父节点,则得到新字符集的一种编码方式,且代价小于 M 1 M_1 M1,这与 M 1 M_1 M1的最小性相违)

由断言3的正确性可知我们算法中递归的合并最终得到的Huffman树是最优解。

穿线二叉树(课上补充)

1丶应用背景
根据二叉树的性质可知,n个节点的二叉树具有n+1个空指针,穿线二叉树就是为了利用这些空间。空指针用来指向遍历二叉树时的前驱节点(左空指针)或者后继节点(右空指针)。遍历的三种顺序可把穿线二叉树分为三类。

2丶中序穿线二叉树的构建
在这里插入图片描述
这里给出了节点的属性:标志位用于确定左右指针是指向左右子树还是指向遍历的前驱或者后继节点。
在这里插入图片描述
递归地将穿线二叉树的空指针设置为指向前驱或者后继节点。整个递归过程其实就是一个中序遍历。这里的pre指针就是中序遍历的前一个节点。值得注意的是,中序遍历的第一个节点的左指针依然是空指针,但lTag=1。最后一个节点的右指针依然是空指针,且rTag=0。另外,指向前驱或者后继的指针总是指向的上层的节点(增加了二叉树向上访问的能力)

在这里插入图片描述
如上图,当设置好空指针之后,就可以不采取递归或者手动压栈的方法来中序遍历二叉树。

3丶一个简单应用
求中序穿线二叉树中指定节点在前序遍历中的后继结点。
在这里插入图片描述
显然,如果是指定节点存在左子节点,这等价于ITag=0(注意在之前构建中序穿线二叉树时,我们把中序遍历的第一个节点的ITag设置为了1)。这时后继节点就是它的左子节点。或者,如果指定节点存在右子节点,则返回右子节点就好了。如果指定节点是叶节点,设该节点的右指针指向的后继节点为A。可知指定节点是A的左子树中最右下方的节点。因此A的右子节点即为所求。如果A的右子节点为空。再继续考虑A的右指针指向的中序后继节点B,B的右子节点即为所求……由此可以理解上面的代码(由于我们把中序遍历中最后的节点的rTag设置为0,因此如果指定节点为前序遍历的最后的节点,上面的代码会返回空指针)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值