堆与堆排序

堆这种数据结构应用的场景非常多,最经典的莫过于堆排序了,堆排序是一种原地的,时间复杂度为 O ( n   l o g n ) O(n \ logn) O(n logn)的排序算法,我们学过快速排序,平均情况下,她的时间复杂度为 O ( n   l o g n ) O(n \ logn) O(n logn) ,甚至堆排序比快速排序的时间复杂度还要稳定,但是,在实际的软件开发中,快速排序的性能要比堆排序好,这是为什么呢?这是由堆的二叉树的结构决定的。

如何理解堆

一、堆是一个完全二叉树。

二、堆中每个结点必须大于等于(或者小于等于)其子树中每个结点的值。

首先是第一点,完全二叉树,完全二叉树要求是除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。然后是第二点,第二点我们还可以换个说法,堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。满足上面两个条件的就是堆。

对于每个节点的值都大于等于子树中每个节点值的堆,我们叫做“大顶堆”。对于每个节点的值都小于等于子树中每个节点值的堆,我们叫做“小顶堆”。

堆需要支持那些操作

一、往堆中插入一个元素

往堆中插入一个元素后,我们需要继续满足堆的两个特性。插入不是在任意位置插入的,需要进行调整,让其重新满足堆的特性,这个过程我们起了一个名字,就叫做堆化(heapify)。

堆化实际上有两种,从下往上和从上往下。这里我先讲从下往上的堆化方法。

堆化非常简单,就是顺着节点所在的路径,向上或者向下,对比,然后交换。

我们可以让新插入的节点与父节点对比大小。如果不满足子节点小于等于父节点的大小关系,我们就互换两个节点。一直重复这个过程,直到父子节点之间满足刚说的那种大小关系。这里我们用C++代码来表述一下

#include <iostream>
#include <vector>

using namespace std;

class MAXHeap {
public:
    MAXHeap(int n) : h_size(n + 1), pos(0) {
        heap = new int[h_size];
    };

    MAXHeap() : h_size(10), pos(0) {
        heap = new int[h_size];
    };

    void insert(int i) {
        // 自下向上堆化
        pos++;
        if (pos >= h_size) expand();
        heap[pos] = i;
        int temp = pos;
        while (temp > 1 && heap[temp / 2] < heap[temp]) {
            swap(heap[temp / 2], heap[temp]);
            temp = temp / 2;
        }
    }

    void expand() {
        // 阔容
        h_size = h_size * 2;
        int *temp = new int[h_size];
        for (int i = 0; i < pos; ++i) {
            temp[i] = heap[i];
        }
        delete[] heap;
        heap = temp;
    }

    int size() {
        return pos;
    }

private:
    int h_size;
    int pos;
    int *heap;

    inline void swap(int &a, int &b) {
        int t = a;
        a = b;
        b = t;
    }

};


int main() {
    MAXHeap h1 = MAXHeap();
    for (int i = 0; i < 10; i++) {
        h1.insert(i);
    }
    h1.insert(100);
    h1.insert(200);
    h1.insert(-100);
    
    return 0;
}

这里我们是用的大根堆,即最大元素在堆顶

二、删除堆顶元素

假设我们构造的是大顶堆,堆顶元素就是最大的元素。当我们删除堆顶元素之后,就需要把第二大的元素放到堆顶,那第二大元素肯定会出现在左右子节点中。然后我们再迭代地删除第二大节点,以此类推,直到叶子节点被删除。但是这样有点小问题,就是删除的过程中可能删除的就不是最后一个结点了,而是删除了树中最后一层的某个结点,所以我们其实可以把最后一个元素放在要删除的堆顶元素的位置上,然后一层一层的交换最大元素,同样我们给出代码。

#include <iostream>
#include <vector>

using namespace std;

class MAXHeap {
public:
    MAXHeap(int n) : h_size(n + 1), pos(0) {
        heap = new int[h_size];
    };

    MAXHeap() : h_size(10), pos(0) {
        heap = new int[h_size];
    };

    void insert(int i) {
        // 自下向上堆化
        pos++;
        if (pos >= h_size) expand();
        heap[pos] = i;
        int temp = pos;
        while (temp > 1 && heap[temp / 2] < heap[temp]) {
            swap(heap[temp / 2], heap[temp]);
            temp = temp / 2;
        }
    }

    int popMax() {
        if (pos == 0) return -1;
        int res = heap[1];
        heap[1] = heap[pos--];
        heapify(1);
        return res;
    }

    void expand() {
        // 阔容
        h_size = h_size * 2;
        int *temp = new int[h_size];
        for (int i = 0; i < pos; ++i) {
            temp[i] = heap[i];
        }
        delete[] heap;
        heap = temp;
    }

    int size() {
        return pos;
    }

private:
    int h_size;
    int pos;
    int *heap;

    inline void swap(int &a, int &b) {
        int t = a;
        a = b;
        b = t;
    }

    // 从上到下堆化
    void heapify(int i) {
        while (true) {
            int maxPos = i;
            int left = 2 * i;
            int right = 2 * i + 1;
            if (left <= pos && heap[i] < heap[left]) maxPos = left;
            if (right <= pos && heap[maxPos] < heap[right]) maxPos = right;
            if (maxPos == i) break;
            swap(heap[i], heap[maxPos]);
            i = maxPos;
        }
    }
};


int main() {
    MAXHeap h1 = MAXHeap();
    for (int i = 0; i < 10; i++) {
        h1.insert(i);
    }
    h1.insert(100);
    h1.insert(200);
    h1.insert(-100);

    for (int i = 0; i < 15; i++) {
        cout << h1.popMax() << endl;
    }
    
    return 0;
}

这样相当于我们从上到下的堆化我们的堆。

基于堆实现排序

我们可以把堆排序的过程大致分解成两个大的步骤,建堆排序

一、建堆

我们首先将数组原地建成一个堆。所谓“原地”就是,不借助另一个数组,就在原数组上操作。建堆的过程,有两种思路。

第一种是借助我们前面讲的,在堆中插入一个元素的思路。尽管数组中包含 n 个数据,但是我们可以假设,起初堆中只包含一个数据,就是下标为 1 的数据。然后,我们调用前面讲的插入操作,将下标从 2 到 n 的数据依次插入到堆中。这样我们就将包含 n 个数据的数组,组织成了堆。

第二种实现思路,跟第一种截然相反,也是我这里要详细讲的。第一种建堆思路的处理过程是从前往后处理数组数据,并且每个数据插入堆中时,都是从下往上堆化。而第二种实现思路,是从后往前处理数组,并且每个数据都是从上往下堆化。

#include <iostream>
#include <vector>

using namespace std;

class MAXHeap {
public:
    MAXHeap(int n) : h_size(n + 1), pos(0) {
        heap = new int[h_size];
    };

    MAXHeap(vector<int> nums) : h_size(nums.size()+1), pos(nums.size()) {
        heap = new int[h_size];
        for (int i = 1; i < h_size; i++) {
            heap[i] = nums[i - 1];
        }
        for (int i = h_size / 2; i >= 1; --i) {
            heapify(i);
        }
    }

    MAXHeap() : h_size(10), pos(0) {
        heap = new int[h_size];
    };

    void insert(int i) {
        // 自下向上堆化
        pos++;
        if (pos >= h_size) expand();
        heap[pos] = i;
        int temp = pos;
        while (temp > 1 && heap[temp / 2] < heap[temp]) {
            swap(heap[temp / 2], heap[temp]);
            temp = temp / 2;
        }
    }

    int popMax() {
        if (pos == 0) return -1;
        int res = heap[1];
        heap[1] = heap[pos--];
        heapify(1);
        return res;
    }

    void expand() {
        // 阔容
        h_size = h_size * 2;
        int *temp = new int[h_size];
        for (int i = 0; i < pos; ++i) {
            temp[i] = heap[i];
        }
        delete[] heap;
        heap = temp;
    }

    int size() {
        return pos;
    }

private:
    int h_size;
    int pos;
    int *heap;

    inline void swap(int &a, int &b) {
        int t = a;
        a = b;
        b = t;
    }

    // 从上到下堆化
    void heapify(int i) {
        while (true) {
            int maxPos = i;
            int left = 2 * i;
            int right = 2 * i + 1;
            if (left <= pos && heap[i] < heap[left]) maxPos = left;
            if (right <= pos && heap[maxPos] < heap[right]) maxPos = right;
            if (maxPos == i) break;
            swap(heap[i], heap[maxPos]);
            i = maxPos;
        }
    }
};


int main() {
    MAXHeap h1 = MAXHeap();
    for (int i = 0; i < 10; i++) {
        h1.insert(i);
    }
    h1.insert(100);
    h1.insert(200);
    h1.insert(-100);

    for (int i = 0; i < 15; i++) {
        cout << h1.popMax() << endl;
    }

    vector<int> input({1, 2, 3, 4, 5, 6, 7, 8, 9});
    MAXHeap h2 = MAXHeap(input);
    for (int i = 0; i < 15; i++) {
        cout << h2.popMax() << endl;
    }
    
    return 0;
}

你可能已经发现了,在这段代码中,我们对下标从 n / 2 n/2 n/2 开始到 1 的数据进行堆化,下标是 n / 2 + 1 n/2 +1 n/2+1 n n n 的节点是叶子节点,我们不需要堆化。实际上,对于完全二叉树来说,下标从 2 n + 1 2n+1 2n+1 n n n 的节点都是叶子节点。

二、排序

建堆之后其实就已经完成排序了,建堆结束之后,数组中的数据已经是按照大顶堆的特性来组织的。数组中的第一个元素就是堆顶,也就是最大的元素。我们把它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。

这个过程有点类似上面讲的“删除堆顶元素”的操作,当堆顶元素移除之后,我们把下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置,一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。

甚至我们可以将堆的最上面的元素一个个的抽出来,这样也就实现了堆排序。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LyaJpunov

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值