二叉树的应用——哈夫曼树

什么是哈夫曼树

很简答啊,是哈夫曼提出来的(雾)

先举一个生活中的例子:
我们都知道考试成绩这东西正常接近于正态分布,那我假设一下

  • 90+占10%
  • 80-90占15%
  • 70-80占25%
  • 60-71占45%
  • 60及以下占5%

然后有一个成绩读入的系统,要统计每一个分数段都有谁,我们可能需要一个switch语句,或者暴力一点if&else,如果我把90+作为比较第一项,那么很多人第一次都不会通过,然后是第二次判断。。把不及格的放在前面也是这个道理。

都知道如果比较的次数多了,时间肯定要长,那么有没有解决办法?
哈夫曼树

如果是随意构建二叉树:
二叉树的应用——哈夫曼树_第1张图片
假设有100个学生,让我们看看需要比较的次数:
10个人是1次,15个人是2次,25个人是三次,50个人是4次,所以一共315次。

上述过程就是求这棵树的加权路长,在我们确定了100个人之后,比例转化成了人数,叫做权,而每一个结点到根节点的距离,叫做路径长。
哈夫曼树的目的就是快速确定相同结点的情况下的加权路长最小的树。

先别急的看哈夫曼树怎么求,我们先看一下刚才的那个树:

  • 权都在叶子上
  • 每一个结点的度数为0或2

好了我们开始求哈夫曼树了:
第一步:我们有5个结点,分别有大小不同的权,找出最小的两个

二叉树的应用——哈夫曼树_第2张图片
将这两个构成一个新的结点,原来的两个不考虑了。
二叉树的应用——哈夫曼树_第3张图片
重复上述过程,直到只剩一个结点。
二叉树的应用——哈夫曼树_第4张图片

存储方式和对应的实现代码

1.结构体数组
结构体中有四项:左右孩子、双亲、权值
因为度数只有0和2的原因,节点数为叶子数加二度结点数,即叶子节点数*2-1

初始化:
将叶子节点(有权值的结点)输入对应权值,非叶子节点为0,左右孩子&双亲下标都是-1;

typedef  struct//数组结构体
{
     
    float weight;
    int lchild, rchild, parent;
} HTNODE;

int main()
{
     
    cout<< "How many numbers do you want?"<<endl;
    int number;
    cin>>number;
    HTNODE a[2*number-1];
    for(int i=0;i<number;i++)//初始化&输入
    {
     
        cin>>a[i].weight;
        a[i].parent=-1;
        a[i].lchild=-1;
        a[i].rchild=-1;
    }
    for(int i=number;i<2*number-1;i++)
    {
     
        a[i].weight=0;
        a[i].parent=-1;
        a[i].lchild=-1;
        a[i].rchild=-1;
     }
    CreatHT(a,number);
    return 0;
}

接下来是实现部分

void  CreatHT(HTNODE* a,int number)//数组建立哈夫曼树并输出数组
{
     
    int place=number;
    while(place!=number*2-1)
    {
     
        float mina=1;//最值
        int pa,pb;//两个位置
        for(int i=0;i<place;i++)//找第一个最值
        {
     
            if((a+i)->parent==-1&&(a+i)->weight<mina)
            {
     
                mina=(a+i)->weight;
                pa=i;
            }
        }
        mina=1;
        for(int i=0;i<place;i++)//找第二个
        {
     
            if((a+i)->parent==-1&&(a+i)->weight<mina&&i!=pa)
            {
     
                mina=(a+i)->weight;
                pb=i;
            }
        }
        (a+place)->weight=(a+pa)->weight+(a+pb)->weight;//合并两个结点
        (a+place)->lchild=pa;
        (a+place)->rchild=pb;
        (a+pa)->parent=place;
        (a+pb)->parent=place;
        place++;
    }
    for(int i=0;i<2*number-1;i++)//检查输出
    {
     
        cout << (a+i)->weight<< " | "<<(a+i)->parent<< " | "<<(a+i)->lchild
        << " | "<<(a+i)->rchild<<endl;
    }
}

咳咳,这个输出的结果可能没有对齐。。看着还是有一点别扭的

线性链表plus
这个实在是不知道怎么称呼了:

typedef struct Btree//线性链表结构体
{
     
    float number;
    int lev;
    struct Btree* lchild,*rchild,*next;
}btree;

看着结构体,说线性链表还有一点像,但这指针也太多了吧。。。
二叉树的应用——哈夫曼树_第5张图片
初始化:(lev为层数,初始为1)
在这里插入图片描述
创建一个新的结点,权值是最小的两个结点的和,左右孩子是最小的两个结点,也就是此时这两个结点已经不属于线性链表了。
二叉树的应用——哈夫曼树_第6张图片
重复上述过程,直到线性链表只有一个结点(这里没考虑头节点)
二叉树的应用——哈夫曼树_第7张图片
实现:

int main()
{
     
	btree* head = (btree*)malloc(sizeof(btree));//头节点
	head->next = NULL;
    head->number = 0;
    head->rchild = NULL;
    head->lchild = NULL;
    float number;
    
    while(cin>>number)//构建链表,初始化
    {
     
		btree* pr = (btree*)malloc(sizeof(btree));
        btree* p = head;
        pr->number = number;
        pr->lev = 1;
        pr->lchild = NULL;
        pr->rchild = NULL;
        while(p->next)//找合适的位置插入保证有序
        {
     
            if(p->next->number < number)
            	p = p->next;
            else
                break;
        }
        pr->next = p->next;
        p->next = pr;
	}
	
    while(head->next->next)//合并两个节点构成一个大的
    {
     
		btree *pa = head->next,*pb=head->next->next;
        btree* temp = (btree*)malloc(sizeof(btree));
        temp->lchild = pa;
        int lev = pa->lev;//层数注意一下,选择大的,并且之前小的要改
        if(pb->lev > pa->lev)
        {
     
            lev = pb->lev;
            pa->lev = lev;
        }
        temp->lev = ++lev;
        head->next = pb->next;
        btree* p = head;
        temp->number = pa->number+pb->number;
        temp->rchild = pb;
        
        while(p->next)//仍有序
        {
     
            if(p->next->number < temp->number)
				p = p->next;
			else
                break;
            temp->next = p->next;
            p->next = temp;
        }
        
        btree* p = head->next;//验证
        visit(p);
    }

最终的形状为一个有头节点的树,所以可以通过遍历的方式验证结果。

void visit(btree* bt)//前序递归哈夫曼树
{
     
    if(bt)
    {
     
        cout << bt->number<< "&"<< bt->lev<<endl;
        visit(bt->lchild);
        visit(bt->rchild);
    }
}

实际应用

最优编码问题
ASCII值都有所了解吧,这个就属于一种等长的编码,几位在一起表示一个字母或者符号等等。
但是在电报等情况下,我们还是希望能尽量节约,编码串尽量短一点,那么如果接着使用等长编码,就不那么合适了,所以人们就想到了解决最优编码问题的方法——哈夫曼树
因为每一个字符使用的概率是不一样的,所以就有了权值的问题,这也是使用哈夫曼树的重要条件。

另外因为编码的特点,所以任意一个编码都不能是其他编码的前缀
比如01代表a,那么011就不能使用了。
那么哈夫曼树能满足吗?
能,当我们在左孩子的连线上写上0,右孩子的连线上写上1,就实现了
二叉树的应用——哈夫曼树_第8张图片
因为字母都是叶子节点,所以不存在上述的前缀问题。

你可能感兴趣的:(数据结构)