数据结构学习之堆、哈夫曼

什么是堆?
堆是为了处理一种特殊的情况而建立的一种特殊的队列,取出的元素的顺序是按照元素的优先权的大小,而不是依照元素进入队列的先后顺序。那为何会组织这种形式呢?因为依靠顺序存储或者链表都有各种不尽人意的地方,平均损耗如下所示:

所以我们采用完全二叉树来进行存储,这样插入和删除都是O(log(n)),而且因为是完全二叉树,所以并不会照成空间上太多的浪费。


堆有两个特性:
(1)结构性:用数组表示的完全二叉树
(2)有序性:任意节点的关键字是其所有子树所有节点的最大值或者最小值,相应的就称为最大堆或者最小堆,也称大顶堆或者小顶堆。
堆关注的三个重要问题,一个是删除,一个是插入,一个是堆的建立。


一、堆的删除(以最大堆举例)
取出根节点元素,同时删除堆中的根节点,为了保证最大堆的性质,我们需要将其进行改造,如果从下方选择一个子树节点值上移,会造成许多麻烦的操作,所以我们选择将存储数组的最后一个节点移动到根节点上,然后不断的进行比较,将其放在其应该处于的位置。


二、最大堆的插入
将新插入的节点放在存储数组最后的位置,然后我们不断的将其与其父节点进行比较,如果大就上移,否则就留着原位,并且插入结束。


三、最大堆的建立
将已经存在的N哥元素按照最大堆的要求存放在一个一维数组中,我们有两种方法。
(1)通过插入操作,一个个相继插入到初始为空的堆中去,其时间代价在最大的情况下为O(NlogN)。
(2)我们可以在线性复杂度下建立最大堆。
    2.1 将N个元素按照输入顺序直接存入,先满足完全二叉树的结构特性
    2.2 调整各节点的位置,以满足最大堆的有序特性,采用自底向上的调整方式,从最后一个具有孩子节点的节点开始,往前开始调整,确保调整过的每一个节点的子树都是最大堆,它是树中各路径的高度和,时间复杂度计算如下所示:

单纯的从代码和图示来看,也是O(nlogn),但是我们说过了,这是一个自底向上的过程,所以它就变成了O(n)。
N个节点的完全二叉树最多存在log2(n+1)层(向下取整),我们设为h,在树的第j层最多有2e(j-1)个节点,每一层可能向下交换的次数为层数减一,所以根节点最多交换次数为层数减一。这样每一个的节点最多比较的次数就是h-j,其中j是自己的层数,h是总的层数,那倒数第二层的节点的比较次数就是1*2^(h-1)次(与左右孩子分别比较,所以乘上了2),由于是bottom-top建立堆,因此在调整上层元素的时候,并不需要同下层所有元素做比较,只需要同其中之一分支作比较,而作比较次数则是树的高度减去当前节点的高度。因此,第x层元素的计算量为2^(x) * (h-x),又以上通项公式可得知,构造树高为h的二叉堆的精确时间复杂度为: 
S = 2^(h-1) × 1 + 2^(h-2) × 2 + …… +1 × (h-1) ①
通过观察上述公式可知,该求和公式为等差数列和等比数列的乘积,因此用错位想减发求解,给公式左右两侧同时乘以2,可知: 
2S = 2^h × 1 + 2^(h-1) × 2+ …… +2 × (h-1) ②
用②减去①可得: S =2^h+2^(h-1)+……- h +1 ③
将h = ㏒n 带入③,得出如下结论:
S =2n-log(n+1)+1= O(n)
上述操作的代码如下:
    
   

    #include<stdio.h>
    #include<stdlib.h>
    typedef int ElementType;
    typedef struct HNode *Heap; /* 堆的类型定义 */
    #define BOOL int
    #define true 1
    #define false 0
    struct HNode {
        ElementType *Data; /* 存储元素的数组 */
        int Size;          /* 堆中当前元素个数 */
        int Capacity;      /* 堆的最大容量 */
    };
    typedef Heap MaxHeap; /* 最大堆 */
    typedef Heap MinHeap; /* 最小堆 */
    
    #define MAXDATA 1000  /* 该值应根据具体情况定义为大于堆中所有可能元素的值 */
    
    MaxHeap CreateHeap( int MaxSize )
    { /* 创建容量为MaxSize的空的最大堆 */
    
        MaxHeap H = (MaxHeap)malloc(sizeof(struct HNode));
        H->Data = (ElementType *)malloc((MaxSize+1)*sizeof(ElementType));
        H->Size = 0;
        H->Capacity = MaxSize;
        H->Data[0] = MAXDATA; /* 定义"哨兵"为大于堆中所有可能元素的值*/
    
        return H;
    }
    
    int IsFull( MaxHeap H )
    {   
        if(H->Size == H->Capacity)
            return 1;
        return 0;
    }
    int Insert( MaxHeap H, ElementType X )
    { /* 将元素X插入最大堆H,其中H->Data[0]已经定义为哨兵 */
        int i;
     
        if ( IsFull(H) ) { 
            printf("最大堆已满");
            return 0;
        }
        i = ++H->Size; /* i指向插入后堆中的最后一个元素的位置 */
        for ( ; H->Data[i/2] < X; i/=2 )
            H->Data[i] = H->Data[i/2]; /* 上滤X */
        H->Data[i] = X; /* 将X插入 */
        return 1;
    }
    #define ERROR -1 /* 错误标识应根据具体情况定义为堆中不可能出现的元素值 */
    
    int IsEmpty( MaxHeap H )
    {
        if(H->Size==0)
            return true;
        return false;
    }
    
    ElementType DeleteMax( MaxHeap H )
    { /* 从最大堆H中取出键值为最大的元素,并删除一个结点 */
        int Parent, Child;
        ElementType MaxItem, X;
    
        if ( IsEmpty(H) ) {
            printf("最大堆已为空");
            return ERROR;
        }
    
        MaxItem = H->Data[1]; /* 取出根结点存放的最大值 */
        /* 用最大堆中最后一个元素从根结点开始向上过滤下层结点 */
        X = H->Data[H->Size--]; /* 注意当前堆的规模要减小 */
        for( Parent=1; Parent*2<=H->Size; Parent=Child ) {
            Child = Parent * 2;
            if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
                Child++;  /* Child指向左右子结点的较大者 */
            if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
            else  /* 下滤X */
                H->Data[Parent] = H->Data[Child];
        }
        H->Data[Parent] = X;
    
        return MaxItem;
    } 
    
    /*----------- 建造最大堆 -----------*/
    void PercDown( MaxHeap H, int p )
    { /* 下滤:将H中以H->Data[p]为根的子堆调整为最大堆 */
        int Parent, Child;
        ElementType X;
        X = H->Data[p]; /* 取出根结点存放的值 */
        for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
            Child = Parent * 2;
            if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
                Child++;  /* Child指向左右子结点的较大者 */
            if( X >= H->Data[Child] ) break; /* 找到了合适位置 */
            else  /* 下滤X */
                H->Data[Parent] = H->Data[Child];
        }
        H->Data[Parent] = X;
    }
    
    void BuildHeap( MaxHeap H )
    { /* 调整H->Data[]中的元素,使满足最大堆的有序性  */
      /* 这里假设所有H->Size个元素已经存在H->Data[]中 */
    
        int i;
        /* 从最后一个结点的父节点开始,到根结点1 */
        for( i = H->Size/2; i>0; i-- )
            PercDown( H, i );
    }
    void Show(MaxHeap p) 
    {
        int len=p->Size;
        for(int i=1;i<=len;i++)
            printf("%d ",p->Data[i]);
        printf("\n");
    }
    void main()
    {
        MaxHeap p=CreateHeap(20);
        Insert(p,44);
        Insert(p,33);
        Insert(p,7);
        Insert(p,6);
        Insert(p,99);
        Insert(p,12);
        Insert(p,999);
        Insert(p,54);
        Insert(p,34);
        Insert(p,15);
        Show(p);
        DeleteMax( p);
        DeleteMax( p);
        Show(p);
        printf("\n");
     
        MaxHeap p1= CreateHeap(15);
        int a[]={55,79,66,83,72,30,49,91,87,43,9,38};
        int len=sizeof(a)/sizeof(int);
        p1->Size=len;
        for(int i=0;i<len;i++)
            p1->Data[i+1]=a[i];
        Show(p1);
        BuildHeap(p1);
        Show(p1);
        system("pause");
        
    
    }


运行结果:


什么是哈夫曼编码以及哈夫曼树?
我们在对数据进行查找或者编码的时候,如果不考虑数据出现的频率,单纯的将每一个数据看成等概率的操作,就会造成资源的浪费,哈夫曼树就是为了解决这个问题的。
哈夫曼树的定义:
带权路径长度(WPL):设二叉树有n个叶子结点,每个叶子结点带有权值 w,从根结点到每个叶子结点的长度为 l,则每个叶子结
点的带权路径长度之和就是:WPL=w*l(从第一个节点到第n个节点)
最优二叉树或哈夫曼树: WPL最小的二叉树
哈夫曼树的构造就是每次把权值最小的两棵二叉树进行合并。
我们依托最小堆进行实现,代码如下:

    

typedef struct TreeNode *HuffmanTree;
        struct TreeNode{
        int Weight;
        HuffmanTree Left, Right;
    }
    HuffmanTree Huffman( MinHeap H )
    { /* 假设H->Size个权值已经存在H->Elements[]->Weight里 */
         int i; HuffmanTree T;
         BuildMinHeap(H); /*将H->Elements[]按权值调整为最小堆*/
         for (i = 1; i < H->Size; i++)
         {     /*做H->Size-1次合并*/
             T = malloc( sizeof( struct TreeNode) ); /*建立新结点*/
             T->Left = DeleteMin(H);
             /*从最小堆中删除一个结点,作为新T的左子结点*/
             T->Right = DeleteMin(H);
             /*从最小堆中删除一个结点,作为新T的右子结点*/
             T->Weight = T->Left->Weight+T->Right->Weight;
             /*计算新权值*/
             Insert( H, T ); /*将新T插入最小堆*/
         }
         T = DeleteMin(H);
         return T;
     }



整体复杂度为O(NlogN).
哈夫曼树的特点:
(1)没有度为1的节点
(2)n个叶子节点的哈夫曼树共有2n-1个节点
(3)哈夫曼树的任意非叶子节点的左右子树交换后扔是哈夫曼树
(4)对同一权值的一组数据,存在不同构的两棵哈夫曼树
像哈夫曼编码这种不等长编码,最重要的就是避免二义性,必须任意字符的编码都不是另一个字符编码的前缀,如此才可以无二义的解码,根据这一特点,我们利用二叉树进行编码,遵循下列两个条件:
(1)左右分支分别为0,1.
(2)字符只在叶子节点上。










 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值