数据结构-堆

简单介绍:

堆是利用完全二叉树的结构来维护一组数据,然后进行相关操作,一般的操作进行一次的时间复杂度在 O(1) ~ O(logn) 之间。

完全二叉树:若设二叉树的深度为 h,除第 h 层外,其它各层 ( 1~ h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。

如图所示为一棵完全二叉树:

wanquanerchashu

堆分为两种类型:大根堆小根堆

顾名思义,就是保证根节点是所有数据中最大/小

堆的存储:一个一维数组,节点编号从 1 开始,左儿子的编号 2x 、右儿子的编号 2x + 1

up 操作:

模拟:在堆中分别加入:{8,5,2,10,3,7,1,4,6}。如图所示:

insert_num
以**小根堆**为例:从上面的数据中可以看出,根节点 `1` 元素 `8` 绝对不是最小的。我们很容易发现它的一个儿子节点 `3` (元素 `2` )比它来的小,我们怎么将它放到最高点呢?很简单,可以直接交换!但是,我们又发现了,`3` 的一个儿子节点 `7` (元素 `1` )似乎更适合在根节点。这时候我们是无法直接和根节点交换的,那我们就需要一个操作来实现这个交换过程,那就是 `up` 操作。从下往上找比自己大的数,然后递归交换。

操作过程如下:

从当前结点开始,和它的父亲节点比较,若是比父亲节点来的小,就交换,然后将当前询问的节点下标更新为原父亲节点下标;否则退出。

up
void up(int u)
{
    while (u / 2 && h[u] < h[u / 2]) 
    {
        swap(h[u], h[u / 2]);
        u >>= 1;
    }
}

down 操作

这一次 up 完毕之后呢,我们又发现了一个问题,貌似节点 3 (元素 8 )不太合适放在那,而它的子节点 7 (元素 2 )好像才应该在那个位置。此时应该让节点 3 下沉!那么问题来了:节点 3 应该往哪下沉呢?我们知道,小根堆是尽力要让小的元素在较上方的节点,而下沉与上浮一样要以交换来不断操作,所以我们应该让节点 7 与其交换。

由此我们可以得出 down 的算法了:

让当前结点的左右儿子(如果有的话)作比较,哪个比较小就和它交换,并更新询问节点的下标为被交换的儿子节点下标,否则退出。

模拟操作图示:

down
void down(int u)
{
    int t = u; // t 表示三个点中的最小值
    if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2; // 左儿子
    if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;//右儿子
    
    if (u != t) // 如果当前的父亲不是最小的,就交换,并递归处理
    {
        swap(h[u], h[t]);
        down(t);
    }
}

其他操作

插入操作:

如何在插入的时候维护堆的性质呢?每次插入的时候我们都在最后一个位置cnt 插入,然后 up(cnt)

弹出操作:

输出栈顶元素,也就是输出最小值。输出栈顶元素,然后让最后一个元素和栈顶进行交换,然后让现在的根元素 down 就可以了。

初始化堆操作

//o(n)建堆
for (int i = n / 2; i; i --) down(i);

i 为什么从 n/2 开始 down

首先要明确要进行 down 操作时必须满足左儿子和右儿子已经是个堆。

开始创建堆的时候,元素是随机插入的,所以不能从根节点开始 down ,而是要找到满足下面三个性质的结点:

  1. 左右儿子满足堆的性质。

  2. 下标最大(因为要往上遍历)

  3. 不是叶结点(叶节点一定满足堆的性质)

那这个点为什么是 n/2

在这里插入图片描述

通俗的说:从 n/2 开始,还有一个角度可以理解,因为 n 是最大值,n/2n 的父节点,因为 n 是最大,所以n/2 是最大的有子节点的父节点,所以从 n/2 往前遍历,就可以把整个数组遍历一遍。

Q:为什么不能从根节点开始沉啊,为什么满足三个性质才能沉呢 ?

A:因为读进来的数是随机的,你建堆的时候可能根节点小于他的左右儿子,而根的左右儿子大于他们的左右儿子,这样排出来的数组就不对了,所以只能自下而上沉。

手写堆

手写实现堆的几个操作:

  1. 插入一个数
  2. 求集合中的最小值
  3. 删除最小值
  4. 删除任意一个元素
  5. 修改任意一个元素
heap_operator

例题:

原题链接:AcWing 838. 堆排序

堆排序补充
  1. 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序, 它的最坏、最好、平均时间复杂度均为 O(nlogn), 它也是不稳定排序。

  2. 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值, 这种情况称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。

  3. 每个结点的值都小于或等于其左右孩子结点的值, 这种情况称为小顶堆。

  4. 大顶堆举例说明:

big_heap

  1. 小顶堆举例说明:
small_heap
  1. 一般升序采用大顶堆, 降序采用小顶堆。
基本思想:
  1. 将待排序序列构造成一个大顶堆。
  2. 此时,整个序列的最大值就是堆顶的根节点。
  3. 将其与末尾元素进行交换, 此时末尾就为最大值。
  4. 然后将剩余 n - 1 个元素重新构造成一个堆, 这样会得到 n 个元素的次小值。 如此反复执行, 便能得到一个有序序列了。
  5. 可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。
图解:

数组 {4,6,8,5,9} 要求使用堆排序法,将数组升序排序(升序采用大顶堆,降序采用小顶堆)。

6.1 构造堆

  1. 将给定无序序列结构如下。
image
  1. 从最后一个非叶子结点开始,从右至左,从下至上进行调整。
    调整规则:找到该节点和他的所有儿子。如果该节点中存的值是找到节点值中的最大值,则不进行调整。如果不是,就将该节点的值和最大值进行交换,然后递归的调整和该节点交换值得那个节点。如下图:

      最后一个非叶子结点开始(也就是下面的 `6` 结点),从左至右,从下至上进行调整。`6` 有两个儿子:`5` 和 `9`,这三个值中 `9` 最大。`6` 和 `9` 交换。
    
image
    因为 `6` 和 `9` 交换了,递归处理现在保存 `6` 的那个节点,发现它没有儿子,停止递归。

    找到第二个非叶节点 `4` ,由于`[4, 9, 8]`中 `9` 元素最大,`4`  和 `9` 交换。
image
     因为 `4` 和 `9` 交换了,递归处理现在保存 `4` 的那个节点: 找到它的两个儿子:`5, 6`, 其中 `6` 最大,交换 `4` 和 `6`。
image
        然后继续递归处理,发现现在保存 `4` 的节点没有儿子,停止。

        此时,我们就将一个无序序列构造成了一个大顶堆。

6.2 排序

将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。如下图:

  • 将堆顶元素 9 和末尾元素 4 进行交换。

    image
  • 重新调整结构(规则如上),使其继续满足堆定义

    image
  • 再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8

    image
  • 后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序。

    image

6.3 总结:

  1. 将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆。

  2. 将堆顶元素与末尾元素交换,将最大元素”沉”到数组末端。

  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。

6.4 完整代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int h[N], cnt; //cnt 表示heap中有多少元素。下标从 1 开始

void down(int u)
{
    int t = u;
    if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        swap(h[u], h[t]);
        down(t);
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    cnt = n;

    for (int i = n / 2; i; i -- ) down(i);

    while (m -- )
    {
        printf("%d ", h[1]);
        h[1] = h[cnt -- ];
        down(1);
    }

    puts("");

    return 0;
}
参考资料:

基本数据结构――堆的基本概念及其操作

堆排序 分析i = n/2 开始初始化

AcWing 838. 堆排序

本文供自己复习数据结构所用

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容