哈夫曼树及其应用

哈夫曼树

  给定N个权值作为N个叶子结点 ,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

定义

  哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数}。树的路径长度是从树根到每一结点的路径长度之和,记为WPL = (W1*L1+W2*L2+W3*L3+…+Wn*Ln),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。

如下图:

Hufu

实现

  了解了哈夫曼树的定义,那么该如何去用实现并创建一颗哈夫曼树呢?

  根据哈夫曼树的定义及特点可知,构建哈夫曼树总共分为三步:

  1. 从给出的节点中找出两个较小的组成一颗树(分别作为左右孩子,一般规定的是权值较小的为左孩子),根节点的权值为两者之和。
  2. 删除(或标记)已经构造过的节点(避免重复构造),并将构造好的根节点放入其中。
  3. 重复1,2步,直至只剩下一个根节点为止

可结合下图进行推理验算。

shixian

逻辑上已经清楚如何创建,如何用代码进行实现呢?


代码实现

首先定义一个哈夫曼树的结构体,其中包含四个int域和一个char域,分别为节点自身权值、双亲及左右孩子的下标和数据域。

typedef struct{
    char data;				//数据域
    int weight;				//结点的权值
    int parent, lChild, rChild;	//双亲与孩子的下标
}htNode,*HuffmanTree;

初始化哈夫曼树

int initHuffmanTree(HuffmanTree& HT){
    HT = (htNode*)malloc(sizeof(htNode) * (2 * 10));//给HT分配2 * 10个htNOde大小的htNode类型的数组
    for (int i = 1; i <= 2 * 10 - 1; i++){
        HT[i].parent = HT[i].lChild = HT[i].rChild = -1;//双亲和孩子的值都置为-1
    }
    printf("请输入数据:\n");
    for (int i = 1; i <= 10; i++){
        //scanf("%c ",&HT[i].data);
        char a = getchar();
        if(a == '\n')	//遇到回车就结束
            break;
        else
            HT[i].data = a;	//给每个结点赋予数据
    }
    printf("请输入权值:\n");
    for (int i = 1; i <= 10; i++){
        scanf("%d",&HT[i].weight);//给每个结点赋予权值
    }
    char c = getchar();//这个来接收上面的回车
    return 1;
}

接着是创建哈夫曼树

void createHuffmanTree(HuffmanTree& HT, int n){
    if (n <= 1)	//如果结点数小于等于1,不创建
        return;
    int min1, min2;	//定义两个数,来存储每次选取最小两个结点的权值
    int rnode, lnode;
    for (int i = n + 1; i <= 2 * n -1; i++){
        int min1 = INT_MAX; int lnode = -1;
        int min2 = INT_MAX; int rnode = -1;
        for (int j = 1; j <= i - 1; j++){
            if (HT[j].weight < min1 && HT[j].parent == -1){
                min2 = min1;	
                rnode = lnode;//碰到比min1小的,那min1的值就给第二小的min2,下标也给它
                min1 = HT[j].weight; lnode = j;	//然后最小的给min1,下标同理
            }else if (HT[j].weight < min2 && HT[j].parent == -1){
                min2 = HT[j].weight;
                rnode = j;
            }
        }
        HT[lnode].parent = HT[rnode].parent = i;
        HT[i].lChild = lnode;
        HT[i].rChild = rnode;
        HT[i].weight = HT[lnode].weight + HT[rnode].weight;
    }
}

这样就可以完整的创建出一颗哈夫曼树了。当然,哈夫曼树不止只有用来存储数据这么简单,其对应的哈弗曼编码才是最实用的王牌

哈夫曼编码

  哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码。

  如最开始的第一张图可知,令哈夫曼树左子树分支编码为0,右子树分支编码为1,根据叶子节点的最短路径为该节点的编码,如:A的编码为10,I的编码为001…

  这样实现的编码是最短且最优的。

  下面将通过代码实现哈夫曼编码

void createHuffmanCode(HuffmanTree HT, HuffmanCode & HC, int n){
    HC = (HuffmanCode)malloc(sizeof(HuffmanCode) * n + 1);//申请n + 1个HuffmanCode大小HuffmanCode类型的临时空间
    //因为下标是从一开始,和哈夫曼树的结构对应
    char* array = (char*)malloc(sizeof(char) * n);//申请n个char大小char类型的临时空间,这个临时数组记录每次遍历出来的编码
    int start = 0,child = 0,parent = 0;		//start为array数组记录下标,child初始为叶子结点下标,而后就是孩子结点的下标,parent记录双亲结点的下标
    array[n - 1] = '\0';
    for (int i = 1; i <= n; i++){//只要叶子结点的编码
        start = n - 1;
        child = i;
        parent = HT[child].parent;
        while (parent != -1){//根节点没有双亲
            start--;
            if (HT[parent].lChild == child)	//左孩子就是0,右孩子就为1
                array[start] = '0';
            else
                array[start] = '1';
            child = parent; parent = HT[child].parent;	//向根结点接近
        }
        HC[i] = (char*)malloc(sizeof(char) * (n - start));	//给数组里的数组申请n - start个char大小的char*类型的临时空间
        strcpy(HC[i], &array[start]);	//array里记录的编码给HC的第i个数组
    }
    free(array);	//释放临时空间
}

  加入main方法进行测试:

int main(){
    HuffmanTree HT ;
    initHuffmanTree(HT);
    HuffmanCode HC;
    createHuffmanTree(HT,10);
    createHuffmanCode(HT,HC,10);
    for (int i = 1; i <= 10; i++){ //遍历输出编码
        printf("%c:\t",HT[i].data);
        printf("%s\n", HC[i]);
    }
    return 0;
}

  测试结果

image-20230511180201888

  然后加入读取文件的功能,就能够对文件中出现的字母进行编码

读取文件功能

  加入读取文件的功能,然后统计各个字符出现的次数为权值。

代码实现

//文本文件读文件
void read(string path) { // path为传入文件绝对路径
    //创建流对象
    ifstream ifs;
    //打开文件并且判断是否打开成功
    ifs.open(path,ios::in) ;
    if (!ifs.is_open()) {
        cout << "文件打开失败" << endl;
        return;
    }
    //读数据
    char buf[1024];
    while (ifs >> buf) {
        cout << "读取文件中...";
    }
    //5、关闭文件
    ifs.close();
}

定义一个记录统计字符次数的结构体(使用师兄师姐的代码)

typedef struct Count{ // 统计字符和对应的次数
    char ch;
    int count = 0;
}Count;
typedef struct NumCount{ // 统计次数的外部封装
    Count count [MaxSize] ;
    int Length = 0;
}NumCount;

函数本体

void WordCount(char *data,NumCount *paraCnt)
{
    int flag;// 标识是否已经记录
    int len = strlen(data);
    for(int i = 0;i < len;++i)
    {
        flag = 0;
        for(int j = 0;j < paraCnt->length;++j)
        {
            if(paraCnt->count[j].ch == data[i]) // 若已有记录,直接++
            {
                ++paraCnt->count[j].cnt;
                flag = 1;
                break;
            }

        }
        if(!flag) // 没有记录,则新增
        {
            paraCnt->count[paraCnt->length].ch = data[i];
            ++paraCnt->count[paraCnt->length].cnt;
            ++paraCnt->length;
        }
    }
    return ;
}

此时可以进行简单的文件编码功能,下面是测试结果:

image-20230511204516164

附上文件内容

image-20230511204536196

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值