写这篇文章的时候我已经是可以独立写出建立一颗赫夫曼树的代码了,其实内心还是挺有感触的,当时觉得很高大的东西现在也不过如此!
赫夫曼树的定义是:给定n个权值作为n个叶子结点,构造一棵二叉树,若带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为赫夫曼树(Huffman tree)。哈夫曼树又称为最优树,其相关术语如下:
1、路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或子孙结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根节点的层数为1,则从根结点到第L层结点的路径长度为L-1。
2、结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
3、树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。
4、赫夫曼树主要是用来实现编码的,一个文件通过赫夫曼编码压缩以后更减少一半左右的存储空间。
想到一年半以前刚刚看严蔚敏数据结构的时候,看到赫夫曼树的建立过程,看了好几遍还是有些懵,就这样过了一年半。现在我重新拿起了数据结构的书本再次看了起来,这个时候我觉得它还是比较容易理解。它所需要用到的数据结构(比如Heap,template,仿函数等等)我使用过并且自己实现过,于是实现起来就比较简单,我也能清楚的分析出实现的过程,那么下面就听我分析一波吧:
首先给出一副图帮助分析吧:
以2 4 5 7 四个数建立一个赫夫曼树,上图中左边是堆中元素的变化过程过程(当然中间取元素调整的过程省略了,看到到是取两个节点建立一个二叉树然后将树根节点放回堆中去),我相信看到图的时候大家都会觉得你这样能建立一颗赫夫曼树吗?我的答案是肯定的,为了使上图中的节点最后能够自动成为一颗哈弗曼树,因为我们往堆里面放的是指向二叉树节点的指针(所以图中每个数字其实都是一个二叉树节点指针),该指针下面其实都挂着节点的,这样节点其实一直被联系着,不会丢失。最后堆中只剩下一个节点了(它的值域是18,而它的左孩子是"7",右孩子是"11"),该节点就是赫夫曼树的根节点了,最后返回该节点指针就行了。那么以指针(指针二叉树节点的指针)作为堆元素,怎样比较指针的大小呢?
为什么我们选择树节点指针作为堆中元素呢,上面的问题又怎么处理呢?这样做的好处就是赫夫曼树的建过程被大大简化了,我们选择"权值"最小的两个节点指针并以他们的"权值和"为父节点建立二叉树,然后将父节点放回堆中,最终堆中只剩一个元素的时候就建立好哈弗曼树了。而麻烦之处是无法比较指针的大小,但是我们可以实现堆的时候,使用模板和仿函数来完成,我们在实现堆的时候可以使用模板参数,然后实现一个比较类,该类重载一下(),使operator()用于比较两个树节点权值的大小,然后以该类作为参数传递给堆的模板参数(这个需要看看具体的实现,口头上说的很难明白,代码接下来就会附上)。在分析一下为什么不直接使用“权值”作为堆的元素?假如我们直接以数字作为堆中元素,该哈弗曼树的建立联系就比较麻烦了,因为我们需要不断的创建树节点,而且该树的建立过程可能存在很多中间指针(建立过程中很多节点先是离散的,后来才被连接起来的),处理这些离散的指针就显得比较麻烦了。这相对于使用模板参数+仿函数来说就显得很low了,要知道模板的使用能增加程序的使用性,可以减少代码的冗余,而且我们会使用模板了为何不用呢?我先给出代码吧,主要分两个部分:一个部分是一个堆的实现(至于大堆还是小堆可以通过模板参数来决定),另外一个部分就是赫夫曼树的建立过程,请看看代码吧:
Heap.h
#pragma once #include <iostream> #include <vector> #include <assert.h> using namespace std; template<class T> class LESS { public: bool operator() (const T& left, const T& right) { //Heap中存放的是 Heffuma<T>* node ,所以需要用其 _data 域尽心比较 //return left > right; return left->_data >= right->_data; } }; template<class T> class BIG { public: bool operator() (const T& left, const T& right) { //return left < right; return left->_data < right->_data; } }; template<class T, class Container = LESS<T> > class Heap { public: Heap(const T* arr, int size)//建堆时间复杂度 N*lg(N) { assert(arr); array.reserve(size); //事先知道空间大小后,直接开辟需要的空间,不需要在插入过程中扩增容量 for (int i = 0; i < size; i++) { array.push_back(arr[i]); } int begin = array.size() / 2 - 1; while (begin >= 0) { _AdjustDown(begin); begin--; //从下往上调用向下排序就可以完成最小堆排序 } } Heap(vector<T>& arr) { array = arr; int begin = array.size() / 2 - 1; while (begin >= 0) { _AdjustDown(begin); begin--; //从下往上调用向下排序就可以完成最小堆排序 } } Heap() {} void Insert(const T& x) //时间复杂度 lg(N) { array.push_back(x); int begin = array.size(); _AdjustUp(begin); } void Pop() //时间复杂度lg(N) { array[0] = array[array.size() - 1]; array.pop_back(); _AdjustDown(0); } void Clear() { array.clear(); } bool Empty() { return array.empty(); } const T& Top() { if (!Empty()) { return array[0]; } exit(1); } size_t size() { return array.size(); } protected: void _AdjustDown(int root) { // 从根节点向下调整 int size = array.size(); int left = root * 2 + 1; int right = left + 1; int key = left; while (left < size) {//Container()仿函数 if (right < size && Container()(array[left], array[right])) //右边小 { key = right; } else { key = left; } if (Container()(array[key], array[root])) { break; } else if (Container()(array[root], array[key])) //左边小 { swap(array[key], array[root]); } root = key; left = root * 2 + 1; right = left + 1; } } void _AdjustUp(int child) { //从叶子节点或子节点向上调整 int size = array.size(); if (size == 0 || size == 1) { return; } int root = (child - 2) / 2; int left = root * 2 + 1; int right = left + 1; int key = 0; while (root >= 0) { if (right < size && Container()(array[left], array[right])) { key = right; } else { key = left; } if (Container()(array[root], array[key])) { swap(array[root], array[key]); } else { break; } if (root == 0) { break; } root = (root - 1) / 2; //root无法小于0,所以循环条件为 root > 0 left = root * 2 + 1; right = left + 1; } } private: vector<T> array; //借助vector来当作数据成员 };
#pragma once #include"Heap.h" #include <iostream> #include <assert.h> using namespace std; template<class T> struct HuffmanNode { typedef HuffmanNode<T> Node; T _data; Node *_left; Node *_right; HuffmanNode(const T& x = T(), Node *left = NULL, Node *right = NULL) :_data(x) , _left(left) , _right(right) {} }; template<class T> class HuffmanTree { typedef HuffmanNode<T> Node; public: HuffmanTree() :_root(NULL) {} HuffmanTree(const T* arr, size_t size) { _root = _Create(arr, size); } HuffmanTree(vector<T>& v) { _root = _Create(v, v.size()); } ~HuffmanTree() { if (_root) { _Destroy(_root); } } protected: Node* _Create(const T*& arr, size_t size) { for (size_t i = 0; i < size; ++i) { Node *tmp = new Node(arr[i]); _heap.Insert(tmp); } while (_heap.size() > 1) { Node *left = _heap.Top(); _heap.Pop(); Node*right = _heap.Top(); _heap.Pop(); Node *parent = new Node(left->_data + right->_data); parent->_left = left; parent->_right = right; _heap.Insert(parent); } return _heap.Top(); } void _Destroy(Node* root) { if (root->_left == NULL && root->_right == NULL) { delete root; root = NULL; } else { _Destroy(root->_left); _Destroy(root->_right); } } private: Node *_root; Heap<Node*> _heap; }; void Test() { int arr[5] = { 2, 4, 5, 7 }; HuffmanTree<int> Ht(arr, 4); }
上面就是主要的代码,我进行一下总结吧:
1.首先我们可以看看,建立赫夫曼树的主要代码吧,这个代码很清晰的反应了建立赫夫曼树的过程。
Node* _Create(const T*& arr, size_t size) { for (size_t i = 0; i < size; ++i) { Node *tmp = new Node(arr[i]); _heap.Insert(tmp); } while (_heap.size() > 1) { Node *left = _heap.Top(); _heap.Pop(); Node*right = _heap.Top(); _heap.Pop(); Node *parent = new Node(left->_data + right->_data); parent->_left = left; parent->_right = right; _heap.Insert(parent); } return _heap.Top(); }2. 这里所用到的模板以及仿函数等等真的很有作用的,尤其是堆的使用,更加简化了建立的过程。
3. 这个故事告诉我们,我们要好好学习,把知识学透点,知识够了,这一切都是得心应手的。