堆是一个完全二叉树,因为完全二叉树有特殊的规律,其可以从上到下从左至右逐层排列到一个数组中,所以可以用线性空间表示堆而不必使用链表。如下图的堆可以用下面的数组表示:
完全二叉树(堆)
完全二叉树的数组表示
对一个完全二叉树,具有几个基本特征。完全二叉树中的任意次序为i的结点,其左孩子在2i位置上,右孩子在2i+1位置上,因此不需要链表遍历该树很简单,在大部分计算机上运行的非常的快。在这种实现方式中,堆的容量需要实现估计,但是在需要的情况下我们可以重新调整。如上图所示,堆的总容量是13,0位置保留他用。因此,堆结构可用一个线性空间和一个代表堆大小的数据成员组成,用C++描述如下:
template<typename T>
class Heap{
public:
explicit Heap(size_t capacity = 100); //构造函数
explicit Heap(const std::vector<T> &); //读入数据系列构造堆
bool isEmpty() const; //判断堆空
T &top() const; //返回堆的顶点
void push(const T &); //插入元素
void pop(); //删除元素
void pop(T &); //删除元素并将其保存起来
void clear(); //清空堆
private:
int currSize; //堆的实际大小
std::vector<T> heapArr; //堆的线性空间
void buildHeap(); //构造堆
void filterDown(int /* begin position*/); //元素上滤
void filterUp(int /* begin position*/); //元素下滤
};
堆根据性质又可以分为大根堆(大顶堆、最大堆)和小根对(小顶堆、最小堆),前者根结点的键值是所有堆结点键值中最大者,后者根结点的键值是所有堆结点键值中最小者。本人以小根堆为主要实现。
1、push
为将一个元素N插入到堆中,在空闲位置,即最后一个有效元素的下一个位置创建一个空穴,如果N的插入不破坏堆的性质,则插入完成,否则把该空穴的父节点上的元素移动到空穴中,这样一来,空闲就朝着根的方向上了一层。重复该过程,知道该N能放入堆中而不影响堆的性质为止。如下图所示,为了插入14,在下一个空闲位置创建一个空穴,由于14的插入破坏了小根堆的性质,因此将31移入到空穴中,如此重复,直到找出放置14的位置。
尝试插入14,创建一个空穴,破坏堆的性质,空穴上冒
将14上滤到合适位置并插入
push实现代码:
template<typename T>
void push(const T &val){
if(currSize == heapArr.size() - 1)//初始化堆的时候多加了1,此处应该减去1
heapArr.resize(2*currSize);
int pos = ++currSize; //在最后一个有效元素的下一个位置插入空元素heapArr[pos]
//此处pos > 1,因为heapArr的有效存储位置是从1开始的。pos的父节点位置为pos/2
for(;pos > 1 && val < heapArr[pos/2];pos /= 2){
heapArr[pos] = heapArr[pos/2];//如果插入的节点值小于父节点,则将空元素的父节点移到空穴heapArr[pos]
}
heapArr[pos] = val;
}
如果插入的元素是新的最小值,则它将一直被推向顶端,直到某一时刻pos为1,pos为0处不放置元素。
2、pop
pop以类似push的方式处理,找出最小元素是容易,即最小元素就在根位置,但是删除则较为麻烦。当删除了堆根时,此处成了一个空穴,因此堆中的最后一个元素需要移动到堆中的某个位置。如果最后一个元素可以放入空穴中,那么删除完成。我们假设将最后一个元素的值移入到空穴中,如果他比左右孩子都小,那么久万事大吉了,否则,将较小的孩子移动到空穴中,那么空穴就下移了一层。重复该步骤,知道找打适合的位置,这个过程一般称为下滤。如下图所示,删除根13后,31不能放入空穴中,因为这会破坏堆的性质。于是把叫小的儿子14移入空穴,这样空穴就下沉了一层。接着,31依然大于19,继续重复,直到空穴找到合适的位置把31移入空穴中。
在根处建立空穴
空穴下移两层
空穴移到合适位置并将31置入空穴中
pop的实现代码:
template<typename T>
void pop(){
if(isEmpty()) throw std::exception();
heapArr[1] = heapArr[currSize--];//删除第一个元素的值使其成为空穴
filterDown(1);//从第一个节点开始下滤
}
void filterDown(int pos){
T tmp = heapArr[pos];
int targetChild;
for(;2*pos <= currSize;pos = targetChild){
targetChild = 2 * pos;
//如果左孩子大于右孩子,则选择值较小的右孩子作为目标孩子
if(heapArr[2*pos] > heapArr[2*pos+1]){
++targetChild;
}
//如果左右孩子其中一个比其父节小,则将较小的孩子往上移动一层
if(heapArr[targetChild] < tmp)
heapArr[pos] = heapArr[targetChild];
else
break;
}
heapArr[pos] = tmp;
}
值得注意的是,当堆存在偶数个结点是,肯定有一个结点只有一个子结点,因此有必要增加filterDown中第7行的测试。
小根堆的C++实现:
/*
* File: MinHeap.hpp
*/
#ifndef MINHEAP_H
#define MINHEAP_H
#include <vector>
template<typename T>
class MinHeap{
public:
explicit MinHeap(size_t capacity = 100); //构造函数
explicit MinHeap(const std::vector<T> &); //读入数据系列构造堆
bool isEmpty() const; //判断堆空
const T &top() const; //返回堆的顶点
void push(const T &); //插入元素
void pop(); //删除元素
void pop(T &); //删除元素并将其保存起来
void clear(); //清空堆
private:
int currSize; //堆的实际大小
std::vector<T> heapArr; //堆的线性空间
void buildHeap(); //构造对
void filterDown(int /* begin position*/); //元素上溯
};
/*
*小根堆根据完全二叉树实现,完全二叉树的节点按照从上到下从左到右的顺序
*存储在线性的数组heapArr中,数组第一个元素保留,所以初始化容量要+1以容
*纳实际的容量。堆的初始大小currSize为0。
*/
template<typename T>
MinHeap<T>::MinHeap(size_t capacity):heapArr(capacity+1),currSize(0){}
/*
*从一个系列中的数据初始化堆
*/
template<typename T>
MinHeap<T>::MinHeap(const std::vector<T> & items):heapArr(items.size()+1),currSize(items.size()){
int i;
for(i = 1;i <= items.size();++i)
heapArr[i] = items[i];
buildHeap();
}
template<typename T>
bool MinHeap<T>::isEmpty() const{ return currSize == 0;}
template<typename T>
const T &MinHeap<T>::top() const{
if(isEmpty())
throw std::exception();
return heapArr[1];
}
template<typename T>
void MinHeap<T>::push(const T &val){
if(currSize == heapArr.size() - 1)
heapArr.resize(2*currSize);
int pos = ++currSize;
for(;pos > 1 && val < heapArr[pos/2];pos /= 2){
heapArr[pos] = heapArr[pos/2];
}
heapArr[pos] = val;
}
template<typename T>
void MinHeap<T>::pop(){
if(isEmpty()) throw std::exception();
heapArr[1] = heapArr[currSize--];//删除第一个元素的值使其成为空穴
filterDown(1);//从第一个节点开始下滤
}
template<typename T>
void MinHeap<T>::pop(T &minItem){//删除最小的顶点并将其值保存到minItem中
if(isEmpty()) throw std::exception();
minItem = heapArr[1];
heapArr[1] = heapArr[currSize--];
filterDown(1);
}
template<typename T>
void MinHeap<T>::clear(){currSize = 0;}
/*
*依次对每个非叶子节点下滤,构造小顶堆
*/
template<typename T>
void MinHeap<T>::buildHeap(){
int i;
for(i = currSize/2;i >= 1;--i)
filterDown(i);
}
template<typename T>
void MinHeap<T>::filterDown(int pos){
T tmp = heapArr[pos];
int targetChild;
for(;2*pos <= currSize;pos = targetChild){
targetChild = 2 * pos;
if(heapArr[2*pos] > heapArr[2*pos+1]){
++targetChild;
}
if(heapArr[targetChild] < tmp)
heapArr[pos] = heapArr[targetChild];
else
break;
}
heapArr[pos] = tmp;
}
#endif
/*
* File: MinHeap.cpp
*/
#include <iostream>
#include <iterator>
#include <algorithm>
#include <ctime>
#include <cstdlib>
#include "MinHeap.hpp"
using namespace std;
int main(){
srand(time(0));
int i;
MinHeap<int> heap1;
for(i = 0;i < 10;i++)
heap1.push(rand()%100+1);
while(!heap1.isEmpty()){
cout<<heap1.top()<<" ";
heap1.pop();
}
cout<<endl;
return 0;
}
测试结果: