堆(heap)

91 阅读3分钟

这里指二叉堆,用 完全二叉树实现。可以以 log2nlog_2n 的时间复杂度找到 n个数的最值,分为

大根堆

堆顶元素为 n 个元素中的最大值 小根堆 同理

从二叉树我们可以知道堆也是一种递归定义的结构(其实也就是一颗完全二叉树)我们可以用递归来实现它的算法。

1)堆构造

一般使用数组来实现,我们从最后一个叶子节点进行堆调整那么假设我们有一个 数组

arr=[..]arr=[…..]

它的下标从 0 开始 ,但是为了我们方便的在父节点和子结点间移动,我们从 1 这个下标开始移动数据。那么就有如下 code

#include <stdio.h>
#define N 12
typedef int et;
void initialize();

void floating(int index);
void _floating(int index);

void sinking();

et top();
int insert(et val);

int max(et lc, et rc);
int swap(int index1, int index2);


et a[100] = {0, 3, 4, 5, 6, 3, 2, 7, 8, 9, 1, 8};

// 多出了一位 方便我们套用公式 left_child=index*2 等
int size = N - 1;

那么如果当前节点的下标是 index 那么 这个节点的 leftchild 如果存在 它的位置是 index*2 它的 rightchild 同样 是 index*2+1

构造堆时我们需要进行两次调整 自下而上(从最后一个分支节点开始到根节点) 自上而下(从第一个分支节点开始到最后一个分支节点)

至于为什么需要两次,我想是因为 第一次从下往上时我们 将每一棵子树的最值 从下往上传递,那么根节点无疑是我们需要的 n个节点中的最值,但是根节点的左右子树并不满足 堆的特性,因为节点在上升时势必会有节点被换下来,被换下来的节点无法确定和原根节点 的左右节点的关系。

当我们从 第一个分支节点开始向下调整时那么就又变成一个由小问题组成大问题了(此时根节点已经被调整好了),我们每次确定一个分支节点的最大值(那么这个节点和被调整好的集合组合到一起就满足堆的特性),那么一直到叶子节点整个堆就被确认好了。

void floating(int index)
{

    int rc_index = index * 2 + 1 <= size ? index * 2 + 1 : -1;

    int sele_index = -1;

    if (rc_index != -1)
    {
        if (max(a[index * 2], a[rc_index]))
            sele_index = rc_index;
        else
            sele_index = index * 2;
    }
    else
        sele_index = index * 2;

    if (a[index] > a[sele_index])
        swap(index, sele_index);
}
void _floating(int index)
{

    if (index < 1)
        return;

    int rc_index = index * 2 + 1 <= size ? index * 2 + 1 : -1;
    int sele_index = -1;
    if (rc_index != -1)
    {
        if (max(a[index * 2], a[rc_index]))
            sele_index = rc_index;
        else
            sele_index = index * 2;
    }
    else
        sele_index = index * 2;

    if (a[index] > a[sele_index])
        swap(index, sele_index);

    return _floating(--index);
}

这是递归的向上调整方法

void sinking(int index)
{
    int rc_index = index * 2 + 1 <= size ? index * 2 + 1 : -1;
    int sele_index = -1;
    if (rc_index != -1)
    {
        if (max(a[index * 2], a[rc_index]))
            sele_index = rc_index;
        else
            sele_index = index * 2;
    }
    else
        sele_index = index * 2;

    if (a[index] > a[sele_index])
        swap(index, sele_index);
}

向下调整 | 也可以选择使用递归来实现

2)堆插入

我们可以使用 O(logn) 的时间复杂度 向 堆中插入元素,具体操作是,将元素作为 整个完全二叉树的新叶子节点。然好进行一次向上调整。

int insert(et val)
{
    a[++size] = val;
    _floating(size / 2);
}

3)取堆顶

将堆顶元素和最后一个叶子节点交换位置(堆的长度减小),然后从堆顶进行一次向下调整。

et top()
{
    swap(1, size--);
    // 往下调整
    for (int i = 1; i <= size / 2; i++)
    {
        sinking(i);
    }

    return a[size + 1];
}

4)总结

#include <stdio.h>
#define N 12
typedef int et;
void adjust(int index);
void initialize();
void floating(int index);
void sinking();
int insert(et val);
int max(et lc, et rc);
int swap(int index1, int index2);
et top();
void _floating(int index);
et a[100] = {0, 3, 4, 5, 6, 3, 2, 7, 8, 9, 1, 8};

// 多出了一位 方便我们套用公式 left_child=index*2 等
int size = N - 1;

int main(int argc, char const *argv[])
{
    initialize();

    insert(-1);
    // printf("%d", );
    printf("%d", top());
    return 0;
}

void initialize()
{
    int n = size;

    for (int i = n / 2; i >= 1; i--)
    {
        floating(i);
    }

    for (int i = 2; i <= n / 2; i++)
    {
        sinking(i);
    }
}

void floating(int index)
{

    int rc_index = index * 2 + 1 <= size ? index * 2 + 1 : -1;

    int sele_index = -1;

    if (rc_index != -1)
    {
        if (max(a[index * 2], a[rc_index]))
            sele_index = rc_index;
        else
            sele_index = index * 2;
    }
    else
        sele_index = index * 2;

    if (a[index] > a[sele_index])
        swap(index, sele_index);
}

void sinking(int index)
{
    int rc_index = index * 2 + 1 <= size ? index * 2 + 1 : -1;
    int sele_index = -1;
    if (rc_index != -1)
    {
        if (max(a[index * 2], a[rc_index]))
            sele_index = rc_index;
        else
            sele_index = index * 2;
    }
    else
        sele_index = index * 2;

    if (a[index] > a[sele_index])
        swap(index, sele_index);
}

int max(et lc, et rc)
{
    return lc > rc ? 1 : 0;
}

int swap(int index1, int index2)
{

    et temp = a[index1];
    a[index1] = a[index2];
    a[index2] = temp;
}

et top()
{
    swap(1, size--);
    // 往下调整
    for (int i = 1; i <= size / 2; i++)
    {
        sinking(i);
    }

    return a[size + 1];
}

int insert(et val)
{
    a[++size] = val;
    _floating(size / 2);
}

void _floating(int index)
{

    if (index < 1)
        return;

    int rc_index = index * 2 + 1 <= size ? index * 2 + 1 : -1;
    int sele_index = -1;
    if (rc_index != -1)
    {
        if (max(a[index * 2], a[rc_index]))
            sele_index = rc_index;
        else
            sele_index = index * 2;
    }
    else
        sele_index = index * 2;

    if (a[index] > a[sele_index])
        swap(index, sele_index);

    return _floating(--index);
}

其实我们可以发现调整,在一些动态结构中是一种常见的在增加或者减少数据好继续恢复结构特性的手段。