C 堆

堆(Heap)

堆,是一种十分基础的数据结构,也是优先队列实现的最好方法,其本身的实现也挺简单的。废话不多说,我们直接来看堆的一些描述和特性。

二叉树

首先,堆其实就是一颗完全二叉树(不了解二叉树的可以看看这个《C/C++ 二叉树》)

在描述一颗二叉树的时候,我们完全可以使用类似链表的方式,一个数据域来储存数据,两个指针来指向其左右节点。但这样储存会导致空间的浪费,所以可以采用数组来储存二叉树。

堆正是一种特殊的二叉树,也就是之前所说的完全二叉树,这样子的设计,可以保证在数组中,空间不会被浪费,因此他的储存效率还是蛮高的。

完全二叉树

先来看一下完全二叉树长啥样。

完全二叉树

可以看出来,这棵树如果一层层,从左到右标上号,是连续的(具体的描述可以看这个《C/C++ 二叉树》),这正符合了数组这种连续的数据结构的特点,因此非常适合用数组(本质上就是连续的内存空间)来进行实现。

数组存储

讲了那么多,我们先用数组来存一波二叉树吧。

重新标号

还是刚刚那棵树,原汁原味,不过这回我把标号从1开始了,这样储存的时候计算下标会更方便。

不难发现,任何一个节点的父节点的标号p 等于 该节点标号i除以2,并向下取整,即p = \lfloor{i / 2}\rfloor

同样的,一个节点的两个子节点i1i2分别等于该节点p的下标 乘以2 和 乘2加1。即i1 = p \times 2i2 = p \times 2 + 1

因此,我们在储存的时候只要注意下,数组下标从1开始计算。



上面的都是废话,正文开始。。。。

堆的描述

堆,正如他的字面意思,是一层层堆上去的,每一层之间都有一些特殊的关系。

在这其中就分为了两种堆,一种叫做大顶堆,另一种叫做小顶堆,其区分的方式便是父节点和其子节点之间的关系不同。

  • 小顶堆

字面意思,也就是在最上面的顶点(root节点)是整个堆最的,往下走,每一个上面的节点都比下面的小。

每个父节点都比子节点小。

  • 大顶堆

同小顶堆的描述,也就是在最上面的顶点(root节点)是整个堆最的,往下走,每一个上面的节点都比下面的大。

每个父节点都比子节点大。

实现

根据堆的描述,我们很容易就用数组实现出来。

不管怎么说我们先开一个数组:

int a[1000];

然后再开一个变量记录堆中元素总数。

int n = 0; // 代表当前堆中元素数量

接着,我们很容易写出一个插入函数:

void push(int num)
{
    a[++ n] = num;
}

但这样也只仅仅是插入元素至数组,那么我们怎么来维护一个堆呢?

我们这边以大顶堆作为例子来进行演示。

维护堆

插入

很容易理解,如果一个堆里没有元素或者只有一个元素,那他就是符合堆的描述的。

那么,我们在插入第二个元素的时候,就有可能出现子节点比父节点大的情况,这时候我们就需要进行交换。而每个这样的上下交换,便叫做shift-up

上浮过程

上图描绘的便是一个堆简单的维护过程。在大顶堆中,只要发现新插入的元素比其父节点来得大,那就进行交换,然后一直重复这个操作到root节点。很明显,插入一次的时间复杂度是O(logn)

根据这个过程,我们很容易就写出代码。

首先随便写一个交换,当然用C++的algorithm头文件也行。

void swap(int &a, int &b)
{
    if (a == b) return; // 防止交换相同元素导致都=0
    a ^= b;
    b ^= a;
    a ^= b;
}

然后就是我们的shift-up的代码:

递归版本:

void _up(int i)
{
    int p = i / 2;
    if (p == 0) return;
    if (a[i] > a[p]) {
        swap(a[i], a[p]);
        _up(p);
    }
}

循环版本(一般用这个):

void up(int i)
{
    int p = i / 2;
    while (p != 0 && a[i] > a[p])
    {
        swap(a[i], a[p]);
        i = p;
        p /= 2;
    }
}

代码的意思很直白,先计算一个节点的父节点的下标p,然后判断a[i]与a[p]之间的大小关系,不对就交换,然后移动到父节点,继续这个过程。

因此堆的push方法就是这样的了

void push(int num)
{
    a[++ n] = num;
    up(n);
}

删除

根据堆的设计,我们一般删除的节点就是root节点,也就是对应数组的a[1]。

删除的方式,其实也挺简单,交换a[1]和a[n],也就是交换root节点和最后一个节点,然后在从上到下进行一遍维护,也就是shift-down操作,恰好和之前的shift-up操作相反。

下沉操作

图中,红色的代表删除的节点,橙色的代表位置不正确的节点,最后一直进行shift-down操作,使所有节点归位,复杂度为O(logn)

同时,最后这个堆中所有的元素是呈一定的顺序的,将它以普通的数组展现出来的时候,它便是升序排序排好的:[1, 2, 3, 4, 5],因此有一种排序就叫做堆排序

同样的,我还是给出两个版本。
递归版本:

void _down(int i)
{
    int k = i * 2;
    if (k > n) return;
    if (k + 1 <= n && a[k + 1] > a[k]) k ++;
    if (a[i] < a[k])
    {
        swap(a[i], a[k]);
        _down(k);
    }
}

循环版本:

void down(int i)
{
    int k = i * 2;
    if (k + 1 <= n && a[k + 1] > a[k]) k ++;
    while (k <= n && a[i] < a[k])
    {
        swap(a[i], a[k]);
        i = k;
        k *= 2;
        if (k + 1 <= n && a[k + 1] > a[k]) k ++;
    }
}

上述代码的意思就是首先计算他的儿子节点k的下标,然后比较左右两个儿子的大小,选择大的那个,之后再与之交换,然后一直进行这个过程,直到符合为止。

有了shift-down函数做基础,那么删除函数,也就变得十分简单,只要交换头尾元素即可。

void pop()
{
    if (n > 0) // n是元素个数,要注意>0才能pop
    {
        swap(a[1], a[n --]); // 交换头尾元素
        down(1); // 开始shift-down
    }
}

建堆

建堆,是将一个不符合堆的描述的数组 转化成 一个符合堆的描述的一个数组。

不要把这个过程想得太复杂,其实很简单,仅仅只需要用到上文中的shift-down函数即可。

一颗完全二叉树

还是以上文中的那棵树为例,我们假设需要构建一个大顶堆,而输入的数组为:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12](如图所示)。

那么,我们只需要从最后一个拥有子节点的父节点开始递减,直到root节点。

很显然,图中最后一个父节点便是6号节点,所以说我们只需for (int i = 6;i >= 1;i --)一直递减即可。

由上文的规律可知:

任何一个节点的父节点的标号p 等于 该节点标号i除以2,并向下取整,即p = \lfloor{i / 2}\rfloor

所以,只要根据最后一个节点的下标,即可求出最后一个父节点的下标。即P = \lfloor{n/2}\rfloor

建堆代码:

void heapify()
{
    for (int i = n / 2;i >= 1;i --)
    {
        down(i);
    }
}

值得注意的是,这种方式的时间复杂度是O(n),推倒过程百度上很多,可以查阅。

完整代码

//
//  main.cpp
//  Heap
//
//  Created by JackLee on 2020/2/20.
//  Copyright © 2020 JackLee. All rights reserved.
//

#include <stdio.h>

//------------------------------------------------------------------------
//------------------------------打印堆函数BEGIN-----------------------------
#include <math.h>
#include <vector>

using namespace std;

struct ps {
    int dps;
    int length;
    int type;
};

void print_binTree(int *root, int n, int index, int d, int lr, vector<ps> dps) // 打印堆函数,用于直观的显示堆中元素
{
    if (index > n) return;
    
    ps p = {d, (int) log10(root[index]) + 1, index * 2 + 1 <= n && lr == 0};
    if (dps.size() <= d) dps.push_back(p);
    else dps[d] = p;
    print_binTree(root, n, index * 2 + 1, d + 1, 1, dps);
    for (vector<ps>::iterator i = dps.begin();i != dps.end() - 1;i ++)
    {
        if (i -> type && i -> dps != 0) printf("|");
        else printf(".");
        for (int j = 0;j < i -> length + ((i -> dps) != 0) * 2;j ++)
        {
            printf(".");
        }
    }
    if (d != 0) printf("|-");
    printf("%d",root[index]);
    if (index * 2 <= n || index * 2 + 1 <= n) printf("-|");
    printf("\n");
    dps[d].type = index * 2 <= n && lr;
    print_binTree(root, n, index * 2, d + 1, 0, dps);
}
//------------------------------打印堆函数END-------------------------------
//------------------------------------------------------------------------


int a[1000]; // 从1开始
int n = 0;

void swap(int &a, int &b)
{
    if (a == b) return; // 防止交换相同元素导致都=0
    a ^= b;
    b ^= a;
    a ^= b;
}

void _up(int i)
{
    int p = i / 2;
    if (p == 0) return;
    if (a[i] > a[p]) {
        swap(a[i], a[p]);
        _up(p);
    }
}

void up(int i)
{
    int p = i / 2;
    while (p != 0 && a[i] > a[p])
    {
        swap(a[i], a[p]);
        i = p;
        p /= 2;
    }
}

void _down(int i)
{
    int k = i * 2;
    if (k > n) return;
    if (k + 1 <= n && a[k + 1] > a[k]) k ++;
    if (a[i] < a[k])
    {
        swap(a[i], a[k]);
        _down(k);
    }
}

void down(int i)
{
    int k = i * 2;
    if (k + 1 <= n && a[k + 1] > a[k]) k ++;
    while (k <= n && a[i] < a[k])
    {
        swap(a[i], a[k]);
        i = k;
        k *= 2;
        if (k + 1 <= n && a[k + 1] > a[k]) k ++;
    }
}

void push(int num)
{
    a[++ n] = num;
    up(n);
}

void pop()
{
    if (n > 0)
    {
        swap(a[1], a[n --]);
        down(1);
    }
}

void heapify()
{
    for (int i = n / 2;i >= 1;i --)
    {
        down(i);
    }
}

int main(int argc, const char * argv[]) {
    // insert code here...
    int t;
    printf("The heap array's length: ");
    scanf("%d",&t);
    int in;
    for (int i = 1;i <= t;i ++) scanf("%d",a + i);
    n = t;
    heapify();
    printf("Heapified\n");
    print_binTree(a, n, 1, 0, 1, vector<ps>());
    printf("\n");
    while (1)
    {
        printf("Operation? (0 - push, 1 - pop, 2 - sort and quit, else - quit): ");
        scanf("%d",&in);
        if (!in)
        {
            printf("Your number: ");
            scanf("%d",&in);
            push(in);
            print_binTree(a, n, 1, 0, 1, vector<ps>());
            printf("\n");
        } else if (in == 1)
        {
            printf("Poped: %d\n",a[1]);
            pop();
            print_binTree(a, n, 1, 0, 1, vector<ps>());
            printf("\n");
            
        } else if (in == 2)
        {
            t = n;
            printf("The final array: [");
            while (n) pop();
            for (int i = 1;i <= t;i ++)
            {
                if (i != 1) printf(", ");
                printf("%d",a[i]);
            }
            printf("]\n");
            break;
        } else break;
        
    }
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272