二叉堆详解 及 C代码实现

二叉堆

[注] 本文以小根堆为演示

存储

二叉堆,本质上是一棵二叉树——一棵完全二叉树

完全二叉树 是 效率很高的数据结构,完全二叉树 是由 满二叉树 而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。

因为有这个性质,,我们可以很方便的存储一棵二叉堆。

假设存在一个数组array[],array[i]表示的是节点i存储的数值。

此时我们令1为这颗完全二叉树的根节点,,,对于节点a来说, a×2 a × 2 是a的左儿子, a×2+1 a × 2 + 1 是a的右儿子(有疑问可以自己画一棵二叉树,标上序号)

堆性质

对于一个堆,这里以小根堆为例,大根堆相反

这个堆的任意一个节点的值都要小于等于这个节点的所有儿子节点

扩展一下可以得知,这个堆中某一棵子树的根节点,是这棵子树中所有节点中值最小的一个

然后如果我们要取得堆中所有元素的最小值,那么只需要获取一下array[1]即可

堆操作

一个二叉堆的操作,最常用的主要有三:

  1. 向堆中加入一个元素
  2. 取出堆中最小的那个元素
  3. 删除堆中最小的元素

接下来我们看看如何完成这三个操作

实现

其实完成以上操作(主要是加入和删除元素),其实都紧紧围绕着一个东西:

维护堆性质

首先来看加入元素,因为要加入的元素我们并不知道大小,所以从堆里找一个元素进行替换这个做法明显不可取,此时我们就要想一想极端的:

  1. 加入的这个数字比堆中任何数字都小
  2. 加入的这个数字比堆中任何数字都大

可以得知,如果以上条件我们的堆可以维护的话,那么其他任何条件想维护起来都不困难了

所以,我们可以把这个数字放在Array数字的末尾后一位(此处的末尾是堆的末尾)

然后堆又是个完全二叉树,所以此时这个新添加的节点算是这棵树的叶子节点,,,好了,既然节点已经加入进来了,我们就要考虑如何维护堆性质了

此时该节点已经是叶子节点,所以不存在什么儿子要大于父亲(自己),此时这个节点的身份是上一层某个节点的儿子,所以我们要考虑的仅仅是该节点的父亲是否小于该节点,如果不满足堆性质则两者交换值

【注】数组存储的完全二叉树中,儿子id = a,则其父亲节点的id = (int)(a÷2) ( i n t ) ( a ÷ 2 )

如果交换节点后,此时这个新节点就不是叶子节点了,而变成了它之前的父节点,然后我们还要继续检测堆性质是否满足,而我们改变的仅仅是这个节点,所以继续检查这个节点就好了,,,接着刚才,这个节点与父节点互换,所以此节点的儿子节点都已经满足堆性质,所以我们继续考虑这个节点的父节点即可,直到满足堆性质或者这个节点已经成为了堆的根。

代码:(其实还是很简短的)

void swap_ (int *a,int *b) {
    *a = *a ^ *b;
    *b = *a ^ *b;
    *a = *a ^ *b;
}

void insert_ (int num) {
    heap[++tail] = num;
    int n = tail, p = n >> 1;
    while (p && heap[p] > heap[n]) {
        swap_(&heap[p],&heap[n]);
        n = p, p = n >> 1; //利用二进制加速
    }
}

接下来就取最小值操作了,,这个没啥说的,返回array[1]就好

最后一个就是pop操作,也就是删除最小值

根据堆性质,最小值一定就是节点1,也就是root节点,所以我们只需要”删除”掉array[1]就好,但是如果真的删除,那么整棵树都会炸掉。。

这样想,我们只是删除了root节点,而root的俩个儿子表示的子树都分别满足堆性质,完全可以用一个无穷大INF来替换掉root,表示删除了root,然后我们在维护一下堆性质,可以知道,INF最终会移动到堆的叶子节点,,,堆的root则替换成了堆中的其他节点,而堆性质仍然满足,,,

但是,这样太麻烦了,因为这里是用数组存储的堆,我们完全可以用堆数组中的最后一个元素替换掉root,然后将堆的大小size-1,表示删除掉root节点,然后对root维护堆性质。

具体操作:
因为堆性质是父亲节点要小于任意儿子,所以我们取两个儿子中更小的那个和root比较
如果不满足堆性质,则交换变量,满足则结束

如果交换了变量,则该节点上方的节点都满足了堆性质,所以我们考虑下面就好,继续比较该变量与其子儿子,直到满足堆性质或者到达堆的尾部。

代码:

void pop () {
    heap[1] = heap[tail--];
    int p = 1, son = p << 1;
    while (son <= tail) {
        if (son < tail && heap[son] > heap[son + 1]) son++;
        if (heap[p] <= heap[son]) break;
        swap_(&heap[p], &heap[son]);
        p = son,son = p << 1;
    }
}

完整代码

最后贴完整代码

#include <stdio.h>

int heap [10000],tail;

void swap_ (int *a,int *b) {///½»»»±äÁ¿
    *a = *a ^ *b;
    *b = *a ^ *b;
    *a = *a ^ *b;
}

void insert_ (int num) {
    heap[++tail] = num;
    int n = tail, p = n >> 1;
    while (p && heap[p] > heap[n]) {
        swap_(&heap[p],&heap[n]);
        n = p, p = n >> 1;
    }
}

int top() {
    if (tail) return heap[1];
    return 0;
}

void pop () {
    heap[1] = heap[tail--];
    int p = 1, son = p << 1;
    while (son <= tail) {
        if (son < tail && heap[son] > heap[son + 1]) son++;
        if (heap[p] <= heap[son]) break;
        swap_(&heap[p], &heap[son]);
        p = son,son = p << 1;
    }
}

int main () {
    insert_(3);
    insert_(2);
    insert_(1);
    insert_(4);

    printf ("%d\n", top());
    pop ();
    printf ("%d\n", top());
    pop ();
    printf ("%d\n", top());
    pop ();
    printf ("%d\n", top());
    pop ();
    printf ("%d\n", top());
    pop ();
    printf ("%d\n", top());
    return 0;
}

输出:

1
2
3
4
0
0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值