一步一步学习数据结构和算法 (三) 堆和堆排序

堆和堆排序

堆排序

堆和优先队列

  • 普通队列: 先进先出; 后进后出.
  • 优先队列: 出队顺序和入队顺序无关, 和优先级相关.

二叉堆

  • 任何一个节点都不大于他的父节点
image
  • 二叉堆是一棵完全二叉树
image

用数组存储二叉堆

因为是一棵完全二叉树, 所以可以使用数组存储.

image

依照层序自上而下存储.

image
parent(i) = i/2
left child (i) = 2 * i
right child (2) = 2 * i + 1

堆的 C++ 实现

基本框架

这里使用 C++ 的模板类来实现一个大根堆, 基本框架如下.

template<typename Item>
class MaxHeap {
private:
    Item *data;
    int count;

public:
    MaxHeap(int capacity) {
        data = new Item[capacity + 1];
        count = 0;
    }

    ~MaxHeap() {
        delete[] data;
    }

    int size() {
        return count;
    }

    bool isEmpty() {
        return count == 0;
    }
};
  • 使用模板来实现
  • 使用 Item 类型的动态数组 data 来存储数据.
  • 构造函数中, 传入最大容量 capacity, 我们构建的 data 数组大小为 capacity+1 (因为数组中位置 0 不存数据).
  • 相应的析构函数中释放内存空间.
  • 定义 size() 函数表示当前存储的数据大小.
  • isEmpty() 函数表示当前堆是否为空.

向堆中添加元素

image

向堆中添加元素, 需要使得新元素放在堆中的合适位置. 实现思路也非常简单:

  • 首先将新元素放在堆的末尾.
  • 比较新元素与其父节点, 如果大于父节点, 则与父节点交换.
  • 直到该元素满足要求.

具体实现也非常简单

void insert(Item item) {
  assert(count + 1 <= capacity);
  data[++count] = item;
  shiftUp(count);
}

void shiftUp(int k) {
  // 注意 k > 1, k 最小为 2
    while(k > 1 && arr[k/2] < arr[k]) {
    swap(arr[k/2], arr[k]);
    k /= 2;
  }
}

删除堆中的一个元素 (出队操作)

对于大根堆, 出堆只能移除根元素 (最大的元素). 在移除一个元素后, 还需要保持堆的性质不变, 具体的操作包括以下几个步骤:

  • 出队根元素
  • 将最后一个元素移动到根元素位置
  • ShiftDown 操作

ShiftDown 操作就是将根节点的元素逐渐下移到合适位置的操作, 具体来说就是:

  • k 初始为 1, 指向根节点.
  • 检测该节点有没有孩子节点, 如果没有孩子节点, 则完成 ShiftDown 操作
  • 检测孩子时, 因为是完全二叉树, 所以只需要先看是否有左孩子 (2 * k).
  • 和孩子中较大元素进行比较, 如果小于较大孩子, 则与之交换, 否则完成 ShiftDown 操作.

具体代码实现如下:

Item extractMax() {
    Item ret = data[1];
    data[1] = data[count -1];
    shiftDown(1);
    return Item;
}

void shiftDown(int k) {
    while (2 * k <= count) {
        int j = 2 * k;
        if (j + 1 <= count && data[j + 1] > data[j]) {
            j++;
        }
        if (data[j] <= data[k]) {
            break;
        }
        swap(data[k], data[j]);
        k = j;
    }
    
}

堆排序

有了堆这一数据结构, 我们可以轻易实现一个堆排序算法. 最简单的实现就是将数据依次放入一个堆中, 再依次取出, 就得到了一个有序数据.

template<typename T>
void heapSort1(T arr[], int n) {
    MaxHeap<T> maxHeap = MaxHeap<T>(n);
    for (int i = 0; i < n; i++) {
        maxHeap.insert(arr[i]);
    }

    for (int i = n - 1; i >= 0; i--) {
        arr[i] = maxHeap.extractMax();
    }
}

Heapify

上面我们额外开辟了一个空间来进行堆排序. 下面我们介绍一个操作, 将一个不满足堆的数组变成堆.

这个过程实现也很简单, 使用从小到大的思路即可.

一个单一的元素肯定满足堆的性质, 我们首先将所有叶子节点看做一个堆, 然后依次, 从后向前, 一个一个的将元素加入到子堆中, 加入后, 执行 shiftDown 操作, 就可以使得新生成的子堆也满足堆的性质. 直到最后一个根节点也加入进来, 则数组满足堆的性质. 具体实现起来也非常简单.

MaxHeap(Item arr[], int n) {
    data = new Item[n + 1];
    capacity = n;
    for (int i = 0; i < n; i++) {
        data[i + 1] = arr[i];
    }
    for (int i = count / 2; i >= 1; i--) {
        shiftDown(i);
    }
}

使用 Heapify 的操作, 完成的堆排序算法

template<typename T>
void heapSort2(T arr[], int n) {
    MaxHeap<T> maxHeap = MaxHeap<T>(arr, nullptr);
    for (int i = n - 1; i >= 0; i--) {
        arr[i] = maxHeap.extractMax();
    }
}

原地堆排序

  • 一个堆
  • 将最大元素与末尾元素交换
  • shiftdown 操作
template<typename T>
void __shiftDown(T arr[], int n, int k) {
    while (2 * k + 1 < n) {
        int j = 2 * k + 1;
        if (j + 1 < n && arr[j] < arr[j + 1]) {
            j += 1;
        }
        if (arr[k] >= arr[j]) {
            break;
        }
        swap(arr[k], arr[j]);
        k = j;
    }
}

template<typename T>
void heapSort(T arr[], int n) {
    for (int i = (n - 1) / 2; i >= 0; i--) {
        __shiftDown(arr, n, i);
    }

    for (int i = n - 1; i > 0; i--) {
        swap(arr[0], arr[i]);
        __shiftDown(arr, i, 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
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,713评论 0 13
  • 一 什么是优先队列? 1⃣️ 优先队列其实就是队列的一种,不过优先队列是区别于普通队列的;普通队列是一种先进先出,...
    十丈_红尘阅读 571评论 0 1
  • 数据结构与算法--优先队列和堆排序 在某些数据处理的例子中,总数据量太大,无法排序(甚至无法全部装进内存)。例如,...
    sunhaiyu阅读 1,034评论 0 2
  • 堆就是用数组实现的二叉树,所以它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。...
    唐先僧阅读 248,805评论 21 252