堆(定义+基本操作+堆排序)

本文讲解了堆的定义、不同类型及其在优先队列中的应用,详细介绍了堆的构造方法、删除与添加元素操作,以及堆排序的原理与代码实现。特别关注了处理重复元素和不严格升序的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

堆是一棵完全二叉树,树中每个结点的值都不小于(或者不大于)其左右孩子结点的值。其中,如果父亲结点的值大于或等于孩子结点的值,那么这样的堆称为大顶堆;如果父亲结点的值小于或等于孩子结点的值,那么这样的堆称为小顶堆。

堆一般用于优先队列的实现,而优先队列默认情况下使用的是大顶堆。此处的讨论以大顶堆为例。

向下调整:总是将当前结点V与它的左右孩子比较(如果有的话),假如孩子中存在权值比结点V的权值大的,就将其中权值最大的那个孩子结点与结点V交换;交换完毕后继续让结点V和孩子比较,直到结点V的孩子的权值都比结点V的权值小或者是结点V存在孩子结点。

向上调整:总是把欲调整结点与父结点比较,如果权值比父亲结点大,那么就交换其与父亲结点,这样反复比较,直到到达堆顶或者父亲结点的权值较大为止。

对于一个给定的初始序列,如何把它建成一个堆?

  1. 将它们按照树的层序从上往下、从左往右依次摆放,形成一个初始堆
  2. 假设序列中元素的个数为n,由于完全二叉树的叶子结点个数为ceil(n/2),因此数组下标在[1,floor(n/2)]范围内的结点都是非叶子结点。从floor(n/2)号位开始倒着枚举结点(这个顺序保证每个结点都是以其为根结点的子树中的权值最大结点),对每个遍历到的结点i进行[i, n]范围的向下调整

删除堆顶元素

只需要最后一个元素覆盖堆顶元素,然后对根结点进行向下调整。

往堆里添加一个元素

可以把想要添加的元素放在数组最后(也就是完全二叉树的最后一个结点后面),然后进行向上调整操作。

堆排序

是指使用堆结构对一个序列进行排序。这里讨论递增排序的情况。

堆排序的直观思路:取出堆顶元素,然后将堆顶的最后一个元素替换至堆顶,再进行一次针对堆顶元素的向下调整——如此重复,直到堆中只有一个元素为止

具体实现时,为了节省空间,可以倒着遍历数组,假设当前访问到i号位,那么将堆顶元素与i号位的元素交换,接着在[1, i-1]范围内对堆顶元素进行一侧向下调整即可。

基础代码

#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN=100;
/*
 对于完全二叉树来说,比较简洁的实现方法是:
 结点按层序存储于数组中,第一个结点存储在数组的1号位,
 数组i号位表示的结点的左孩子就是2*i号位,右孩子就是(2*i+1)号位
*/
int heap[MAXN],n=10;// heap为堆,n为元素个数
// 对heap数组在[low, high]范围内进行向下调整
// 其中low为欲调整结点的数组下标,high一般为堆的最后一个元素的数组下标
void DownAdjust(int low,int high) {  // 时间复杂度为O(logn)
    int root = low,child = root*2;
    while(child<=high) {  // 存在孩子结点
        // 如果右孩子存在,且右孩子的值大于左孩子
        if(child+1 <= high && heap[child+1] > heap[child])
            child = child + 1;
        if(heap[child] > heap[root]) {  // 如果孩子中最大的权值比欲调整结点i大
            swap(heap[child], heap[root]);
            root = child;
            child = root*2;
        } else  // 孩子的权值均比root结点小,调整结束
            break;
    }
}
// 对heap数组在[low, high]范围内进行向上调整
// 其中low一般设置为1,high为欲调整结点的数组下标
void UpAdjust(int low,int high) {  // 时间复杂度为O(logn)
    int root = high, parent = root/2;
    while(parent>=low) {
        if(heap[parent]<heap[root]) {
            swap(heap[parent], heap[root]);
            root = parent;
            parent = root/2;
        } else
            break;
    }
}
void CreateHeap() { // 建堆,时间复杂度为O(n)
    for(int i=n/2; i>=1; i--)
        DownAdjust(i,n);
}
void DeleteTop() { //  删除堆顶元素,时间复杂度为O(logn)
    heap[1] = heap[n--];  // 用最后一个元素覆盖堆顶元素,并让元素个数减1
    DownAdjust(1, n);   // 向下调整堆顶元素
}
void Insert(int x) {  // 添加元素x,时间复杂度为O(logn)
    heap[++n] = x;  // 让元素个数加1,然后将数组末位赋值为x
    UpAdjust(1,n);  // 向上调整新加入的结点n
}
void HeapSort() {
    CreateHeap();
    for(int i=n; i>1; i--) { // 倒着枚举,直到堆中只有一个元素
        swap(heap[i],heap[1]);  // 交换heap[i]与堆顶
        DownAdjust(1,i-1);  // 调整堆顶
    }
}
int main() {
    return 0;
}

例题PAT A1098

        这道题始终有一个测试点过不去,是因为插入排序如 序列s为:1 3 5 7 7 2 0,这个序列前边5个都是有序的。代码第59处不要忘记等号。

        从中我吸取的教训是:题目给出的是N integers,并没有指出这N个数是不存在重复元素的,那么做题的时候不能想当然觉得序列中无重复元素。虽然题目有提到ascending,但是也要考虑到这个ascending可能不是一个严格的ascending。这道题一开始没通过全部测试用例,根源是我审题不清。

#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXN=101;
int heap[MAXN],part[MAXN],N;
void DownAdjust(int low,int high) {
    int root = low,child = root*2;
    while(child<=high) {
        if(child+1<=high && heap[child+1] > heap[child])
            child = child + 1;
        if(heap[child] > heap[root]) {
            swap(heap[child], heap[root]);
            root = child;
            child = root*2;
        } else
            break;
    }
}
void Create() {
    for(int i=N/2; i>=1; i--)
        DownAdjust(i, N);
}
bool jud() { // 判断heap数组和part数组是否相等
    for(int i=1; i<=N; i++) {
        if(heap[i]!=part[i])
            return false;
    }
    return true;
}
bool HeapSort() {
    Create();
    for(int i=N; i>1; i--) {
        swap(heap[i],heap[1]);
        DownAdjust(1, i-1);
        if(jud()) {
            swap(heap[i-1],heap[1]);
            DownAdjust(1, i-2);
            return true;
        }
    }
    return false;
}
int main() {
    scanf("%d",&N);
    for(int i=1; i<=N; i++)
        scanf("%d",&heap[i]);
    for(int i=1; i<=N; i++)
        scanf("%d",&part[i]);
    if(HeapSort()) {
        printf("Heap Sort\n");
        for(int i=1; i<=N; i++) {
            if(i>1)
                printf(" ");
            printf("%d",heap[i]);
        }
    } else {
        printf("Insertion Sort\n");
        int pos=1;
        while(pos<N && part[pos]<=part[pos+1])  // 此处一定要考虑重复元素的情况,是part[pos]<=part[pos+1]而不是part[pos]<part[pos+1]
            pos++;
        if(pos<N) {
            for(int i=pos+1; i>1&&part[i]<part[i-1]; i--)
                swap(part[i],part[i-1]);
        }
        for(int i=1; i<=N; i++) {
            if(i>1)
                printf(" ");
            printf("%d",part[i]);
        }
    }
    printf("\n");
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值