堆、堆排序和优先队列的那些事

文章图片来源于 GitHub,网速不佳的朋友,请看《堆、堆排序和优先队列的那些事》 或者 来我的技术小站 godbmw.com

堆、堆排序和优先队列

1. 什么是堆?

堆是一种数据结构,它是一颗完全二叉树。

堆分为最大堆和最小堆:

  1. 最大堆:任意节点的值不大于其父亲节点的值。
  2. 最小堆:任意节点的值不小于其父亲节点的值。

如下图所示,就是个最大堆:

image

注:本文中的代码实现是最大堆,最小堆的实现相似,不再冗赘。

2. 堆有什么用途?

堆最常用于优先队列以及相关动态问题。

优先队列指的是元素入队和出队的顺序与时间无关,既不是先进先出,也不是先进后出,而是根据元素的重要性来决定的。

例如,操作系统的任务执行是优先队列。一些情况下,会有新的任务进入,并且之前任务的重要性也会改变或者之前的任务被完成出队。而这个出队、入队的过程利用堆结构,时间复杂度是O(log2_n)

image

3. 实现堆结构

3.1 元素存储

堆中的元素存储,一般是借用一个数组:这个数组是从 1 开始计算的。更方便子节点和父节点的表示。

image

3.2 入堆

入堆即向堆中添加新的元素,然后将元素移动到合适的位置,以保证堆的性质。

在入堆的时候,需要shift_up操作,如下图所示:

image

插入 52 元素后,不停将元素和上方父亲元素比较,如果大于,则交换元素;直到达到堆顶或者小于等于父亲元素。

3.3 出堆

出堆只能弹出堆顶元素(最大堆就是最大的元素),调整元素为止保证堆的性质。

在入堆的时候,需要shift_down操作,如下图所示:

image

已经取出了堆顶元素,然后将位于最后一个位置的元素放入堆顶(图中是16被放入堆顶)。

重新调整元素位置。此时元素应该和子节点比较,如果大于等于子节点或者没有子节点,停止比较;否则,选择子节点中最大的元素,进行交换,执行此步,直到结束。

3.4 实现优化

在优化的时候,有两个部分需要做:

  1. swap操作应该被替换为:单次赋值,减少赋值次数
  2. 入堆操作:空间不够的时候,应该开辟 2 倍空间,防止数组溢出

3.5 代码实现

// MaxHeap.h
// Created by godbmw.com on 2018/9/19.
//

#ifndef MAXHEAP_MAXHEAP_H
#define MAXHEAP_MAXHEAP_H

#include <iostream>
#include <algorithm>
#include <cassert>
#include <typeinfo>

using namespace std;

template <typename Item>
class MaxHeap {
private:
    Item* data; // 堆数据存放
    int count; // 堆目前所含数据量大小
    int capacity; // 堆容量大小

    void shift_up(int k) {
        Item new_item = this->data[k]; // 保存新插入的值
//      如果新插入的值比父节点的值小, 则父节点的值下移, 依次类推, 直到到达根节点或者满足最大堆定义
        while( k > 1 && this->data[k/2] < new_item ) {
            this->data[k] = this->data[k/2];
            k /= 2;
        }
        this->data[k] = new_item; // k就是 新插入元素 应该在堆中的位置
    }

    void shift_down(int k) {
        Item root = this->data[1];
//        在完全二叉树中判断是否有左孩子即可
        while(2*k <= this->count) {
            int j = k + k;
//            如果有右子节点,并且右节点 > 左边点
            if( j + 1 <= this->count && this->data[j + 1] > this->data[j]) {
                j += 1;
            }
//            root找到了堆中正确位置 k 满足堆性质, 跳出循环
            if(root >= this->data[j]) {
                break;
            }
            this->data[k] = this->data[j];
            k = j;
        }
        this->data[k] = root;
    }
public:
    MaxHeap(int capacity) {
        this->data = new Item[capacity + 1]; // 堆中数据从索引为1的位置开始存储
        this->count = 0;
        this->capacity = capacity;
    }
//    将数组构造成堆:heapify
    MaxHeap(Item arr[], int n) {
        this->data = new Item[n+1];
        this->capacity = n;
        this->count = n;
        for(int i = 0; i < n; i++) {
            this->data[i + 1] = arr[i];
        }
        for(int i = n/2; i >= 1; i--) {
            this->shift_down(i);
        }
    }
    ~MaxHeap(){
        delete[] this->data;
    }
//    返回堆中元素个数
    int size() {
        return this->count;
    }
//    返回布尔值:堆中是否为空
    bool is_empty() {
        return this->count == 0;
    }

//    向堆中插入元素
    void insert(Item item) {
        // 堆空间已满, 开辟新的堆空间.
        // 按照惯例,容量扩大到原来的2倍
        if(this->count >= this->capacity) {
            this->capacity = this->capacity + this->capacity; // 容量变成2倍
            Item* more_data = new Item[this->capacity + 1]; // data[0] 不存放任何元素
            copy(this->data, this->data + this->count + 1, more_data); // 将原先 data 中的有效数据拷贝到 more_data 中
            delete[] this->data;
            this->data = more_data;
        }
        this->data[this->count + 1] = item; // 插入堆尾部
        this->shift_up(this->count + 1); // 执行 shift_up,将新插入的元素移动到应该在的位置
        this->count ++;
    }

//    取出最大值
    Item extract_max() {
        assert(this->count > 0);
        Item ret = this->data[1]; // 取出根节点
        swap(this->data[1], this->data[this->count]); // 将根节点元素和最后元素交换
        this->count --; // 删除最后一个元素
        this->shift_down(1); // shift_down 将元素放到应该在的位置
        return ret;
    }
};
#endif //MAXHEAP_MAXHEAP_H

4. 堆排序

根据实现的MaxHeap类,实现堆排序很简单:将元素逐步insert进入堆,然后再extract_max逐个取出即可。当然,这个建堆的平均时间复杂度是O(n*log2_n)代码如下:

template <typename T>
void heap_sort1(T arr[], int n) {
    MaxHeap<T> max_heap = MaxHeap<T>(n);
    for(int i = 0; i < n; i++) {
        max_heap.insert(arr[i]);
    }
    for(int i = n -1; i >= 0; i--) {
        arr[i] = max_heap.extract_max();
    }
}

仔细观察前面实现的构造函数,构造函数可以传入数组参数。

//    将数组构造成堆:heapify
MaxHeap(Item arr[], int n) {
    this->data = new Item[n+1];
    this->capacity = n;
    this->count = n;
    for(int i = 0; i < n; i++) {
        this->data[i + 1] = arr[i];
    }
    for(int i = n/2; i >= 1; i--) {
        this->shift_down(i);
    }
}

过程叫做heapify,实现思路如下:

  1. 将数组的值逐步复制到this->data
  2. 从第一个非叶子节点开始,执行shift_down
  3. 重复第 2 步,直到堆顶元素

这种建堆方法的时间复杂度是: O(n)。因此, 编写heap_sort2函数:

//  建堆复杂度:O(N)
template <typename T>
void heap_sort2(T arr[], int n) {
    MaxHeap<T> max_heap = MaxHeap<T>(arr, n);
    for(int i = n -1; i >= 0; i--) {
        arr[i] = max_heap.extract_max();
    }
}

上面阐述的两种排序方法,借助实现的最大堆这个类,都需要在类中开辟this->data,空间复杂度为O(n)其实,借助shift_down可以实现原地堆排序,代码如下:

// 这里的 swap 操作并没有优化
// 请对比 MaxHeap 中的 shift_down 函数
template <typename T>
void __shift_down(T arr[], int n, int k) {
    while( 2*k + 1 < n) {
        int j = 2 * k + 1;
        if( j + 1 < n && arr[j + 1] > arr[j]) {
            j += 1;
        }
        if(arr[k] >= arr[j]) {
            break;
        }
        swap(arr[k], arr[j]);
        k = j;
    }
}

//  原地堆排序
template <typename T>
void heap_sort3(T arr[], int n) {
    for(int i = (n -1)/2; i>=0; i--) {
        __shift_down(arr, n, i);
    }
    for(int i = n-1; i > 0; i--) {
        swap(arr[0], arr[i]);
        __shift_down(arr, i, 0);
    }
}

5. 测试

5.1 测试MaxHeap

测试代码如下:

#include <iostream>
#include <ctime>
#include <algorithm>
#include "MaxHeap.h"
#include "SortHelper.h"

#define HEAP_CAPACITY 10
#define MAX_NUM 100

using namespace std;

int main() {

    MaxHeap<int> max_heap = MaxHeap<int>(HEAP_CAPACITY);
    srand(time(NULL));
    for(int i = 0; i < HEAP_CAPACITY + 5; i++) { // 容量超出初始化时的容量。测试:自动
        max_heap.insert(rand() % MAX_NUM);
    }

    while( !max_heap.is_empty() ) {
        cout<< max_heap.extract_max() << " "; // 控制台输出数据是从大到小
    }
    cout<<endl;
    return 0;
}

5.2 测试堆排序

借助前几篇文章的SortHelper.h封装的测试函数:

#include <iostream>
#include <ctime>
#include <algorithm>
#include "MaxHeap.h"
#include "SortHelper.h"

#define HEAP_CAPACITY 10
#define MAX_NUM 100

using namespace std;

int main() {
    int n = 100000;
    int* arr = SortTestHelper::generateRandomArray<int>(n, 0, n);
    int* brr = SortTestHelper::copyArray<int>(arr, n);
    int* crr = SortTestHelper::copyArray<int>(arr, n);
    SortTestHelper::testSort<int>(arr, n, heap_sort1<int>, "first heap_sort");
    SortTestHelper::testSort<int>(brr, n, heap_sort2<int>, "second heap_sort");
    SortTestHelper::testSort<int>(crr, n, heap_sort3<int>, "third heap_sort");
    delete[] arr;
    delete[] brr;
    delete[] crr;
    return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容

  • 1 初级排序算法 排序算法关注的主要是重新排列数组元素,其中每个元素都有一个主键。排序算法是将所有元素主键按某种方...
    深度沉迷学习阅读 1,398评论 0 1
  • 堆是一棵满足一定性质的二叉树,具体的讲堆具有如下性质:父节点的键值总是不大于它的孩子节点的键值(小顶堆), 堆可以...
    9527Roy阅读 618评论 0 0
  • 数据结构与算法--优先队列和堆排序 在某些数据处理的例子中,总数据量太大,无法排序(甚至无法全部装进内存)。例如,...
    sunhaiyu阅读 1,034评论 0 2
  • SEO的概念 SEO:搜索引擎优化(search engine optimization),是指在了解搜索引...
    简小妞儿阅读 525评论 0 4
  • 近年来,无论是企业还是个人都非常强调核心竞争力,那么到底什么才是核心竞争力呢?它最早由普拉哈拉徳和加里哈默尔两位教...
    我在丛中笑阅读 520评论 1 2