堆与优先队列详解
2012-05-01 17:14
351 查看
说明: 1 . 根据看STL源码剖析与张铭的数据结构而总结。
2. 本人比较懒,图有的是自己画的有的是来自上面资源,算法实现直接copy的张铭数据结构中代码
堆是一个满足如下性质的完全二叉树:树中任意一个非叶结点的关键码均不大于(或者不小于)其左右孩子(若存在)结点的关键码。堆和二叉搜索树的区别是堆不像二叉搜索树一样实现了关键码的完全排序(中序周游二叉搜索树结点并打印出来讲得到右小到大的排序)。对于堆而言只有当结点之间是父子关系时,才可以确定这两个关键码之间的大小关系。
在讲解堆的操作之前,我们先来复习下完全二叉树的几个性质:
对于具有n个结点的完全二叉树,结点按层次由左到右编号,则对于任一结点i( i从0开始计数)有:
(1) 如果i =0 ,则结点i是二叉树的根结点;若i> 0,则其父节点编号为[(i -1 )/2];
(2) 当2i + 1 <= n-1 时结点i的左子结点为2i+1 ,否则结点i没有左子结点
(3) 当 2i + 2 <= n-1时,节点i的右子结点是2i+2,否则节点i没有右子结点
(4) 当i为偶数且0< i < n,结点i的左兄弟节点为i-1 ,否则I 没有左兄弟
(5) 当i为奇数且i + 1 < n 时,节点i的右兄弟节点为i + 1 否则结点i没有右兄弟
由于最小堆和最大堆的操作类似,我们所讲的例子以最大堆为例。
由于堆满足完全二叉树的性质,所以我们用顺序结构存储(C语言中的数组或者C++中的vector)。
下面我们介绍下实现堆的过程中涉及的算法:(使用C++中的vector作为顺序存储结构)
插入算法:
首先将新添加元素添加到vector从左到右第一个空白位置,即vector的end() 处,然后执行上溯操作,即将新节点与父节点做比较,如果键值大于父节点,则交换父子位置,如此一直上溯,直到不需要对换或者到根结点为止。举例如下图(插入50):
删除算法
首先找到被删除元素的位置,由于删除某个位置元素后出现了空位置,所以要把最末端的结点填入这个位置。然后与此位置关键码与父节点关键码进行比较,如果大于父节点关键码则执行上溯操作,否则执行下溯操作。下溯操作指即将新节点与子节点做比较,并与较大的子结点交换位置,如此一直下溯,直到结点的键值大于两个左右子结点或者下溯到叶子结点为止。注意,在整个过程中,上溯操作和下溯操作是二选一,即if else 不会既执行上溯操作又执行下溯操作。下面以图的形式演示下:
删除44 (上溯):
删除76(下溯):
删除最大元素:
删除最大元素其实就是删除的特殊情况,只是由于删除的是根结点,不存在父节点所以我删
除之后直接执行下溯程序调整堆即可,而不会出现执行上溯程序的过程,举例
建堆
在含有n个节点的完全二叉树中,并不满足堆的性质,但是叶子结点构成的子树已经堆了
所以我们在建堆的过程中从第一个非叶子结点(i= [n/2 - 1 ])开始,从右往左依次进行下溯调
整,对这一层调整完之后进行对上一层进行调整,直到达到树根时,树已经满足堆结构了。
堆排序:
每次获取并删除最大元素(pop_head)便可以获得heap之后的最大元素,如果持续对整个
Heap做pop_heap动作,每次将操作范围从后向前缩减一个元素(我们将pop_heap的结果
放在底部容器的最尾端),当整个程序执行完毕之后,我们便有了一个递增序列,注意:排
序之后数组中元素就不符合堆性质了。实际操作
算法时间复杂度分析:
建立堆的时间代价为O(n) 由于堆有log(n) 层深,所以插入结点删除普通元素和删除最大元素的平均时间代价和最差时间代价都为O(logn).堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。
下面给出各种操作的源代码实现:
2. 本人比较懒,图有的是自己画的有的是来自上面资源,算法实现直接copy的张铭数据结构中代码
堆是一个满足如下性质的完全二叉树:树中任意一个非叶结点的关键码均不大于(或者不小于)其左右孩子(若存在)结点的关键码。堆和二叉搜索树的区别是堆不像二叉搜索树一样实现了关键码的完全排序(中序周游二叉搜索树结点并打印出来讲得到右小到大的排序)。对于堆而言只有当结点之间是父子关系时,才可以确定这两个关键码之间的大小关系。
在讲解堆的操作之前,我们先来复习下完全二叉树的几个性质:
对于具有n个结点的完全二叉树,结点按层次由左到右编号,则对于任一结点i( i从0开始计数)有:
(1) 如果i =0 ,则结点i是二叉树的根结点;若i> 0,则其父节点编号为[(i -1 )/2];
(2) 当2i + 1 <= n-1 时结点i的左子结点为2i+1 ,否则结点i没有左子结点
(3) 当 2i + 2 <= n-1时,节点i的右子结点是2i+2,否则节点i没有右子结点
(4) 当i为偶数且0< i < n,结点i的左兄弟节点为i-1 ,否则I 没有左兄弟
(5) 当i为奇数且i + 1 < n 时,节点i的右兄弟节点为i + 1 否则结点i没有右兄弟
由于最小堆和最大堆的操作类似,我们所讲的例子以最大堆为例。
由于堆满足完全二叉树的性质,所以我们用顺序结构存储(C语言中的数组或者C++中的vector)。
下面我们介绍下实现堆的过程中涉及的算法:(使用C++中的vector作为顺序存储结构)
插入算法:
首先将新添加元素添加到vector从左到右第一个空白位置,即vector的end() 处,然后执行上溯操作,即将新节点与父节点做比较,如果键值大于父节点,则交换父子位置,如此一直上溯,直到不需要对换或者到根结点为止。举例如下图(插入50):
删除算法
首先找到被删除元素的位置,由于删除某个位置元素后出现了空位置,所以要把最末端的结点填入这个位置。然后与此位置关键码与父节点关键码进行比较,如果大于父节点关键码则执行上溯操作,否则执行下溯操作。下溯操作指即将新节点与子节点做比较,并与较大的子结点交换位置,如此一直下溯,直到结点的键值大于两个左右子结点或者下溯到叶子结点为止。注意,在整个过程中,上溯操作和下溯操作是二选一,即if else 不会既执行上溯操作又执行下溯操作。下面以图的形式演示下:
删除44 (上溯):
删除76(下溯):
删除最大元素:
删除最大元素其实就是删除的特殊情况,只是由于删除的是根结点,不存在父节点所以我删
除之后直接执行下溯程序调整堆即可,而不会出现执行上溯程序的过程,举例
建堆
在含有n个节点的完全二叉树中,并不满足堆的性质,但是叶子结点构成的子树已经堆了
所以我们在建堆的过程中从第一个非叶子结点(i= [n/2 - 1 ])开始,从右往左依次进行下溯调
整,对这一层调整完之后进行对上一层进行调整,直到达到树根时,树已经满足堆结构了。
堆排序:
每次获取并删除最大元素(pop_head)便可以获得heap之后的最大元素,如果持续对整个
Heap做pop_heap动作,每次将操作范围从后向前缩减一个元素(我们将pop_heap的结果
放在底部容器的最尾端),当整个程序执行完毕之后,我们便有了一个递增序列,注意:排
序之后数组中元素就不符合堆性质了。实际操作
算法时间复杂度分析:
建立堆的时间代价为O(n) 由于堆有log(n) 层深,所以插入结点删除普通元素和删除最大元素的平均时间代价和最差时间代价都为O(logn).堆排序的最坏时间复杂度为O(nlogn)。堆序的平均性能较接近于最坏性能。
下面给出各种操作的源代码实现:
template<class T> class MaxHeap { private: T* heapArray; //存放堆数据的数组 int CurrentSize; //当前堆中元素数目 int MaxSize; //堆所能容纳的最大元素数目 public: MaxHeap(T* array,int num,int max); virtual ~MaxHeap(){}; //析构函数 void BuildHeap(); bool isLeaf(int pos) const; //如果是叶结点,返回TRUE int leftchild(int pos) const; //返回左孩子位置 int rightchild(int pos) const; //返回右孩子位置 int parent(int pos) const; //返回父结点位置 bool Remove(int pos, T& node); //删除给定下标的元素 void SiftDown(int left); //筛选法函数,参数left表示开始处理的数组下标 void SiftUp(int position); //从position向上开始调整,使序列成为堆 BOOL Insert(const T& newNode); //向堆中插入新元素newNode void MoveMax(); //从堆顶移动最大值到尾部 T& RemoveMax(); //从堆顶删除最大值 }; template<class T> MaxHeap<T>::MaxHeap(T* array,int num,int max) { heapArray=array; CurrentSize=num; MaxSize=max; BuildHeap(); } template<class T> void MaxHeap<T>::BuildHeap() { for (int i=CurrentSize/2-1;i>=0; i--) SiftDown(i); } template<class T> bool MaxHeap<T>::isLeaf(int pos) const { return (pos>=CurrentSize/2)&&(pos<CurrentSize); } template<class T> int MaxHeap<T>::leftchild(int pos) const { return 2*pos+1; //返回左孩子位置 } template<class T> int MaxHeap<T>::rightchild(int pos) const { return 2*pos+2; //返回右孩子位置 } template<class T> int MaxHeap<T>::parent(int pos) const //返回父节点位置 { return (pos-1)/2; } template<class T> void MaxHeap<T>::SiftDown(int left) { //准备 int i=left; //标识父结点 int j=2*i+1; //标识关键值较小的子结点 T temp=heapArray[i]; //保存父结点 //过筛 while(j<CurrentSize) { if((j<CurrentSize-1)&&(heapArray[j]<heapArray[j+1])) j++; //j指向右子结点 if(temp<heapArray[j]) { heapArray[i]=heapArray[j]; i=j; j=2*j+1; //向下继续 } else break; } heapArray[i]=temp; } template<class T> void MaxHeap<T>::SiftUp(int position) {//从position向上开始调整,使序列成为堆 int temppos=position; T temp=heapArray[temppos]; int parentpos=0; if(temppos>0) parentpos=parent(temppos); while((temppos>0)&&(heapArray[parentpos]<temp)) { heapArray[temppos]=heapArray[parentpos]; temppos=parentpos; parentpos=parent(temppos); } heapArray[temppos]=temp; } template<class T> BOOL MaxHeap<T>::Insert(const T& newNode) {//向堆中插入一个结点 if(CurrentSize==MaxSize) //堆空间已经满 return FALSE; heapArray[CurrentSize]=newNode; SiftUp(CurrentSize); //向上调整 CurrentSize++; } template<class T> T&MaxHeap<T>::RemoveMax() { if(CurrentSize==0) { AfxMessageBox("Can'tDelete"); } else { T temp=heapArray[0]; //取堆顶元素 heapArray[0]=heapArray[CurrentSize-1]; //堆末元素上升至堆顶 CurrentSize--; if(CurrentSize>1) SiftDown(0); //从堆顶开始筛选 return temp; } } template<class T> void MaxHeap<T>::MoveMax() { if(CurrentSize==0) { //堆为空 return; } else { T temp=heapArray[0]; //取堆顶元素 heapArray[0]=heapArray[CurrentSize-1]; //堆末元素上升至堆顶 CurrentSize--; if(CurrentSize>1) SiftDown(0); //从堆顶开始筛选 heapArray[CurrentSize]=temp; } return; } template<class T> bool MaxHeap<T>::Remove(int pos, T& node) {// 删除给定下标的元素 if((pos<0)||(pos>=CurrentSize)) return false; node =heapArray[pos]; // 记录删除的元素 heapArray[pos]=heapArray[--CurrentSize]; //用最后的元素代替被删除元素 if(heapArray[Parent(pos)]> heapArray[pos]) // 当前元素小于父节点,需要向上调整 SiftUp(pos); //上升筛 else SiftDown(0); //向下筛 return true; }