【算法导论】手动实现堆

来源:互联网 发布:人民警察证制作软件 编辑:程序博客网 时间:2024/05/19 10:12

A. 堆的简介

堆是一个很好用的数据结构,它既没有搜索二叉树那样严格(要求左子结点必须小于父结点,而右子结点必须不小于左子结点),同时也可以毫不费力气地维护平衡的高度(实现过平衡二叉树的就知道,挺麻烦的)。

堆可以按照它的性质分为最大堆(任意父结点不小于左右子结点)和最小堆(任意父结点不大于左右子结点),两种原理都是一样的,下面就最大堆来讨论。

首先是堆的底层数据的结构,只用到了一个普通的数组!!!没错,没有眼花,就是这样而已,不需要任何指针,简单直观的下标访问——假设父结点的下标为i,那么它的子结点下标分别是:2i(左),2i + 1(右);那么反过来,已知某节点的下标为x,那么它的父结点的下标就是x/2,不管它是左还是右。注意到这里的运算都是跟2的乘法或除法,所以可以用位移运算来加快速度(取下标的操作是很频繁的,积少成多),比如:

size_t Heap::Parent(size_t i) {    return i >> 1;}size_t Heap::Left(size_t i) {    return i << 1;}size_t Heap::Right(size_t i) {    return (i << 1) + 1;}

B. 实现堆的类定义

我把堆当做一个类来实现了,具体定义如下:

#include <vector>using namespace std;// EType表示element type,即堆的元素类型typedef int EType;class Heap{private:    vector<EType> data;    size_t HEAP_SIZE;    size_t Parent(size_t i);    size_t Left(size_t i);    size_t Right(size_t i);public:    // 构造函数    Heap(const vector<EType>& v);    // 维护最大堆的性质,假设i处的左右子结点都是一个最大堆了    // 而 i 可能得往下移动才能保持整个堆是最大堆    void MAX_HEAPIFY(size_t i);    // 建堆的操作,算法在具体实现时有介绍    void BUILD_MAX_HEAP();    // 堆排序    vector<EType> HEAPSORT();    // 展示堆的这个数组的数据    void display();};

C. 维护堆的性质

首先最重要的是MAX_HEAPIFY这个函数,它假设i结点的下层已经是完好的最大堆了,现在考虑将它放到合适的位置来维护最大堆的性质。

void Heap::MAX_HEAPIFY(size_t i) {    size_t left_index = Left(i), right_index = Right(i);    size_t largest = i;    if (left_index <= HEAP_SIZE && data[left_index] > data[largest])        largest = left_index;    if (right_index <= HEAP_SIZE && data[right_index] > data[largest])        largest = right_index;    if (largest != i) {        swap(data[i], data[largest]);        MAX_HEAPIFY(largest);    }}

从代码中可以看出,其实就是在它与它的左右子结点中找一个最大的值来充当新的父结点,而小的值就递归往下走,直到它到达它合适的位置,递归自然会终止(因为有largest != i的判断成立才会递归下去)。

D. 建堆

其实是很简单的事,首先考虑从[HeapSize/2 + 1, HeapSize/2 + 2, …, HeapSize]这个区间上,所有的结点都是平凡的最大堆(因为它们都没有子结点了,为什么?你算一下如果有子结点,它们的下标是多少)

所以从[HeapSize/2, 1],可以“从下往上”来维护最大堆的性质,以致于最后整个堆都是最大堆了!

void Heap::BUILD_MAX_HEAP() {    for (size_t i = HEAP_SIZE/2; i >= 1; --i)        MAX_HEAPIFY(i);}

E. 堆排序

其实关键是明白一个点就够了——对于最大堆,堆里的最大元素就是根结点!!!!
那么我们每次先把根节点拿出来,放一个最后面的元素(即数组里最后面的那个元素)到根节点原来的地方,这个时候堆的性质很可能是会丢失的,但是注意到根节点的左右子结点仍然保持着堆的性质,所以只需要调用一次那个维护堆的性质的函数就好了!!!
这样每次都能拿到堆里最大的元素,这就是有序了,不过注意,这个有序并不一定能够保证是稳定的(稳定性参考https://zh.wikipedia.org/wiki/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95#.E7.A9.A9.E5.AE.9A.E6.80.A7),需要额外地维护!

vector<EType> Heap::HEAPSORT() {    for (size_t i = data.size()-1; i >= 1; --i) {        swap(data[1], data[i]);        --HEAP_SIZE;        MAX_HEAPIFY(1);    }    return vector<EType>(data.begin()+1, data.end());}

F. 代码与参考

本次的实现参考的是《算法导论》中的介绍和伪代码。
所有的代码可以在我的github上面找到:
https://github.com/cooljacket/TheMethodOfProgramming/tree/master/2

1 0
原创粉丝点击