• 浅谈堆


    数据结构——堆

    1. 概述

    堆(也叫优先队列),是一棵完全二叉树,它的特点是父节点的值大于(小于)两个子节点的值(分别称为大顶堆和小顶堆)。它常用于管理算法执行过程中的信息,应用场景包括堆排序,优先队列等。

    2. 堆的基本操作

    堆是一棵完全二叉树,高度为O(lg n),其基本操作至多与树的高度成正比。在介绍堆的基本操作之前,先介绍几个基本术语:

    A:用于表示堆的数组,下标从1开始,一直到n

    PARENT(t):节点t的父节点,即floor(t/2)

    RIGHT(t):节点t的左孩子节点,即:2*t

    LEFT(t):节点t的右孩子节点,即:2*t+1

    HEAP_SIZE(A):堆A当前的元素数目

    下面给出其主要的四个操作(以大顶堆为例)

    2.1 Heapify(A,n,t)

    该操作主要用于维持堆的基本性质。假定以RIGHT(t)和LEFT(t)为根的子树都已经是堆,然后调整以t为根的子树,使之成为堆。

    void heapify(int t)//t根节点,注意t是根节点在a数组中的位置,也就是一个编号,不是对应的数值 
    {
        int left=t*2,right=t*2+1;//左右孩子编号,注意也是编号 
        int maxn=t;//maxn为t,t的左孩子,t的右孩子中最大的,注意也是编号 
        if(left<=n) {maxn=a[maxn]>a[left] ? maxn:left;}// ? :为三目运算符,如果=和?之间的条件为真,等号左边的maxn为 :左边的maxn;否则为:右边的left 
        if(right<=n){maxn=a[maxn]>a[right] ? maxn:right;}
        if(maxn!=t)//不是根节点,即要调整 
        {
            swap(a[maxn],a[t]);//根节点和最大的孩子节点交换 
            heapify(maxn);//因为把根节点调下来之后,根节点有可能使所在子树不再是一个堆,所以要递归调整 
        }
    }

    注意heapify的调整是从上往下的,因为递归的参数是maxn,若能更新,maxn一定是t的孩子节点

    问题1:heapify什么时候结束

    当t为叶子节点时,left与right都>n,所以maxn不会更新,不满足maxn!=t的条件,结束

    问题2:为什么heapify操作要满足左右子树都是堆得时候才进行?

    因为heapify操作的结束条件,一旦有一个结点不能更新,此操作结束,而结点更新的条件是比较左右孩子。如果子树不是堆,可能存在当前结点和左右孩子比较,不能更新,但孩子的孩子可能可以更新。子树是堆得话,孩子的孩子一定不能更新

    heapify是另外3个操作的基础,所以一定要搞懂

    2.2     BuildHeap(A,n)

    该操作主要是将数组A转化成一个大顶堆。思想是,先找到堆的最后一个非叶子节点(即为第n/2个节点),然后从该节点开始,从后往前逐个调整每个子树,使之称为堆,最终整个数组便是一个堆。

    问题1:为什么是非叶子节点?

    因为本操作要以heapify操作为基础,具体看代码。而heapify操作是通过比较与孩子节点的关系实现的,所以要满足节点要有孩子节点。

    问题2:为什么是最后一个/为什么要从后往前?

    最后一个和从后往前的是相互对应的,从最后一个开始,就是从后往前开始。

    有人说了,我从第一个节点开始,从前往后不行吗?不行。

    因为本操作要联系heapify操作,heapify操作要求节点t的左右子树已经是一个堆。若从前往后调整,以t为根节点的左右子树,在这之前没有调整,不能保证是堆

    void build()
    {
        for(int i=n/2;i;i--)
        heapify(i);
    }

    2.3 GetMaximum(A,n)

    该操作主要是获取堆中最大的元素,同时保持堆的基本性质。堆的最大元素即为第一个元素,将其保存下来,同时将最后一个元素放到A[1]位置,之后从上往下调整A,使之成为一个堆。

    int get()
    {
        int top=a[1];//取出栈顶
        a[1]=a[n];//把最后一个元素放到栈顶
        n--;//取出之后元素少一个
        heapify(1);
        return top;
    }

    2.4  Insert(A, n, t)

    向堆中添加一个元素t,同时保持堆的性质。算法思想是,将t放到A的最后,然后从该元素开始,自下向上调整,直至A成为一个大顶堆。

    void insert(int k)//在数组a中加入元素k,注意k不是编号,是一个确切的值 
    {
        n++;
        a[n]=k;//把k放在最后一个地方 
        int p=n;//p为当前调整位置,每次都从最后一个位置开始 
        while(p>1&&a[p/2]<k)//p>1,即p不是根节点,同时p的父节点小于k,注意<后面不能是a[p]
            a[p]=a[p/2];//把p的父节点挪下来 
            p/=2;//p指向父节点的位置 
        }
        a[p]=k;//最终k的位置为p,赋值 
    }

    问题1:为什么<后面不能是a[p]?


    由此可见,每次更新只是把父节点移到子节点处,父节点暂时保留原值,等到下一次以这个父节点为子节点时,再赋值。所以如果比较的是a[p],就比较的是原来父节点的值,而实际上此处父节点应为要添加的数k

    问题2:insert操作为什么要自下而上?

    问题3:insert操作结束条件为什么有p>1?

    当p=1时为根节点,本操作为自下而上,根节点无法向上操作

    3.  堆的应用

    3.1  堆排序

    堆的最常见应用是堆排序,时间复杂度为O(N lg N)。如果是从小到大排序,用大顶堆;从大到小排序,用小顶堆。

    3.2  在O(n lg k)时间内,将k个排序表合并成一个排序表,n为所有有序表中元素个数。

    【解析】取前100 万个整数,构造成了一棵数组方式存储的具有小顶堆,然后接着依次取下一个整数,如果它大于最小元素亦即堆顶元素,则将其赋予堆顶元素,然后用Heapify调整整个堆,如此下去,则最后留在堆中的100万个整数即为所求 100万个数字。该方法可大大节约内存。

    3.3 一个文件中包含了1亿个随机整数,如何快速的找到最大(小)的100万个数字?(时间复杂度:O(n lg k))

    4. 总结

    堆是一种非常基础但很实用的数据结构,很多复杂算法或者数据结构的基础就是堆,因而,了解和掌握堆这种数据结构显得尤为重要。

    5. 参考资料

    经典算法教程《算法导论》

    Codevs堆练习

    黄金:2830、2879、2995、3110

    钻石:1052、1063、1245、1246、2057、2573、3377

    大师:1021、1765、2069、2913、3032

  • 相关阅读:
    LeetCode:1_Two_Sum | 两个元素相加等于目标元素 | Medium
    算法导论第十章 栈队列和链表
    算法导论2-9章补充几道题
    算法导论第九章中位数和顺序统计量(选择问题)
    算法导论第八章线性时间排序
    算法导论第七章快速排序
    算法导论第六章优先队列(二)
    算法导论第六章堆排序(一)
    mysql中查看视图的元数据?
    mysql中,什么是视图,视图的作用是什么?
  • 原文地址:https://www.cnblogs.com/TheRoadToTheGold/p/6238795.html
Copyright © 2020-2023  润新知