堆的结构:

一颗完全二叉树(十分平衡,除最后一层(叶结点)以外,其他节点均非空,最后一层从左到右依次排布)。

小根堆

也指最小堆。经过排序的完全二叉树,其中每一个非终端节点均小于其左右子节点,其根节点为所有元素的最小值。

大根堆同理。

存储方式

用一维数组维护堆状数据结构。

一号点为根节点。下标为x的节点,其左节点下标为2 * x,右节点下标为2 * x + 1。

基本操作

1.向下调整(down操作):

若将一个数变大,则须将这个节点下移。以小根堆为例,将一个节点下移,则需要判断它与左右子节点的大小关系,并将它与(自己,左节点,右节点)三个数中的最小值交换。

void down(int u)
{
	int t = u; // 定义t,用来寻找三个数中最小值的下标。
    if (u * 2 <= size && h[u * 2] < h[u]) t = u * 2; // 判断左节点是否存在,且左节点是否更小。
    if (u * 2 + 1 <= size && h[u * 2 + 1] < h[u]) t = u * 2 + 1; // 同上,判断右节点。
    if (t != u) // 需要操作
    {
        swap(h[u], h[t]);
        down (t); // 递归操作,直到u为当前三个数的最小值。
    }
}

2.向上调整(up操作):

同理,要将一个数变小,则需将这个节点上移,对于小根堆,上移操作仅需跟它的父节点比较即可。

void up(int u)
{
	while (u / 2 && h[u / 2] > h[u]){
		swap(h[u / 2], h[u]);
         u /= 2;
    }// 迭代操作,当为头节点或不能再向上时结束。
}

3.插入一个数:

将该数插入到整个堆的最后一个位置,然后进行up操作。

heap[++ size] = x; up(size);

4.删除任意一个元素:

对于一维数组,删除数组的尾部非常简单,令size–即可,但若要删除中间元素,则需要整体改变下标的位置。

假设我们现在要删除第k个节点,只需将第k个节点变为最后一个节点,然后再删除最后一个节点。最后,对k节点进行down操作或up操作(因为此处第k个节点被赋给了最后一个结点的值),为了不用再进行判断,我们让k节点都进行up和down操作(不用担心会额外进行操作,当满足条件时,up操作和down操作只会执行其一)。

heap[k] = heap[size]; size -- ; down(k); up(k);// 删除头节点,将K改为1即可。

5.求集合中的最小(大)值:

对堆来说,最值即为根结点。

heap[1]; //大根堆为最大值,小根堆为最小值。

6.修改任意一个元素:

将要修改的元素(k节点)修改后,再进行一次up操作和down操作。

heap[k] = x; down(k); up(k);

7.堆的构建

如果我们将n个元素每个都进行一次down操作,则时间复杂度为O(nlogn),也可通过优化,将复杂度改为O(n)。

堆的最后一层至多有n / 2个元素,如果我们要对所有元素进行down操作,则只需要对倒数第二层以上进行down操作,在对倒数第二层进行down操作时,也会保证处理到最后一层的元素,这样,我们就只须处理n / 2个数据,时间复杂度变为O(n)。

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

进阶:堆的映射:

当我们把上述所提到的删除和修改第K个节点改为删除和修改第K个插入的数,就会变成更复杂的情况。

在这里我们需要额外开两个数组ph[N]和hp[N],ph[k]用来存储第k个插入的数在堆中的下标,但仅有此并不够,当我们在进行up或down操作时,堆中的值会改变。

举个例子,当第5个插入的数在堆中的下标为3,其子节点在堆中的下标为6。那么ph[5]即为3,但此时小根堆是不稳定的,如果我们要进行一次down操作,会将heap[3]和heap[6]的值进行交换,此时第5个插入的数,即ph[5]就会改变。一句话来说,单单用ph数组对堆进行映射是单向的关系

为了解决这种情况,我们另设一个数组hp[N]用来表示,第k个插入的数在堆中的下标在ph数组中的值。也就是说,用此数组将堆中的下标映射与ph数组,从而达到双向的关系。

ph数组与hp数组互为反函数。即ph[j] = k, hp[k] = j;

有这两个工具,我们就可以将down和up操作中的swap函数进行“升级”,即不仅仅改变堆中的两个值,他们所对应的插入关系也应该交换。

void heap_swap(int a, int b)
{
	swap(ph[hp[a]], ph[hp[b]]);
	swap(hp[a], hp[b]);
	swap(h[a], h[b]);
}

堆排序完整代码

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int n, m;
int h[N], cnt;

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);
    }
}

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

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);
    }

    return 0;
}

文章作者: Cedeat
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cedeat !
  目录