(二叉)堆是一个数组,是一颗近似完全二叉树,分为大顶堆&小顶堆。表示堆的数组A有两个属性:(1)A.length表示数组元素的个数;(2)A.heap-size表示有多少个堆元素存储在数组A中。更多的关于堆的性质的介绍:算法导论第三版:p85-p89、编程珠玑:p141-p145。
堆的操作主要包括堆插入、堆删除两个,而堆插入设计到SiftUp操作(自底向上调整),堆删除涉及到SiftDown操作(自顶向下调整,大顶堆时对应算法导论上的MAX-HEAPIFY操作)。
下面是大顶堆和小顶堆的基本操作的C++的实现:
//heap.h #pragma once template<class T> class Heap { public: Heap(const int nmax = 100) : max_size(nmax) { arr = new T[max_size]; size = 0; } virtual ~Heap() {delete []arr;} void Insert(const T &e); void Delete(const T &e); int Search(const T &e) const; void Print() const; protected: T *arr; const int max_size; //max array elements int size; //size of heap virtual void SiftDown(int i) = 0; //adjust from up to down virtual void SiftUp(int i) = 0; //adjust from down to up }; template<class T> void Heap<T>::Insert(const T &e) { if (size == max_size) return; arr[size++] = e; //将新插入的元素添加在最后位置并增加size SiftUp(size - 1); //数组下标从0开始 } template<class T> void Heap<T>::Delete(const T &e) { int pos = Search(e); if (pos == -1) return; arr[pos] = arr[--size]; //用最后的元素填充pos位置的元素,并减少size SiftDown(pos); } template<class T> int Heap<T>::Search(const T &e)const { for (int i = 0; i < size; i++) if (arr[i] == e) return i; return -1; } template<class T> void Heap<T>::Print()const { for (int i = 0; i < size; i++) cout << arr[i] << " "; cout << endl; }
最大堆:maxHeap.h
//maxHeap.h #pragma once #include "heap.h" #include <iostream> using std::cout; using std::endl; template<class T> class MaxHeap : public Heap<T> { public: MaxHeap(const int nmax = 100) : Heap(nmax) {} ~MaxHeap() {} private: virtual void SiftDown(int i); //adjust from up to down virtual void SiftUp(int i); //adjust from down to up }; template<class T> void MaxHeap<T>::SiftDown(int i) { //已知arr[i,...,size)除arr[i]之外均满足大顶堆的定义,本函数向下调整arr[i] //使得在具有size个结点的堆中,以i为下标的根节点的子树重新遵循最大堆的性质 //size为节点总数,从0开始计算,i节点的子节点为 2*i+1, 2*i+2 int tmp, j; j = 2 * i + 1;//结点i的左孩子下标 tmp = arr[i]; while (j < size) { if (j + 1 < size && arr[j + 1] > arr[j])//找左右孩子中最大的 j++; if (arr[j] <= tmp) // 父结点tmp=arr[i]比孩子结点arr[j]大 break; arr[i] = arr[j];//把较大的子结点往上移动,替换它的父结点 i = j; j = 2 * i + 1; } arr[i] = tmp; } template<class T> void MaxHeap<T>::SiftUp(int i) { //新加入结点i,原本arr[0, size)满足堆性质,向上调整使得[0, size+1)满足最大堆性质 int j, tmp; j = (i - 1) / 2; //i的父结点 tmp = arr[i]; while (j >= 0 && i != 0) { if (arr[j] >= tmp) //父节点arr[j]比子结点tmp = arr[i]大 break; arr[i] = arr[j];//把较小的父结点往下移动,替换它的子结点 i = j; j = (i - 1) / 2; } arr[i] = tmp; }
最小堆:minHeap.h
//minHeap.h #pragma once #include "heap.h" #include <iostream> using std::cout; using std::endl; template<class T> class MinHeap : public Heap<T> { public: MinHeap(const int max_size = 100) : Heap(max_size) {} ~MinHeap(){} private: virtual void SiftDown(int i); virtual void SiftUp(int i); }; template<class T> void MinHeap<T>::SiftDown(int i) { //已知arr[i,...,size)除arr[i]之外均满足小顶堆的定义,本函数向下调整arr[i] //使得在具有size个结点的堆中,以i为下标的根节点的子树重新遵循最大堆的性质 //size为节点总数,从0开始计算,i节点的子节点为 2*i+1, 2*i+2 int j, tmp; j = 2 * i + 1; //左孩子 tmp = arr[i]; while (j < size ) { if (j + 1 < size && arr[j + 1] < arr[j]) //找左右孩子中最小的 j++; if (arr[j] >= tmp) // 父结点tmp=arr[i]比孩子结点arr[j]小 break; arr[i] = arr[j]; //把较小的子结点往上移动,替换它的父结点 i = j; j = 2 * i + 1; } arr[i] = tmp; } template<class T> void MinHeap<T>::SiftUp(int i) { int j, tmp; j = (i - 1) / 2; //父结点 tmp = arr[i]; while (j >= 0 && i != 0) { if (arr[j] <= tmp) //父节点arr[j]比子结点tmp = arr[i]小 break; arr[i] = arr[j]; //把较大的父结点往下移动,替换它的子结点 i = j; j = (i - 1) / 2; } arr[i] = tmp; }
测试代码:
#include "maxHeap.h" #include "minHeap.h" int main() { int a[12] = {15, 13, 9, 5, 12, 8, 7, 4, 0, 6, 2, 1}; MaxHeap<int> maxheap(20); MinHeap<int> minheap(20); for (int i = 0; i < 12; i++) maxheap.Insert(a[i]); //向上调整 maxheap.Print(); maxheap.Delete(4); //向下调整 maxheap.Print(); for (int i = 0; i < 12; i++) minheap.Insert(a[i]); //向上调整 minheap.Print(); minheap.Delete(4); //向下调整 minheap.Print(); getchar(); return 0; }
这里用的是插入的方法建堆:最坏情况下,建立一个包含n个元素的堆的时间复杂度是O(nlgn),大theta。
下一篇文中的堆排序,可以在线性时间O(n)内,把一个无需数组构造成一个最大堆。
一般地,还要k叉堆:算法导论第三版p93。
利用堆可以实现:堆排序、优先队列。在Dijkstra算法中:
EXTRACT-MIN(Q) 操作,就可以先建立最小堆,然后从最小队列中,每次抽取最小结点(参考 最短路径之Dijkstra算法 一文的参考资料链接)。
此外,堆还可以用于:海量数据中选择前k个最大(最小)的数。
思想:在一个很大的无序数组里面选择前k个最大(最小)的数据,最直观的做法是把数组里面的数据全部排好序,然后输出前面最大(最小)的k个数据。但是,排序最好需要O(nlogn)的时间,而且我们不需要前k个最大(最小)的元素是有序的。这个时候我们可以建立k个元素的最小堆(得出前k个最大值)或者最大堆(得到前k个最小值),我们只需要遍历一遍数组,在把元素插入到堆中去只需要logk的时间,这个速度是很乐观的。利用堆得出前k个最大(最小)元素特别适合海量数据的处理。
参考资料:算法导论第三版:p85-p89、编程珠玑:p141-p145
数据结构之(二叉)堆