[数据结构] 二叉堆(堆)详解+常用操作

堆的性质:

  • 堆一定是一棵完全二叉树
  • 堆序性:分为 大根堆——每个父节点元素都大于它的子节点元素,小根堆——每个父节点元素都小于它的子节点元素

完全二叉树的性质:

  • 只允许最后一行不为满
  • 最后一行的元素必须从左往右排序
  • 最后一行元素之间不能有间隔

<-- 一棵完全二叉树

堆如何存储:

从上到下、从左到右给堆的每个元素编号,这些编号对应数组的下标,把所有元素存储在一个数组中(通常从索引1处开始存储数据)

节点与数组下标之间的对应关系:节点下标为i,则左子节点下标为2i+1,右子节点下标为2i+2

堆的常用操作:

  • 将无序数组转换为大根堆(大元素“下坠”)

在顺序存储的完全二叉树中,非终端节点的编号i≤2n​

步骤:从[n/2]元素开始向前遍历数组中元素,将它们分别与自己的左右子节点比较,把更大的项移到根部

                                                           

代码实现:

void BuildMaxHeap(int *arr,int size)     //size是堆中的元素数目,数组的实际长度是(size+1)
{
    for(int i=size/2;i>=1;i--)     //从后往前对每个根节点处理
    {
        HeadAdjust(arr,i,size);
    }
}

void HeadAdjust(int *arr,int i,int size)     //将以索引k元素作为根节点的堆转换成大根堆
{
    arr[0]=arr[i];     //arr[i]是当前待排序的根元素,在arr[0]暂存
    //索引为i的位置看做空位
    for(int j=i*2;j<=size;j*=2)     //从i的左子节点开始处理
    {
        if(j<size && arr[j]<arr[j+1])     //左子节点与右子节点比较找到最大的子节点元素,这里要判断是否存在右子节点
            j++;
        if(arr[0]<arr[j])     
        {
            arr[i]=arr[j];     //将最大的子节点移到根处
            i=j;   //继续处理该子节点的子节点
        }
        else     //后面已经是大根堆了,跳出
            break;
    }
    arr[i]=arr[0];
}
 

时间复杂度分析:

BuildMaxHeap(建堆函数)中:

(1) 一个节点每下坠一层,最多进行两次关键字的对比(先对比左右子节点(如果有右子节点),再对比自己和最大的子节点)

      因此若树高为h,某节点在第 i 层,则调整该节点最多需要“下坠” (h−i) 层,关键字对比不超过 2(h−i) 次

(2) n个节点的完全二叉树树高h=⌊log2​n⌋+1

(3) 第i层最多有2i−1个节点,而只有1∼(h−1) 层的节点才有可能需要下坠调整(最后一层不需要下坠)

综上得出下坠操作的最大总次数:

(其中用到了换元,令j=h-i,将i=h-1与i=1代入得到新的上下界;2^⌊log ​n⌋≤n;最后一步是对差比数列进行错位相减,得出≤4n)

得出结论:建堆的过程,关键字对比次数不超过4n,建堆时间复杂度=O(n)

  • 向堆中插入新元素

步骤:

对于小根堆,将新元素放到堆尾,将它与父节点对比,若新元素<父节点,则将二者互换,重复该过程直至无法继续上升;

对于大根堆,将新元素放到堆尾,将它与父节点对比,若新元素>父节点,则将二者互换,重复该过程直至无法继续上升

代码实现:

int* MaxHeapInsert(int *arr,int size,int element)     //大根堆插入,前提是arr已经是一个大根堆
{
    int newSize=size+1,index=newSize;     //index始终指向新元素的位置
    int *newArr=malloc(sizeof(int)*(newSize+1));     //申请更大的堆
    if(newArr==NULL)
    {
        printf("内存申请失败\n");
        return NULL;
    }
    for(int i=1;i<=size;i++)     //将原堆数据复制过来
        newArr[i]=arr[i];
    newArr[index]=element;     //将新元素插入到堆尾
    for(int i=index/2;i>0;i/=2)
    {
        if(newArr[index]>newArr[i])
        {
            int tmp=newArr[index];
            newArr[index]=newArr[i];
            newArr[i]=tmp;
            index=i;
        }
        else
            return newArr;
    }
    return newArr;
}

  • 从堆中删除元素

步骤:

对于大/小根堆,用堆尾元素覆盖要删除的元素,再将该元素不断下坠,直至无法下坠为止

代码实现:

int* MaxHeapDelete(int *arr,int size,int pos)     //大根堆删除,删除第pos个元素,前提是arr已经是一个大根堆
{
    int newSize=size-1;
    int *newArr=malloc(sizeof(int)*(newSize+1));
    if(newArr==NULL)
    {
        printf("内存申请失败\n");
        return NULL;
    }
    for(int i=1;i<=newSize;i++)
        newArr[i]=arr[i];
    newArr[pos]=arr[size];
    HeadAdjust(newArr,pos,newSize);
    return newArr;
}

注意:

某根节点有两个子节点时,每下坠一层需要对比两次根节点;某根节点只有一个子节点时,每下坠一层需要对比一次关键字

“下坠”和“上升”是两种逻辑顺序,他们都可以分别用来构建大/小根堆


欢迎大家来访个人博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值