最小/大堆的操作及堆排序

摘自:《啊哈算法》

我们要用1, 2, 5, 12, 7, 17, 25, 19, 36, 99, 22, 28, 46, 92来建立最小堆,并且删除最小的数,并增加一个数23

如何建立这个堆:

//建堆
n = 0;
for (int i = 1; i <= m; i++) {
    n++;
    h[n] = a[n];
    shiftup(n);
}

我们还有更快的方法可以建立一个堆

思路:直接把1, 2, 5, 12, 7, 17, 25, 19, 36, 99, 22, 28, 46, 92放入一个完全二叉树中,在这个二叉树中,我们从最后一个节点开始,依次判断以这个节点为根的子树是否符合最小堆的特性。如果所有的子树都符合最小堆的特性,那么整棵树就是最小堆了。(注意完全二叉树有一个性质,最后一个非叶子节点是第n/2个节点,从1数起而不是从0数起)

算法:把n个元素建立一个堆,首先我们可以将这n个节点以自顶向下,从左到右的方式从1到n编码,可以将这n个节点转换成一棵完全二叉树。紧接着从最后一个非叶节点(节点编号为n/2)开始到根节点(节点编号为1),逐个扫描所有的节点,根据需要将当前节点向下调整,直到以当前结点为根节点的子树符合堆的特性。时间复杂度是O(N)

代码如下:

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

建堆后是完全二叉树,并且所有父节点都比子节点小

接下来,我们将堆顶删除,把新增加的23放在堆顶。

显然加了数后已经不符合最小堆的特性了,我们需要将新增加的数调整到合适的位置。

向下调整,将这个数与它的两个儿子2和5比较,选择较小的一个与它交换

此时我们发现还是不满足最小堆,于是继续将23与它的两个儿子中较小的一个交换。

再交换

向下调整的代码:

void shiftdown(int i) { //传入一个需要向下调整的结点编号i
    int t, flag = 0; //flag用来标记是否需要继续向下调整
    while (i * 2 <= n && flag == 0) {
        //首先判断它和左儿子的关系,并用t记录值较小的节点编号
        if (h[i] > h[i*2]) {
            t = i*2;
        } else {
            t = i;
        }

        //如果它有右儿子,再对右儿子进行讨论
        if (i*2 + 1 <= n) {
            //如果它的右儿子的值更小,更新较小的结点编号
            if (h[t] < h[i*2 + 1])
                t = i * 2 + 1;
        }

        //如果发现最小的编号不是自己,说明子结点中有比父节点更小的
        if (t != i) {
            swap(t, i);
            i = t;
        } else {
            flag = 1;
        }
    }
}

如果只是想新增一个数,而不是删除最小值,只需要将新元素插入到末尾,再根据情况判断新元素是否需要上移,直到满足新的特性位置。

加入我们现在要加入一个3

先将3与它的父节点25比较,发现比父节点小,需要与父节点交换。以此类推

向上调整的代码:

//新增加一个元素
void shiftup(int i) { //传入一个需要向上调整的结点编号i
    int flag = 0; //用来标记是否需要继续向上调整
    if (i == 1) return ; //如果是堆顶,就返回,不需要再调整了
    //不在堆顶,并且当前结点i的值比父节点小的时候就继续向上调整
    while (i != 1 && flag == 0) {
        //判断是否比父节点的小
        if (h[i] < h[i/2]) {
            swap(i, i/2);
        } else{
            flag = 1;
        }
        i = i/2;
    }
}

--------------------------------------------------------------------------------------------------

堆排序:

时间复杂度是O(NlogN),假如要从小到大排序,那么只需要建立最小堆,然后每次删除顶部元素并将顶部元素输出或者放入到一个新的数组中,直到堆为空为止。

int deleteMax() {
    int t;
    t = h[1];
    h[1] = h[n];
    n--; //堆的元素减少1
    shiftdown(1); //向下调整
    return t;
}

一种更好的方法是,从小到大排序的时候不是建立最小堆而是建立最大堆,最大堆建立好后,最大的元素在h[1],因为我们的需求是从小到大排序,希望最大的放在最后,因此我们将h[1]和h[n]交换,此时h[n]就是数组中的最大的元素。交换后将h[1]向下调整以保持堆的特性。

void heapsort() {
    while (n > 1) {
        swap(1, n);
        n--;
        shiftdown(1);
    }
}

-------------------------------------------------------------------------------------------------

堆的其他应用:

1.求一个数列中的第K大的数

建立一个大小为K的最小堆,堆顶就是第K大的数

例如,假设有10个数,要求求第3大的数,第一步选取任意的3个数,比如说是前3个,将这3个数建成最小堆,然后从第4个数开始,与堆顶
的数比较,如果比堆顶的数要小,那么这个数就不要,如果比堆顶的数大,则舍弃当前的堆顶而将这个数作为新的堆顶,并再去维护堆

posted @ 2016-03-10 10:10  向日葵的祈愿  阅读(37001)  评论(0编辑  收藏  举报