堆数据结构是一种数组对象,它可以被视为一科完全二叉树结构。
它的特点是父节点的值大于(小于)两个子节点的值(分别称为最大堆和最小堆)。它常用于管理算法执行过程中的信息,应用场景包括堆排序,优先队列等。
1、根结点若有子树,则子树一定也是堆。
2、根结点一定大于(或小于)子结点。
因为要求堆必须是完全二叉树,所以可以用线性的数据结构,比如数组,来实现堆。
利用数组实现,则对于长为N的堆中的元素从0到N-1排列,有:
i的父结点:Parent(i)=(i+1)/2-1
i的左叶子:Left(i)=(i+1)*2-1
i的右叶子:Right(i)=(i+1)*2
堆(Heap),其实也没什么大不了,简单地说就是一种有序队列而已,普通的队列是先入先出,而二叉堆是:最小先出。
这不是很简单么?如果这个队列是用数组实现的话那用打擂台的方式从头到尾找一遍,把最小的拿出来不就行了?行啊,可是出队的操作是很频繁的,而每次都得打一遍擂台,那就低效了,打擂台的时间复杂度为Ο(n),那如何不用从头到尾fetch一遍就出队呢?二叉堆能比较好地解决这个问题,不过之前先介绍一些概念。
完全树(Complete Tree):从下图中看出,在第n层深度被填满之前,不会开始填第n+1层深度,还有一定是从左往右填满。
这样有什么好处呢?好处就是能方便地把指针省略掉,用一个简单的数组来表示一棵树,如图:
那么下面介绍二叉堆:二叉堆是一种完全二叉树,其任意子树的左右节点(如果有的话)的键值一定比根节点大,上图其实就是一个二叉堆。
你一定发觉了,最小的一个元素就是数组第一个元素,那么二叉堆这种有序队列如何入队呢?看图:
假设要在这个二叉堆里入队一个单元,键值为2,那只需在数组末尾加入这个元素,然后尽可能把这个元素往上挪,直到挪不动,经过了这种复杂度为Ο(logn)的操作,二叉堆还是二叉堆。
那如何出队呢?也不难,看图:
将最小的节点删除后,把最后的一个节点放到第一个节点的位置,然后在调整。
最小堆的类模板实现:类接口部分
///////////////////////// #include <iostream> using namespace std; #define DefaultSize 10 template<class T> class MinHeap //最小堆的类模板实现 { public: MinHeap(int sz=DefaultSize); MinHeap(T arr[], int n); ~MinHeap(){delete []heap;} bool isEmpty()const{return currentSize==0;} bool isFull()const{return currentSize==maxHeapSize;} void makeempty(){currentSize=0;} bool insert(const T& x); //在数组尾部插入,并调整堆 bool removeMin(T& x); //删除堆顶上的最小元素,最后一个元素补到堆顶,然后调整 private: T * heap; //采用数组作为其存储方式。 int currentSize; int maxHeapSize; void siftDown(int start, int m);//从start到m下滑调整为最小堆 void siftUp(int start); //从start到0上滑调整为最小堆 };
具体接口函数实现部分:
template<class T> MinHeap<T>::MinHeap(int sz=DefaultSize) { maxHeapSize=(sz<DefaultSize)?DefaultSize:sz; heap=new T[maxHeapSize]; if(heap==NULL){cerr<<"err\n";exit(-1);} currentSize=0; } template<class T> MinHeap<T>::MinHeap(T arr[], int n) { maxHeapSize=(n<DefaultSize)?DefaultSize:n; heap=new T[maxHeapSize]; if(heap==NULL){cerr<<"err\n";exit(-1);} currentSize=n; int i=0; while(i<n){ //copy heap[i]=arr[i]; ++i; } int currentPos=(currentSize-2)/2; //找到最初调整位置,最后节点的父节点位置,也就是最后的分支节点 while(currentPos>=0){ //自底向上逐步扩大形成堆 siftDown(currentPos,currentSize-1); --currentPos; } } template<class T> void MinHeap<T>::siftDown(int start, int m)//堆的下滑调整算法,从节点start到m为止,从上到下比较,如果子女小于父节点, { //关键码上浮 ,据需向下层比教,将局部子树调整为最小堆 int i=start;int j=2*i+1; //i当前子树的根节点,j左子女 T temp=heap[i]; //保存根节点的值 while(j<=m){ //从上向下调整,检查是否到最后位置 if(j<m && heap[j+1]<heap[j]) //j指向两个子女中最小的 ++j; if(temp<=heap[j]) break; //小则不做调整 else{ heap[i]=heap[j];i=j;j=2*i+1;//小者上移,i,j下降 } } heap[i]=temp; } template<class T> void MinHeap<T>::siftUp(int start)//新节点插入到最小堆的后面,故需从下到上,与父节点比较,调整 { //从start开始到0为止,从下向上 int j=start, i=(j-1)/2; //j表示子节点,i表示j的父节点 int temp=heap[j]; while(j>0){ if(heap[i]<=temp)break; else{ heap[j]=heap[i];j=i;i=(j-1)/2; } heap[j]=temp; } } template<class T> bool MinHeap<T>::insert(const T& x) { if(currentSize==maxHeapSize){ cerr<<"full\n";return false; } heap[currentSize]=x; siftUp(currentSize); ++currentSize; return true; } template<class T> bool MinHeap<T>::removeMin(T& x) { if(!currentSize){ cerr<<"empty\n";return false; } x=heap[0]; heap[0]=heap[currentSize-1]; --currentSize; siftDown(0,currentSize-1); return true; }
验证程序:
为了简单,验证时把类中private屏蔽掉,以便外面访问;
int main(int argc, char* argv[]) { int arr[6]={5,4,3,2,1,0}; int i=0; while(i<sizeof(arr)/sizeof(arr[0])){ cout<<arr[i]<<" "; ++i; } cout<<endl; MinHeap<int> h(arr,sizeof(arr)/sizeof(arr[0])); i=0; while(i<6){ cout<<h.heap[i]<<" "; ++i; } cout<<endl; int x=10; h.insert(x); i=0; while(i<7){ cout<<h.heap[i]<<" "; ++i; } cout<<endl; h.removeMin(x); i=0; while(i<6){ cout<<h.heap[i]<<" "; ++i; } cout<<endl; system("pause"); return 0; }
验证结果:
5 4 3 2 1 0
0 1 3 2 4 5
0 1 3 2 4 5 10
1 2 3 10 4 5
请按任意键继续. . .
版权声明:本文为【借你一秒】原创文章,转载请标明出处。