从零开始学数据结构系列之第三章《哈夫曼编码构造及代码》

文章目录

  • 哈夫曼编码基本概念
  • 哈夫曼编码方法
    • 两个特点
    • 总代码
    • 往期回顾

      哈夫曼编码基本概念

      哈夫曼编码,又称为 Huffman Coding

      ​   是一种可变长编码( VLC, variable length coding))方式,比起定长编码的 ASCII 编码来说,哈夫曼编码能节省很多的空间,因为每一个字符出现的频率不是一致的;

      ​   是一种用于无损数据压缩的熵编码算法,通常用于压缩重复率比较高的字符数据。

      ​   如果我们通过转换成ASCII码对应的二进制数据将字符串 BCAADDDCCACACAC 通过二进制编码进行传输,那么一个字符传输的二进制位数为 8bits,那么总共需要 120 个二进制位;而如果使用哈夫曼编码,该串字符可压缩至 28位。

      哈夫曼编码方法

        哈夫曼编码首先会使用字符的频率创建一棵树,然后通过这个树的结构为每个字符生成一个特定的编码,出现频率高的字符使用较短的编码,出现频率低的则使用较长的编码,这样就会使编码之后的字符串平均长度降低,从而达到数据无损压缩的目的。

      2. 按照字符出现的频率进行排序,组成一个队列 Q 出现频率最低的在前面,出现频率高的在后面。

      3. 把这些字符作为叶子节点开始构建一颗哈夫曼树

      哈夫曼树又称为最优二叉树,是一种带权路径长度最短的二叉树。

      (1)首先创建一个空节点 z,将最小频率的字符分配给 z 的左侧,并将频率排在第二位的分配给 z 的右侧,然后将 z 赋值为两个字符频率的和;然后从队列 Q 中删除 B 和 D,并将它们的和添加到队列中,上图中 * 表示的位置。

      (2)紧接着,重新创建一个空的节点 z,并将 4 作为左侧的节点,频率为 5 的 A 作为右侧的节点,4 与 5 的和作为父节点,并把这个和按序加入到队列中,再根据频率从小到大构建树结构(小的在左)。

      3)

        继续按照之前的思路构建树,直到所有的字符都出现在树的节点中,哈弗曼树构建完成。

        节点的带权路径长度为从根节点到该节点的路径长度与节点权值的乘积。

        该二叉树的带权路径长度 WPL = 6 * 1 + 5 * 2 + 3 * 3 + 1 * 3 = 28。

      4. 对字符进行编码:

        哈夫曼树构建完成,下面我们要对各个字母进行编码,编码原则是:对于每个非叶子节点,将 0 分配给连接线的左侧,1 分配给连接线的右侧,最终得到字符的编码就是从根节点开始,到该节点的路径上的 0 1 序列组合。

      在没有经过哈夫曼编码之前,字符串“BCAADDDCCACACAC”的二进制为:

      10000100100001101000001010000010100010001000100010001000100001101000011010000010100001101000001010000110100000101000011
      

      也就是占了 120 比特;

      编码之后为:

      1000111110110110100110110110
      

      占了 28 比特。

      再看我们上面的内容

      或者:

      两个特点

      带权路径长度WPL最短且唯一;

      编码互不为前缀(一个编码不是另一个编码的开头)。

      为什么通过哈夫曼编码后得到的二进制码不会有前缀的问题呢?

        这是因为在哈夫曼树中,每个字母对应的节点都是叶子节点,而他们对应的二进制码是由根节点到各自节点的路径所决定的,正因为是叶子节点,每个节点的路径不可能和其他节点有前缀的关系。

      为什么通过哈夫曼编码获得的二进制码短呢?

        因为哈夫曼树是带权路径长度最短的树,权值较大的节点离根节点较近。而带权路径长度是指:树中所有的叶子节点的权值乘上其到根节点的路径长度,这与最终的哈夫曼编码总长度成正比关系的。

      总代码

      #include #include  typedef struct TreeNode 
      { int weight;
          int parent;
          int lchild;
          int rchild;
      }TreeNode;
      typedef struct HFTree 
      { TreeNode* data;
          int length;
      }HFTree;
      /*
      * 开辟空间,同时赋予初值,这边左右子树未塞入值时,初始值为-1
      * 同时将权值注入
      */
      HFTree* initTree(int* weight,  int length) 
      {int i=0;
      	HFTree *T = (HFTree*)malloc(sizeof(HFTree));
      	T->data = (TreeNode*)malloc(sizeof(TreeNode) * (length * 2 - 1));
      	T->length = length;
      	for(i=0;iT->data[i].parent = 0;
      		T->data[i].lchild = -1;
      		T->data[i].rchild = -1;
      		T->data[i].weight = weight[i];
      	}
      	return T;
      }
      /*
      * 这里的min,second_min均为设置的初值,同时循环寻找最小值并更新该值
      * 同时传递两个int 类型的数值,需要开辟空间过去
      */
      int* selectMin(HFTree* T) 
      {int i=0;
      	int min = 100000;
      	int second_min = 100000;
      	int flag_first=0;
      	int flag_second =0;
      	int *address = (int*)malloc(sizeof(int) * 2);
      	
      	for(i=0;ilength;i++)
      	{if(T->data[i].weightdata[i].parent == 0)
      		{min=T->data[i].weight;
      			flag_first=i;
      		}
      	}
      	for(i=0;ilength;i++)
      	{if(T->data[i].weightdata[i].parent == 0 && i !=flag_first)
      		{second_min=T->data[i].weight;
      			flag_second=i;
      		}
      	}
      	address[0] = flag_first;
      	address[1] = flag_second;
      	return address;
      }
      /*
      * length = T->length * 2 - 1;这里的初始值是最终开辟的整个空间大小,因为后续会新增父节点
      * 左右子树赋予初值,这里顺序可以可以颠倒,并不影响最终结果
      * 同时更新并初始化新值,T -> data[i].parent = 0;如果没有这个,selectMin函数并不会将新增值列入判断范围
      */
      void createHFTree(HFTree* T) 
      {int i=0;
      	int length = T->length * 2 - 1;
      	int *receive=NULL;
      	for(i=T->length;ireceive=selectMin(T);
      		T->data[i].lchild = receive[0];
      		T->data[i].rchild = receive[1];
      		T->data[i].weight = T->data[receive[0]].weight + T->data[receive[1]].weight;
      		T -> data[i].parent = 0;
      		T -> data[receive[0]].parent = i;
      		T -> data[receive[1]].parent = i;
      		T->length++;
      	}
      }
      void preOrder(HFTree* T, int index) 
      {if (index != -1) 
      	{ printf("%d ", T -> data[index].weight);
              preOrder(T, T -> data[index].lchild);
              preOrder(T, T -> data[index].rchild);
          }
      }
      int main() 
      { int weight[] = {1,2,3,4};
          HFTree* T = initTree(weight, sizeof(weight)/sizeof(int));
          createHFTree(T);
          preOrder(T, T -> length - 1);
          printf("\n");
          return 0;
      }
      

      往期回顾

      1.【第一章】《线性表与顺序表》

      2.【第一章】《单链表》

      3.【第一章】《单链表的介绍》

      4.【第一章】《单链表的基本操作》

      5.【第一章】《单链表循环》

      6.【第一章】《双链表》

      7.【第一章】《双链表循环》

      8.【第二章】《栈》

      9.【第二章】《队》

      10.【第二章】《字符串暴力匹配》

      11.【第二章】《字符串kmp匹配》

      12.【第三章】《树的基础概念》

      13.【第三章】《二叉树的存储结构》

      14.【第三章】《二叉树链式结构及实现1》

      15.【第三章】《二叉树链式结构及实现2》

      16.【第三章】《二叉树链式结构及实现3》

      17.【第三章】《二叉树链式结构及实现4》

      18.【第三章】《二叉树链式结构及实现5》

      19.【第三章】《中序线索二叉树理论部分》

      20.【第三章】《中序线索二叉树代码初始化及创树》

      21.【第三章】《中序线索二叉树线索化及总代码》

      22【第三章】《先序线索二叉树理论及线索化》

      23【第三章】《先序线索二叉树查找及总代码》

      24【第三章】《后续线索二叉树线索化理论》

      25【第三章】《后续线索二叉树总代码部分》

      26【第三章】《二叉排序树基础了解》

      27【第三章】《二叉排序树代码部分》

      28【第三章】《二叉排序树代码部分》

      29【第三章】《平衡二叉树基础概念》

      30【第三章】《平衡二叉树的平衡因子》

      31【第三章】《平衡二叉树的旋转基础详解》

      32【第三章】《平衡二叉树的旋转类型图文详解》

      33【第三章】《平衡二叉树的旋转类型总结及总代码》

      34【第三章】《哈夫曼树简单了解》

      35【第三章】《哈夫曼树的构造方法》