树:哈夫曼树和哈夫曼编码的详细介绍以及代码实现

闲扯前言

哈夫曼编码的代码实现对于初学数据结构的同学可能会有些困难,没有必要灰心,其实没啥,学习就犹如攀登一座又一座的山峰,每当我们攻克一个难点后,回首来看,也不过如此嘛。我们要做的就是不断的去攀越学习上的山峰 不断的超越过去的自己。尤其是我们程序员,不进则退,中国最不缺的就是人,肯定不缺替代你的程序员,没有越老越吃香的程序员,中国的老程序员都去哪了?要么转管理,要么被淘汰转行了,当然还有一小部分成为技术专家 继续活跃在IT圈。时代在进步,技术在不断换代的更新,但是计算机一些固有的东西仍没有改变,编程语言只是我们手中的武器,我们唯有把内功修炼好,然后拿着利器 可以无往不胜!数据结构和算法就是我们程序员的内功修炼的一方面。不扯淡了。。。下面是博主如何攻克哈夫曼树和哈夫曼编码的一些思路。参考教材为 大话数据结构

哈夫曼树和哈夫曼编码的关系

哈夫曼树当然是一种树,不过这种树有些特殊之处。哈夫曼编码呢,是根据哈夫曼树规则生成的编码提供一个字符,根据哈夫曼编码规则,你会得到一个哈夫曼编码,不过你提供的字符必须在哈夫曼编码表中有对应的编码才行

哈夫曼树

哈夫曼大叔说,从树中一个结点到另一个结点之间的分支构成2个结点之间的路径,路劲上的分支数目称作路径长度。树的路径长度就是从树根到每一结点的路径长度之和。如果考虑到带权的结点,结点的带权的路径长度就为从该结点到树根之间的路径长度与结点上权的乘积。树的带权路径长度为树中所有叶子结点的带权路径长度之和。

假设有n个权值{W1,W2.....,Wn},构造一棵n个叶子结点的二叉树,每个叶子结点带权Wk,每个叶子的路径长度为1k,我们通常记作,其中带权路径长度WPL最小的二叉树称为哈夫曼树(又称最优二叉树)。

定义我们只要有理解就ok重点是下面如何构建哈夫曼树

哈夫曼树构造步骤

  1. 根据给定的n个权值{W1,W2,...,Wn}构成n棵二叉的集合F={T1,T2,...Tn},其中每棵二叉树Ti只有一个带权为Wi的根结点,其左右子树均为空。
  2. 在F中选取2棵根结点最小的树 作为左右子树 构造一棵新的二叉树,且新的二叉树的根结点左右子树根结点权值之和。
  3. 在F中删除这2棵子树,同时将新得到的二叉树加入F中。
  4. 重复2和3步骤,直到F只含一棵树为止,这棵树便是哈夫曼树。
我们看这4个步骤,能否联想到队列的出队入队呢?往队列方向想,思路是对的。将权值最小的2棵树节点出队,然后创建新结点入队,这样一直循环,最后剩下那一个树结点时不就是哈夫曼树么。下面是构建哈夫曼树的HuffQueue.h的相关接口和数据结构。代码汇总在最后。

HuffQueue.h

#pragma once 
#ifndef __HUFFQUEUE_H__
#define __HUFFQUEUE_H__
#include "HuffmanTree.h"

//哈夫曼结点的队列 结点的数据结构
typedef struct HuffQueueNode
{
	HuffmanNode* treeNode;//哈夫曼树结点
	int weightNum;//权值
	struct HuffQueueNode* next;//指向下一个结点
}HuffQueueNode;

//哈夫曼结点组成的队列数据结构,由于插入数据(根据权值)和删除数据(删除队头的数据)的特性,需要结点个数 和 头指针
typedef struct HuffQueue
{
	int size;
	HuffQueueNode* first;
}HuffQueue;

//初始化队列
void InitHuffQueue(HuffQueue** queue);

//获取结点(出队操作),从队头出队
HuffQueueNode* GetHuffQueueNode(HuffQueue* queue);

//插入结点(入队操作),特殊插入 根据权值大小进行有序的插入结点 非结尾处插入
void AddHuffQueueNode(HuffQueue* queue, HuffmanNode* treeNode, int weightNum);


#endif // !__HUFFQUEUE_H__

BuildHuffmanTree函数

//根据用户提供字符集,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString)
{
	//统计每个字符出现的权值
	int charWeight[MAX_SZ] = { 0 };
	for (int i = 0; i < inputString[i]!='\0'; i++)
	{
		charWeight[(unsigned char)inputString[i]]++;
	}
	HuffQueue* queue = NULL;
	InitHuffQueue(&queue);

	for (int i = 0; i <MAX_SZ; i++)
	{
		//对应的字符有权值,创建树结点,添加到树节点队列中
		if (charWeight[i])
		{
			HuffmanNode* treeNode = (HuffmanNode*)malloc(sizeof(HuffmanNode));
			treeNode->data = i;
			treeNode->lchild = treeNode->rchild = NULL;
			AddHuffQueueNode(queue, treeNode, charWeight[i]);
		}
	}
	//根据哈夫曼树创建原理构建哈夫曼树
	//核心就是将权值最小的2个结点,取出作为新创建树结点的孩子结点,新创建树结点的权值为它们之和,然后放回树结点队列
	//一直这样循环进行操作,直到队列中最后剩一个结点,它就是树的根结点。
	while (queue->size != 1)
	{
		HuffQueueNode* node1 = GetHuffQueueNode(queue);
		HuffQueueNode* node2 = GetHuffQueueNode(queue);
		HuffmanNode* treeNode = (HuffmanNode*)malloc(sizeof(HuffmanNode));
		treeNode->data = '\0';
		treeNode->lchild = node1->treeNode;
		treeNode->rchild = node2->treeNode;
		int weightNum = node1->weightNum + node2->weightNum;
		AddHuffQueueNode(queue, treeNode, weightNum);
	}
	return queue->first->treeNode;
}

哈夫曼编码

定义还是先过一遍!一般地,设需要编码的字符集为{d1,d2,...dn},各个字符在电文中出现的次数或者频率集合{W1,W2,...Wn},以d1,d2,...dn作为叶子结点,以W1,W2,...Wn作为相应叶子结点的权值来构造一棵哈夫曼树规定哈夫曼树的左分支代表0,右分支代表1,则从根结点到叶子结点所经过的路劲分支组成的0和1的序列便为该结点对应字符的编码,这就是哈夫曼编码

后面一句话是重点!走到叶子结点经过的0101...就是对应字符的哈夫曼编码

有了哈夫曼树,写哈夫曼编码表 就很好,编写了!看下哈夫曼编码表的数据结构采用链式队列的方式,这样是否更方便!数据结构是我们人为去定义,是为我们操作数据提供方便的。下面是 HuffmanTree.h提供的相关接口和相应的数据结构。代码汇总在最后。

HuffmanTree.h

#pragma once 
#ifndef __HUFFMANTREE_H__
#define __HUFFMANTREE_H__

//创建哈夫曼树提供的字符的最大长度
#define MAX_SZ 256
//哈夫曼编码字符串最大长度
#define MAX_ENCODE 1024
//哈夫曼树结点数据结构
typedef struct HuffmanNode
{
	char data;
	struct HuffmanNode *lchild, *rchild;
}HuffmanNode,*HuffmanTree;

//哈夫曼编码表结点数据结构
typedef struct HuffmanTableNode
{
	char data;		//字符
	char *encode;	//字符对应的哈夫曼编码
	struct HuffmanTableNode *next;
}HuffmanTableNode;
//哈夫曼编码表数据结构
typedef struct HuffmanTable
{
	HuffmanTableNode* front;//指向队头元素
	HuffmanTableNode* tail;	//指向队尾元素
}HuffmanTable;

//根据用户提供原始数据,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString);

//根据哈夫曼树 生成对应的哈夫曼编码表
HuffmanTable* BuildHuffmanTable(HuffmanTree tree);

//对用户提供的源字符进行哈夫曼编码
char* encode(HuffmanTable* table, char* src);

//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root, char* encode);

//遍历哈夫曼编码表
void TraverseHuffmanTable(HuffmanTable* table);
#endif // !__HUFFMANTREE_H__

BuildHuffmanTable函数

/*
递归遍历哈夫曼树
depth 树的深度
tree 哈夫曼树
hTable 哈夫曼编码表
encode 字符对应的哈夫曼编码
*/
void traverseHuffTree(HuffmanTable* hTable,HuffmanTree tree,char* encode,int depth)
{
	
	if (NULL == tree->lchild && NULL == tree->rchild)
	{
		HuffmanTableNode* tableNode = (HuffmanTableNode*)malloc(sizeof(HuffmanTableNode));
		tableNode->data = tree->data;
		tableNode->next = NULL;
		encode[depth] = '\0';
		tableNode->encode = (char*)malloc(depth+1);
		strcpy(tableNode->encode, encode);
		if (hTable->front == NULL)
		{
			hTable->front = hTable->tail = tableNode;
		}
		else
		{
			hTable->tail->next = tableNode;
			hTable->tail = tableNode;
		}
	}
	if (NULL != tree->lchild)
	{
		encode[depth] = '0';//左分支代表0
		traverseHuffTree(hTable, tree->lchild, encode, depth+1);
	}

	if (NULL != tree->rchild)
	{
		encode[depth] = '1';//右分支代表1
		traverseHuffTree(hTable, tree->rchild, encode, depth+1);
	}
	return;
}

//根据哈夫曼树生成哈夫曼编码表
HuffmanTable * BuildHuffmanTable(HuffmanTree tree)
{
	HuffmanTable* hTable = (HuffmanTable*)malloc(sizeof(HuffmanTable));
	hTable->front = hTable->tail = NULL;
	char encode[MAX_SZ] = { 0 };
	traverseHuffTree(hTable, tree, encode, 0);
	return hTable;
}

哈夫曼树和哈夫曼编码的使用

既然有了哈夫曼编码,那么就可以进行对用户提供的字符集进行哈夫曼编码了当然也可用利用用户提供的哈夫曼编码进行解码咯。这就是小case了,encode 编码根据用户提供的字符到哈夫曼编码表中找到对应的哈夫曼编码然后返回给用户就可以了decode解码用户提供01100等哈夫曼编码,利用哈夫曼树进行解码就ok,0就往左走,1就往右走,直到叶子结点 对应的字符不就出来了么,然后又从哈夫曼树根结点开始走,直到将哈夫曼编码走完。下面是encode和decode代码。

encode函数

char* encode(HuffmanTable* table,char* src)
{
	char* encode = (char*)calloc(sizeof(char)*MAX_ENCODE,1);
	for (int i = 0; i < src[i]!='\0'; i++)
	{
		char ch = src[i];
		HuffmanTableNode* iterator = table->front;
		while (iterator != NULL)
		{
			if (iterator->data == ch)
			{
				strcat(encode, iterator->encode);
				break;
			}
			iterator = iterator->next;
		}
		if (iterator == NULL)
		{
			printf("哈夫曼编码表中没有字符%c对应的哈夫曼编码!\n",ch);
			return NULL;
		}
	}
	return encode;
}

decode函数

//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root,char* encode)
{
	char* decode = (char*)calloc(MAX_SZ, 1);
	HuffmanTree tree = root;
	for (int i = 0; i < encode[i]!='\0'; i++)
	{
		char ch = encode[i];
		if ('0' == ch)//0 往左走
		{
			tree = tree->lchild;
		}
		else//1 往右走
		{
			tree = tree->rchild;
		}
		//走到头,也就是找到相应的 源字符了
		if (tree->lchild == NULL && tree->rchild == NULL)
		{
			strncat(decode, &tree->data, 1);
			//找到字符后,树节点回到根结点,继续解码
			tree = root;
		}
	}
	return decode;
}

代码汇总一览

HuffQueue.h

#pragma once 
#ifndef __HUFFQUEUE_H__
#define __HUFFQUEUE_H__
#include "HuffmanTree.h"

//哈夫曼结点的队列 结点的数据结构
typedef struct HuffQueueNode
{
	HuffmanNode* treeNode;//哈夫曼树结点
	int weightNum;//权值
	struct HuffQueueNode* next;//指向下一个结点
}HuffQueueNode;

//哈夫曼结点组成的队列数据结构,由于插入数据(根据权值)和删除数据(删除队头的数据)的特性,需要结点个数 和 头指针
typedef struct HuffQueue
{
	int size;
	HuffQueueNode* first;
}HuffQueue;

//初始化队列
void InitHuffQueue(HuffQueue** queue);

//获取结点(出队操作),从队头出队
HuffQueueNode* GetHuffQueueNode(HuffQueue* queue);

//插入结点(入队操作),特殊插入 根据权值大小进行有序的插入结点 非结尾处插入
void AddHuffQueueNode(HuffQueue* queue, HuffmanNode* treeNode, int weightNum);


#endif // !__HUFFQUEUE_H__

HuffQueue.c

#include "HuffQueue.h"
#include <stdlib.h>

//初始化队列
void InitHuffQueue(HuffQueue** queue)
{
	*queue = (HuffQueue*)malloc(sizeof(HuffQueue));
	(*queue)->size = 0;
	(*queue)->first = NULL;
}

//获取结点(出队操作),只需要树节点的数据,故只返回树节点数据
HuffQueueNode* GetHuffQueueNode(HuffQueue* queue)
{
	if (NULL == queue || 0 == queue->size || NULL == queue->first)
	{
		return NULL;
	}
	HuffQueueNode* queueNode = queue->first;
	queue->first = queue->first->next;
	queue->size--;
	return queueNode;

}

//插入结点(入队操作),特殊插入 根据权值大小进行有序的插入结点 非队尾处插入
void AddHuffQueueNode(HuffQueue* queue, HuffmanNode* treeNode, int weightNum)
{
	HuffQueueNode *queueNode = (HuffQueueNode*)malloc(sizeof(HuffQueueNode));
	queueNode->weightNum = weightNum;
	queueNode->treeNode = treeNode;
	queueNode->next = NULL;
	//队列为空
	if (0 == queue->size)
	{
		queue->first = queueNode;
		queue->size++;
		return;
	}
	else
	{
		//比第一个结点权值都小
		if (weightNum < queue->first->weightNum)
		{
			queueNode->next = queue->first;
			queue->first = queueNode;
			queue->size++;
			return;
		}

		HuffQueueNode* iterator = queue->first;
		HuffQueueNode* pre = NULL;
		while (iterator != NULL && weightNum > iterator->weightNum)
		{
			pre = iterator;
			iterator = iterator->next;
		}
		//在队列中找到自己位置,将其插入其中
		if (NULL != iterator)
		{
			queueNode->next = iterator->next;
			iterator->next = queueNode;
		}
		//没找到,说明自己权值最大,插入到末尾
		else
		{
			pre->next = queueNode;
		}
		queue->size++;
		return;
	}
	return;
}

HuffmanTree.h

#pragma once 
#ifndef __HUFFMANTREE_H__
#define __HUFFMANTREE_H__

//创建哈夫曼树提供的字符的最大长度
#define MAX_SZ 256
//哈夫曼编码字符串最大长度
#define MAX_ENCODE 1024
//哈夫曼树结点数据结构
typedef struct HuffmanNode
{
	char data;
	struct HuffmanNode *lchild, *rchild;
}HuffmanNode,*HuffmanTree;

//哈夫曼编码表结点数据结构
typedef struct HuffmanTableNode
{
	char data;		//字符
	char *encode;	//字符对应的哈夫曼编码
	struct HuffmanTableNode *next;
}HuffmanTableNode;
//哈夫曼编码表数据结构
typedef struct HuffmanTable
{
	HuffmanTableNode* front;//指向队头元素
	HuffmanTableNode* tail;	//指向队尾元素
}HuffmanTable;

//根据用户提供原始数据,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString);

//根据哈夫曼树 生成对应的哈夫曼编码表
HuffmanTable* BuildHuffmanTable(HuffmanTree tree);

//对用户提供的源字符进行哈夫曼编码
char* encode(HuffmanTable* table, char* src);

//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root, char* encode);

//遍历哈夫曼编码表
void TraverseHuffmanTable(HuffmanTable* table);
#endif // !__HUFFMANTREE_H__

HuffmanTree.c

#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "HuffmanTree.h"
#include "HuffQueue.h"

//根据用户提供字符集,生成对应的哈夫曼树
HuffmanTree BuildHuffmanTree(char* inputString)
{
	//统计每个字符出现的权值
	int charWeight[MAX_SZ] = { 0 };
	for (int i = 0; i < inputString[i]!='\0'; i++)
	{
		charWeight[(unsigned char)inputString[i]]++;
	}
	HuffQueue* queue = NULL;
	InitHuffQueue(&queue);

	for (int i = 0; i <MAX_SZ; i++)
	{
		//对应的字符有权值,创建树结点,添加到树节点队列中
		if (charWeight[i])
		{
			HuffmanNode* treeNode = (HuffmanNode*)malloc(sizeof(HuffmanNode));
			treeNode->data = i;
			treeNode->lchild = treeNode->rchild = NULL;
			AddHuffQueueNode(queue, treeNode, charWeight[i]);
		}
	}
	//根据哈夫曼树创建原理构建哈夫曼树
	//核心就是将权值最小的2个结点,取出作为新创建树结点的孩子结点,新创建树结点的权值为它们之和,然后放回树结点队列
	//一直这样循环进行操作,直到队列中最后剩一个结点,它就是树的根结点。
	while (queue->size != 1)
	{
		HuffQueueNode* node1 = GetHuffQueueNode(queue);
		HuffQueueNode* node2 = GetHuffQueueNode(queue);
		HuffmanNode* treeNode = (HuffmanNode*)malloc(sizeof(HuffmanNode));
		treeNode->data = '\0';
		treeNode->lchild = node1->treeNode;
		treeNode->rchild = node2->treeNode;
		int weightNum = node1->weightNum + node2->weightNum;
		AddHuffQueueNode(queue, treeNode, weightNum);
	}
	return queue->first->treeNode;
}
/*
递归遍历哈夫曼树
depth 树的深度
tree 哈夫曼树
hTable 哈夫曼编码表
encode 字符对应的哈夫曼编码
*/
void traverseHuffTree(HuffmanTable* hTable,HuffmanTree tree,char* encode,int depth)
{
	
	if (NULL == tree->lchild && NULL == tree->rchild)
	{
		HuffmanTableNode* tableNode = (HuffmanTableNode*)malloc(sizeof(HuffmanTableNode));
		tableNode->data = tree->data;
		tableNode->next = NULL;
		encode[depth] = '\0';
		tableNode->encode = (char*)malloc(depth+1);
		strcpy(tableNode->encode, encode);
		if (hTable->front == NULL)
		{
			hTable->front = hTable->tail = tableNode;
		}
		else
		{
			hTable->tail->next = tableNode;
			hTable->tail = tableNode;
		}
	}
	if (NULL != tree->lchild)
	{
		encode[depth] = '0';//左分支代表0
		traverseHuffTree(hTable, tree->lchild, encode, depth+1);
	}

	if (NULL != tree->rchild)
	{
		encode[depth] = '1';//右分支代表1
		traverseHuffTree(hTable, tree->rchild, encode, depth+1);
	}
	return;
}

//根据哈夫曼树生成哈夫曼编码表
HuffmanTable * BuildHuffmanTable(HuffmanTree tree)
{
	HuffmanTable* hTable = (HuffmanTable*)malloc(sizeof(HuffmanTable));
	hTable->front = hTable->tail = NULL;
	char encode[MAX_SZ] = { 0 };
	traverseHuffTree(hTable, tree, encode, 0);
	return hTable;
}

//对用户提供的源字符进行哈夫曼编码
char* encode(HuffmanTable* table,char* src)
{
	char* encode = (char*)calloc(sizeof(char)*MAX_ENCODE,1);
	for (int i = 0; i < src[i]!='\0'; i++)
	{
		char ch = src[i];
		HuffmanTableNode* iterator = table->front;
		while (iterator != NULL)
		{
			if (iterator->data == ch)
			{
				strcat(encode, iterator->encode);
				break;
			}
			iterator = iterator->next;
		}
		if (iterator == NULL)
		{
			printf("哈夫曼编码表中没有字符%c对应的哈夫曼编码!\n",ch);
			return NULL;
		}
	}
	return encode;
}

//根据用户提供的哈夫曼编码进行解码
char* decode(HuffmanTree root,char* encode)
{
	char* decode = (char*)calloc(MAX_SZ, 1);
	HuffmanTree tree = root;
	for (int i = 0; i < encode[i]!='\0'; i++)
	{
		char ch = encode[i];
		if ('0' == ch)//0 往左走
		{
			tree = tree->lchild;
		}
		else//1 往右走
		{
			tree = tree->rchild;
		}
		//走到头,也就是找到相应的 源字符了
		if (tree->lchild == NULL && tree->rchild == NULL)
		{
			strncat(decode, &tree->data, 1);
			//找到字符后,树节点回到根结点,继续解码
			tree = root;
		}
	}
	return decode;
}

//打印哈夫曼编码表
void TraverseHuffmanTable(HuffmanTable* table)
{
	HuffmanTableNode* node = table->front;
	while (node != NULL)
	{
		printf("源字符:%c ->哈夫曼编码:%s\n", node->data, node->encode);
		node = node->next;
	}
}

int main(int argc, char *argv[])
{
	HuffmanTree tree = BuildHuffmanTree("aabbccc");
	HuffmanTable* table = BuildHuffmanTable(tree);
	TraverseHuffmanTable(table);
	char *src = "abc";
	char* dest = "10110101101";
	char* encode_str = encode(table, src);
	char* decode_str = decode(tree, dest);
	printf("原始字符串:%s ->哈夫曼编码:%s\n", src, encode_str);
	printf("哈夫曼编码:%s ->解码为:%s\n", dest, decode_str);
	return 0;
}

图解和运行测试

图解

我们那字符串aabbccc来生成哈夫曼树和哈夫曼编码表。对应的哈夫曼树如下图:

很明显,c字符->哈夫曼编码为0,a字符->哈夫曼编码10,b字符->哈夫曼编码11。

测试代码

int main(int argc, char *argv[])
{
	HuffmanTree tree = BuildHuffmanTree("aabbccc");
	HuffmanTable* table = BuildHuffmanTable(tree);
	TraverseHuffmanTable(table);
	char *src = "abc";
	char* dest = "10110101101";
	char* encode_str = encode(table, src);
	char* decode_str = decode(tree, dest);
	printf("原始字符串:%s ->哈夫曼编码:%s\n", src, encode_str);
	printf("哈夫曼编码:%s ->解码为:%s\n", dest, decode_str);
	return 0;
}

运行结果




评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值