AcWing 838. 堆排序

题目描述


分析:

前置

堆是一棵完全二叉树,树中每个结点的值都小于(或大于)其左右孩子的值。其中,如果父亲结点的值小于左右孩子结点的值,那么称这样的堆为小根堆,这时每个结点的值都是以它为根结点的子树的最小值。堆一般用于优先队列的实现。

对于堆的基本概念还有一点是需要了解的。完全二叉树可以使用一维数组来存储,这样结点就按照层序存储于数组中,其中一个第一个节点将存储与数组中的 1号下标位(从 0 0 0 开始也可以),并且数组 i i i 号位表示的结点的左孩子就是 2 i 2i 2i 号位,而右孩子则是 ( 2 i + 1 2i+1 2i+1) 号位。

如何建堆就成了解题的关键。建堆大体思想为将序列中逐个元素向下调整:即先将当前结点(记为 v)与它的左孩子(若有的话),假如左孩子的值比 v 小的话,将其互换位置;再与当前根结点的右孩子进行比较,按照同样方法操作。若原先结点 v 与左右孩子有过交换,则让 v 继续向下与孩子结点比较,按照相同方法操作;若原先结点 v 与左右孩子没有交换,说明 v 小于左右孩子,小根堆成立。由二叉树的性质可知向下调整操作的时间复杂度为 O ( l o g n ) O(logn) O(logn)

// 递归版本
void downAdjust(int v)
{
    // 用 t 来表示子树中的最小值
    int t = v;
    // 判断左儿子是否存在以及左孩子值是否小于根结点的值
    if (v * 2 <= ssize && heap[v * 2] < heap[t]) t = v * 2;
    // 判断右儿子是否存在以及右孩子值是否小于根结点的值
    if (v * 2 + 1 <= ssize && heap[v * 2 + 1] < heap[t]) t = v * 2 + 1;
    
    // 如果 t 发生了变化(不等于 v)即说明在子树中找到了比根结点更小的值
    // 那么 v 结点就要继续向下调整
    if (v != t)
    {
        // 将其交换,维护小根堆的特性
        swap(heap[v], heap[t]);
        // 以找到的较小值的位置为根结点继续向下调整
        downAdjust(t);
    }
}

// 非递归本版本
void downAdjust(int v, int last)
{
    // v 为欲调整结点,t 表示了是否产生了调整
    int t = v;
    while (v * 2 <= last) // v 存在孩子结点
    {
        // 判断左儿子是否存在以及左孩子值是否小于根结点的值
        if (v * 2 <= ssize && heap[v * 2] < heap[t]) t = v * 2;
        // 判断右儿子是否存在以及右孩子值是否小于根结点的值
        if (v * 2 + 1 <= ssize && heap[v * 2 + 1] < heap[t]) t = v * 2 + 1;
        
        // 如果 t 发生了变化(不等于 v)即说明在子树中找到了比根结点更小的值
        // 那么 v 结点就要继续向下调整        
        if (t != v)
        {
            swap(heap[v], heap[t]);
            // v 来到来到其左/右孩子的位置
            v = t;
        }
        else break; // 无调整,小根堆成立
    }
}

理解了建堆的思想,我们来看一下建堆的具体实现:假设序列中元素的个数为 n n n,由于完全二叉树叶子结点个数为 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor n/2,因此数组下标在 [ 1 , ⌊ n / 2 ⌋ ] [1,\lfloor n/2 \rfloor] [1,n/2⌋] 范围内的结点都是非叶子结点。于是可以从 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor n/2 号位开始倒着枚举结点,对每个遍历到的结点 i i i 进行向下调整。倒着枚举可以在每次调整完一个结点后使当前子树值最小的结点出现在根结点的位置,这样当遍历到父结点时,就可以直接使用这个结果。也就是说,这种做法保证每个结点都是以其为根结点的子树中权值最小的结点,时间复杂度为: O ( n ) O(n) O(n)

// 为了表示清楚写成一个函数
void createHeap()
{
	for (int i = n / 2; i >= 1; i --) down(i);
}

对于我们已经建好的堆,如果要删除堆中最小元素(堆顶元素)并仍让其保持堆的结构该怎么做?这是很重要的,因为题意要求我们从小到大输出前 m m m 个数,那我们只需要每次输出堆顶元素再对根结点进行调整即可:具体方法是用最后一个元素覆盖堆顶元素再对更新后的根结点进行向下调整,时间复杂度为: O ( l o g n ) O(logn) O(logn)

void deleteTop()
{
	// 初始化时 ssize = n
	heap[1] = heap[ssize --];
	downAdjust(1);
}

上面我们一共介绍了三个操作 downAdjustcreateHeapdeleteTop。这是堆结构的精华所在,接下来给出一个样例中堆的调整过程,我从这里感受到了关于堆的“别扭”的美感。(草稿过程是刻意留下的,能够加深思考)

5 3
4 5 1 3 2

在这里插入图片描述


代码(C++)

#include <iostream>

using namespace std;

const int N = 100010;
int heap[N], ssize, n, m;

void downAdjust(int v)
{
    // 用 t 来表示子树中的最小值
    int t = v;
    // 判断左儿子是否存在以及左孩子值是否小于根结点的值
    if (v * 2 <= ssize && heap[v * 2] < heap[t]) t = v * 2;
    // 判断右儿子是否存在以及右孩子值是否小于根结点的值
    if (v * 2 + 1 <= ssize && heap[v * 2 + 1] < heap[t]) t = v * 2 + 1;
    
    // 如果 t 发生了变化(不等于 v)即说明在子树中找到了比根结点更小的值
    // 那么 v 结点就要继续向下调整
    if (v != t)
    {
        // 将其交换,维护小根堆的特性
        swap(heap[v], heap[t]);
        // 以找到的较小值的位置为根结点继续向下调整
        downAdjust(t);
    }
}

// void downAdjust(int v, int last)
// {
//     // v 为欲调整结点,t 表示了是否产生了调整
//     int t = v;
//     while (v * 2 <= last) // v 存在孩子结点
//     {
//         // 判断左儿子是否存在以及左孩子值是否小于根结点的值
//         if (v * 2 <= ssize && heap[v * 2] < heap[t]) t = v * 2;
//         // 判断右儿子是否存在以及右孩子值是否小于根结点的值
//         if (v * 2 + 1 <= ssize && heap[v * 2 + 1] < heap[t]) t = v * 2 + 1;
        
//         // 如果 t 发生了变化(不等于 v)即说明在子树中找到了比根结点更小的值
//         // 那么 v 结点就要继续向下调整        
//         if (t != v)
//         {
//             swap(heap[v], heap[t]);
//             // v 来到来到其左/右孩子的位置
//             v = t;
//         }
//         else break; // 无调整,小根堆成立
//     }
// }


int main()
{
    int n, m;
    cin >> n >> m;
    ssize = n;
    
    for (int i = 1; i <= n; i ++) cin >> heap[i];
    
    for (int i = n / 2; i >= 1; i --) downAdjust(i);
    
    
    
    while (m --)
    {
        cout << heap[1] << ' ';
        heap[1] = heap[ssize --];
        downAdjust(1);
    }
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值