北邮数据结构与算法实践|作业一:构建哈夫曼树

本博客仅供学习交流使用,请勿用于作弊以及用于应付作业,珍惜每一个学习的机会

首先,明确要实现的功能

1. 构建哈夫曼树:根据给定的字符及其权重值,构建出对应的哈夫曼树。

2.计算编码长度:对于构建出的哈夫曼树,计算每个字符的编码长度,即根节点到该字符叶子节点的路径长度之和。路径长度定义为从根节点到某个节点的边的数量。

3.输出编码表:根据哈夫曼树,生成字符到对应编码的映射表。将每个字符与其对应的哈夫曼编码打印输出。

4.对字符串“pneumonoultramicroscopicsilicovolcanoconiosis”进行编码:利用步骤3中生成的编码表,对给定的字符串进行编码,将各个字符替换为其对应的哈夫曼编码。

然后分解任务

任务1:哈夫曼树的构建

我做了这些事:

首先,二十六个字母对应二十六个要编码的节点。每个节点包含了左孩子,右孩子,父节点和其权值。

于是我定义了节点的数据类型:

struct HNode//建立一个节点
{
    float weight;//表示权重。注意要用float类型
    int parent;//父节点的下标
    int LChild;//左孩子的下标
    int RChild;//右孩子的下标
};

然后,我定义了哈夫曼编码表,用来存储编码长度和编码:

struct CodeTableNode//建立编码表
{
    char name;//像ABC这样的名字
    int length;
    string code;//哈夫曼编码
};

之后,我回想了一下哈夫曼树编码的流程:

1.先创建好各个节点,并且把他们设置成相互独立的,组成集合T

2.在所有节点中,先选两个权值最小的节点作为左右子树

3.将他们构建成一颗新的二叉树,这个新二叉树的根节点的权值=左节点权值+右节点权值。在T中删除这两颗树,并且把新的树加入T中

4.重复2,3步骤

实现:

1.先创建好各个节点,并且把他们设置成相互独立的,组成集合T

我写了个InitializeHuffmanTree()的函数,来对哈夫曼树初始化。

talk is cheep,show the code:

void InitializeHuffmanTree()//初始化哈夫曼树
{
    //先将哈夫曼树初始化
    float weight[26]={
        0.0819,0.0147,0.0383,0.0391,0.1225,
        0.0226,0.0171,0.0457,0.0710,0.0041,
        0.0014,0.0377,0.0334,0.0706,0.0726,
        0.0289,0.0009,0.0685,0.0636,0.0941,
        0.0258,0.0109,0.0159,0.0021,0.0158,
        0.0008
    };//A到Z的权值(数据由老师提供)
    for(int i=0;i<26;i++)
    {
        HTree[i].weight=weight[i];
    }
    for(int i=0;i<2*26-1;i++)
    {
        HTree[i].LChild=HTree[i].RChild=HTree[i].parent=-1;
        //将父节点,左孩子,右孩子都设置成-1,来表示他们各个节点相互独立。
    }
}

数组存储二十六个字母,然后初始化了他们的权值(数据来源是老师给的)。

之后把左孩子,右孩子,父节点的下标都设置为了-1,以表示他们相互独立。

2.在所有节点中,先选两个权值最小的节点作为左右子树

我几经修改后,选了一种较为简单实现的方法(修改过程见“碎碎念”模块)

由于同时选出两个最小的数不是很好选,于是我选择了“曲线救国”:

首先,我开了个visited数组,来标记每个数有没有被选为最小值过。

接着,我通过FindMin(int i)函数选出最小值,并且将visited数组中这个数标为1。

最后,在新数组中再选出最小值,就是“第二小”的了。

代码实现:

bool visited[26*2-1];
int FindMin(int i)//找到最小的数
{
    //找到最小值
    float min=1000000;
    //注意这里一定要用float!(我第一次习惯性的用int,结果出bug了,找了好久才发现罪魁祸首
    int pos=-1;
    
    for(int j=0;j<i;j++)
    {
        if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
        {
            min=HTree[j].weight;
            pos=j;
        }
    }
    visited[pos]=1;
    return pos;
}

3.然后将他们构建成一颗新的二叉树,这个新二叉树的根节点的权值=左节点权值+右节点权值。在T中删除这两颗树,并且把新的树加入T中

选出后,构建子树的算法:

//建立哈夫曼树
        HTree[Min1].parent=i;
        HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
        HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
        HTree[i].LChild=Min1;
        HTree[i].RChild=Min2;
        HTree[i].parent=-1;

4.重复2,3步骤

外层套个for循环就搞定了

构建整颗哈夫曼树完整的代码

void CreatHuffmanTree()//建立哈夫曼树
{
    
    for(int i=26;i<26*2-1;i++)
    {
        //这里的查找算法我一开始用的是一遍找出最小的两个数。
        //但是这样子容易出bug
        //于是我就另外开了一个数组,记录某个数是否有被当过最小值。
        //然后我先找出最小值,接着把最小值剔除,再在新的数组中找最小值。这就是第二小的值了。
        int Min1=FindMin(i);//找到从0到i的最小值。
        int Min2=FindMin(i);//找到从0到i第二小的值。

        
        //建立哈夫曼树
        HTree[Min1].parent=i;
        HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
        HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
        HTree[i].LChild=Min1;
        HTree[i].RChild=Min2;
        HTree[i].parent=-1;
    }
}

哈夫曼树的可视化

又于时间不够,我就简单做了个表格。不然,我还可以用Graphviz来对哈夫曼树做可视化。

代码:

void DrawHuffmanTree()//画出哈夫曼树(以编码表的形式)
{
    printf("|-------------Huffman Tree--------------|\n");
    printf("|---------|--------|------|------|------|\n");
    printf("|Character| Weight |LChild|RChild|Parent|\n");
    printf("|---------|--------|------|------|------|\n");
    for(int i=0;i<26;i++)
    {
        printf("|    %c    | %.4f |  %02d  |  %02d  |  %02d  |\n",(char)(65+i),HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
        printf("|---------|--------|------|------|------|\n");
    }
    for(int i=26;i<26*2-1;i++)
    {
        printf("|   %02d    | %.4f |  %02d  |  %02d  |  %02d  |\n",i,HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
        printf("|---------|--------|------|------|------|\n");
    }
}

输出结果:

任务2.计算编码长度

要有编码长度表,就得定义表的数据类型。一个节点包含了:字母,编码长度,编码:

struct CodeTableNode//建立编码表
{
    char name;//像ABC这样的名字
    int length;
    string code;//哈夫曼编码
};

计算编码长度

这边用的是递归函数求解:

void CountStep(int i,int num)//记录步数(用于计算编码长度的)
{
    //通过递归来实现记录编码长度
    if(HTree[i].LChild==-1)
    {
        CodeTable[i].length=num;
        return;
    }
    CountStep(HTree[i].LChild,num+1);
    CountStep(HTree[i].RChild,num+1);
}

确定编码

这边用的也是递归求解:

void Encode(int i,string code)//编码函数(用于创建哈夫曼编码的)
{
    //通过递归来实现建立哈夫曼编码
    if(HTree[i].LChild==-1)
    {
        CodeTable[i].code=code;
        return;
    }
    //按照哈夫曼编码的规则,向左为0,向右为1
    Encode(HTree[i].LChild,code+"0");
    Encode(HTree[i].RChild,code+"1");
}

将数据写入数组

void CreatTable()//建立编码表(将数据写入数组)
{
    for(int i=0;i<26;i++)
    {
        //利用ASCII码来写名字
        CodeTable[i].name=(char)(65+i);
    }
    CountStep(26*2-2,0);
    Encode(26*2-2,"");
}

任务3.输出编码表

打印表格

void PrintTable()//打印编码表
{
    //用printf来格式化输出,比cout更加简单清晰
    printf("|-----------Code Table-----------|\n");
    printf("|---------|-----------|----------|\n");
    printf("|Character|Code Length|   Code   |\n");
    printf("|---------|-----------|----------|\n");
    for(int i=0;i<26;i++)
    {
        printf("|    %c    |    %02d     |%-10s|\n",CodeTable[i].name,CodeTable[i].length,CodeTable[i].code.c_str());
        printf("|---------|-----------|----------|\n");
    }
}

输出结果

(下图是我用Syntax Tree Generator做的树可视化)

网站:Syntax Tree Generator (mshang.ca)

编辑树的源码:

[1.0000[0.4233[0.1826[0.0885[0.0428[0.0202[0.0093[J,0.0041][0.0052[X,0.0021][0.0031[K,0.0014][0.0017[Z,0.0008][Q,0.0009]]]]][V,0.0019]][F,0.0226]][H,0.0457]][T,0.0941]][0.2407[0.1182[0.0547[U,0.0258][P,0.0289]][0.0635[0.0305[B,0.0147][Y,0.0158]][0.0330[W,0.0159][G,0.0171]]]][E,0.1225]]][0.5767[0.2737[0.1321[R,0.0685][S,0.0636]][0.1416[N,0.0706][I,0.0710]]][0.3030[0.1437[0.0711[M,0.0334][L,0.0377]][O,0.0726]][0.1593[0.0774[C,0.0383][D,0.0391]][A,0.0819]

任务4.测试例的编码

void EncodeTheTestString()
{
    cout<<"The Huffman Code of the string 'pneumonoultramicroscopicsilicovolcanoconiosis' is:"<<endl;
    string str("pneumonoultramicroscopicsilicovolcanoconiosis");
    int num=str.length();
    for(int i=0;i<num;i++)
    {
        cout<<CodeTable[str[i]-'a'].code;
    }
}

完整代码

#include<iostream>
#include<stdio.h>
#include<string>
#include<string.h>
#include<algorithm>
using namespace std;

struct HNode//建立一个节点
{
    float weight;//表示权重。注意要用float类型
    int parent;//父节点的下标
    int LChild;//左孩子的下标
    int RChild;//右孩子的下标
};
struct CodeTableNode//建立编码表
{
    char name;//像ABC这样的名字
    int length;
    string code;//哈夫曼编码
};

//the function declare
void InitializeVisitedArray();//初始化数组
void InitializeHuffmanTree();//初始化哈夫曼树
void CreatHuffmanTree();//建立哈夫曼树
int FindMin(int i);//找到最小的数
void DrawHuffmanTree();//画出哈夫曼树(以编码表的形式)
void CountStep(int i,int num);//记录步数(用于计算编码长度的)
void Encode(int i,string code);//编码函数(用于创建哈夫曼编码的)
void CreatTable();//建立编码表(将数据写入数组)
void PrintTable();//打印编码表
void EncodeTheTestString();//将测试的语句编码

CodeTableNode CodeTable[26];
HNode HTree[26*2-1];//Huffman Tree is a triple fork tree,so at most have 2*N-1 nodes
bool visited[26*2-1];

int main()
{
    InitializeVisitedArray();
    InitializeHuffmanTree();
    CreatHuffmanTree();
    DrawHuffmanTree();
    printf("\n\n\n");
    CreatTable();
    PrintTable();
    printf("\n\n\n");
    EncodeTheTestString();
    printf("\n\n\n");
    system("pause");
}

void InitializeVisitedArray()//初始化数组
{
    for(int i=0;i<26*2-1;i++)
    {
        visited[i]=0;
    }
}
void InitializeHuffmanTree()//初始化哈夫曼树
{
    //先将哈夫曼树初始化
    float weight[26]={
        0.0819,0.0147,0.0383,0.0391,0.1225,
        0.0226,0.0171,0.0457,0.0710,0.0041,
        0.0014,0.0377,0.0334,0.0706,0.0726,
        0.0289,0.0009,0.0685,0.0636,0.0941,
        0.0258,0.0109,0.0159,0.0021,0.0158,
        0.0008
    };//A到Z的权值(数据由老师提供)
    for(int i=0;i<26;i++)
    {
        HTree[i].weight=weight[i];
    }
    for(int i=0;i<2*26-1;i++)
    {
        HTree[i].LChild=HTree[i].RChild=HTree[i].parent=-1;
        //将父节点,左孩子,右孩子都设置成-1,来表示他们各个节点相互独立。
    }
}
int FindMin(int i)//找到最小的数
{
    //找到最小值
    float min=1000000;
    //注意这里一定要用float!(我第一次习惯性的用int,结果出bug了,找了好久才发现罪魁祸首
    int pos=-1;
    
    for(int j=0;j<i;j++)
    {
        if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
        {
            min=HTree[j].weight;
            pos=j;
        }
    }
    visited[pos]=1;
    return pos;
}
void CreatHuffmanTree()//建立哈夫曼树
{
    
    for(int i=26;i<26*2-1;i++)
    {
        //这里的查找算法我一开始用的是一遍找出最小的两个数。
        //但是这样子容易出bug
        //于是我就另外开了一个数组,记录某个数是否有被当过最小值。
        //然后我先找出最小值,接着把最小值剔除,再在新的数组中找最小值。这就是第二小的值了。
        int Min1=FindMin(i);//找到从0到i的最小值。
        int Min2=FindMin(i);//找到从0到i第二小的值。

        
        //建立哈夫曼树
        HTree[Min1].parent=i;
        HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
        HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
        HTree[i].LChild=Min1;
        HTree[i].RChild=Min2;
        HTree[i].parent=-1;
    }
}
void DrawHuffmanTree()//画出哈夫曼树(以编码表的形式)
{
    printf("|-------------Huffman Tree--------------|\n");
    printf("|---------|--------|------|------|------|\n");
    printf("|Character| Weight |LChild|RChild|Parent|\n");
    printf("|---------|--------|------|------|------|\n");
    for(int i=0;i<26;i++)
    {
        printf("|    %c    | %.4f |  %02d  |  %02d  |  %02d  |\n",(char)(65+i),HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
        printf("|---------|--------|------|------|------|\n");
    }
    for(int i=26;i<26*2-1;i++)
    {
        printf("|   %02d    | %.4f |  %02d  |  %02d  |  %02d  |\n",i,HTree[i].weight,HTree[i].LChild,HTree[i].RChild,HTree[i].parent);
        printf("|---------|--------|------|------|------|\n");
    }
}
void CountStep(int i,int num)//记录步数(用于计算编码长度的)
{
    //通过递归来实现记录编码长度
    if(HTree[i].LChild==-1)
    {
        CodeTable[i].length=num;
        return;
    }
    CountStep(HTree[i].LChild,num+1);
    CountStep(HTree[i].RChild,num+1);
}
void Encode(int i,string code)//编码函数(用于创建哈夫曼编码的)
{
    //通过递归来实现建立哈夫曼编码
    if(HTree[i].LChild==-1)
    {
        CodeTable[i].code=code;
        return;
    }
    //按照哈夫曼编码的规则,向左为0,向右为1
    Encode(HTree[i].LChild,code+"0");
    Encode(HTree[i].RChild,code+"1");
}
void CreatTable()//建立编码表(将数据写入数组)
{
    for(int i=0;i<26;i++)
    {
        //利用ASCII码来写名字
        CodeTable[i].name=(char)(65+i);
    }
    CountStep(26*2-2,0);
    Encode(26*2-2,"");
}
void PrintTable()//打印编码表
{
    //用printf来格式化输出,比cout更加简单清晰
    printf("|-----------Code Table-----------|\n");
    printf("|---------|-----------|----------|\n");
    printf("|Character|Code Length|   Code   |\n");
    printf("|---------|-----------|----------|\n");
    for(int i=0;i<26;i++)
    {
        printf("|    %c    |    %02d     |%-10s|\n",CodeTable[i].name,CodeTable[i].length,CodeTable[i].code.c_str());
        printf("|---------|-----------|----------|\n");
    }
}
void EncodeTheTestString()//将测试的语句编码
{
    cout<<"The Huffman Code of the string 'pneumonoultramicroscopicsilicovolcanoconiosis' is:"<<endl;
    string str("pneumonoultramicroscopicsilicovolcanoconiosis");
    //利用string已有的函数获得字符串的长度
    int num=str.length();
    for(int i=0;i<num;i++)
    {
        //字符串也可以像数组一样进行一个个字符的操作
        cout<<CodeTable[str[i]-'a'].code;
    }
}

程序复杂度分析

这个程序主要的空间复杂度在于三个数组:

CodeTableNode CodeTable[26];
HNode HTree[26*2-1];
bool visited[26*2-1];

空间复杂度为O(n)。

时间复杂度主要在于构建哈夫曼树的代码:

int FindMin(int i)//找到最小的数
{
    //找到最小值
    float min=1000000;
    //注意这里一定要用float!(我第一次习惯性的用int,结果出bug了,找了好久才发现罪魁祸首
    int pos=-1;
    
    for(int j=0;j<i;j++)
    {
        if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
        {
            min=HTree[j].weight;
            pos=j;
        }
    }
    visited[pos]=1;
    return pos;
}
void CreatHuffmanTree()//建立哈夫曼树
{
    
    for(int i=26;i<26*2-1;i++)
    {
        //这里的查找算法我一开始用的是一遍找出最小的两个数。
        //但是这样子容易出bug
        //于是我就另外开了一个数组,记录某个数是否有被当过最小值。
        //然后我先找出最小值,接着把最小值剔除,再在新的数组中找最小值。这就是第二小的值了。
        int Min1=FindMin(i);//找到从0到i的最小值。
        int Min2=FindMin(i);//找到从0到i第二小的值。

        
        //建立哈夫曼树
        HTree[Min1].parent=i;
        HTree[Min2].parent=i;//这一步就相当于把原先最小的两棵树删除了
        HTree[i].weight=HTree[Min1].weight+HTree[Min2].weight;
        HTree[i].LChild=Min1;
        HTree[i].RChild=Min2;
        HTree[i].parent=-1;
    }
}
n个节点中,对于每个节点要之前所有数值的最小值,找2n次,所以的时间复杂度为O(n^2)。
此外,构建编码表的时间复杂度一个节点是logn,一共有n个节点,所以是O(n*logn)。
综上,时间复杂度为O(n^2).
此外,关于查找两个最小的数时,用了两遍遍历,可以再进行优化。

碎碎念

1.因为用中文写注释要来回切换输入法,太麻烦了,所以我就用英文写注释了(主要是我懒orz)

2.代码运行的时候老是报错,最后发现原因都是少打了分号(python写多了是这样的hhh)

3.在输出编码长度的时候,一开始输出很奇怪

于是我开始debug。debug了一个多小时,发现是构建哈夫曼树的时候,在找到最小两个数字时,j标成i了。。。(吐血)

经验教训:内层循环是i还是j一定要写清楚!

后来调试,还是不行。又经过一个多小时debug,发现:

1.选取最小值的时候应该加上“父节点==-1”这个条件

2.min1和min2的初始化应该是要两个最大的数值。所以将第50个节点的权重设置为最大,并且将min1和min2都初始化为50

后来这种方法一直调不对,于是我放弃了。换另外一种方法:

每次只找最小的,不找第二小的。然后将找到的标记一下,下次不找了。之后再找一遍,就是第二小的。

这样虽然要多遍历一次,但是方法简单,不容易出错。

具体代码:

bool visited[26*2-1]={0};
int FindMin(int i)
{
    //find the min:
    int min=1000000;
    int pos=-1;
    
    for(int j=0;j<i;j++)
    {
        if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
        {
            min=HTree[j].weight;
            pos=j;
        }
    }
    visited[pos]=1;
    return pos;
}

然后还是不行,我找了好久,甚至开始怀疑人生。。。

最后,发现min要用float。。。(再次吐血)

修正后:

bool visited[26*2-1]={0};
int FindMin(int i)
{
    //find the min:
    float min=1000000;
    int pos=-1;
    
    for(int j=0;j<i;j++)
    {
        if(HTree[j].weight<min&&HTree[j].parent==-1&&visited[j]==0)
        {
            min=HTree[j].weight;
            pos=j;
        }
    }
    visited[pos]=1;
    return pos;
}
  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值