堆与堆排序

优先级队列

优先级队列是一个由相同类型的数据元素组成的集合,其中每个数据项都带有一个特定的优先级。它支持insert元素操作,findMin访问优先级最小元素的操作,deleteMin删除优先级最小元素的操作,当然,也可能是支持findMax访问优先级最大元素的操作,deleteMax删除优先级最大元素的操作,以需求而定。
有两种很自然的方法来实现优先级队列:
一种是使用无序队列(数组、向量或链表实现)。两种基本操作的实现方法及时间复杂度如下:

  • 插入:把数据项插入到队尾或队头。时间复杂度:O(1)。
  • 删除:遍历队列找出优先级最高或最低的项,然后删除。时间复杂度:O(n)。
另一种是使用 有序队列 (数组、向量或链表实现)。
两种基本操作的实现方法及时间复杂度如下:
  • 插入:基本线性插入排序。时间复杂度:O(n)。
  • 删除:删除队头和队尾的项。时间复杂度:O(1)。
当然,还有一个可能的实现是 二叉查找树 ,但二叉查找树很容易变得不平衡,这样,插入和删除效率便会大大降低,另外,二叉查找树实现了比优先级队列更多的功能,可以想见,它必然也消耗了更多的资源。所以,将二叉查找树用作优先级队列的实现,实在太大材小用了。
当当当当!我们今天的主角登场了。 堆,或称作二叉堆 ,是“已知的最优雅的数据结构之一”。堆是实现优先级队列的典型方法,两种基本操作时间复杂度如下:
  • 插入:最坏情况下时间复杂度为O(logn);平均情况下时间复杂度为O(1),业已证明,执行一次插入平均需要 2.607 次比较,因此平均 insert 操作上移元素 1.607层。
  • 删除:在平均情况和最坏情况下时间复杂度均为O(logn)。
表现简直完美!STL中的priority_queue类模板就是采用二叉堆来实现的。

堆是符合下列性质的二叉树:

  1. 结构属性:堆是一棵完全树。
  2. 顺序属性:每个结点中的数据项都大于或等于儿子的数据项。
以上的顺序结构是针对最大堆而言的,最小堆的叙述则恰好相反。下面的讲解都默认为最大堆。
一般采用数组或向量来实现堆,初次接触这个事实可能会感到奇怪,因为我们所接触过的树结构都是使用链式结构来实现的。采用数组或向量来实现堆效率更高,这主要是因为堆没有在中间位置删除和插入元素的需求,另外堆是一棵完全树,方便索引。
一个最大堆结构如下图所示,它既是一棵二叉树,也是一个数组。
堆1
我们来看看堆的基本操作:
1、 插入操作(insert)
插入操作的过程很简单:将新数据插入到堆的下一个空位,然后向上渗透直到二叉树重新满足堆的顺序属性为止。

实现代码:

[cpp]  view plain copy
  1. //插入操作在平均情况下花费常量时间,在最坏情况下花费对数时间  
  2. //业已证明,执行一次插入平均需要 2.607 次比较,因此平均 insert 操作  
  3. //上移元素 1.607层。  
  4. template<typename T>  
  5. void BinaryHeap<T>::insert(const T& value)  
  6. {  
  7.     if(theSize+1==theArray.size())  
  8.         theArray.resize(theArray.size()*2);  
  9.     int hole = ++theSize;  
  10.     //hole==1时已是最高点,不可向上渗透了。  
  11.     for(;value<theArray[hole/2]&&hole>1;hole/=2)  
  12.         theArray[hole] = theArray[hole/2];  
  13.     theArray[hole] = value;  
  14. }  

2、下滤操作(percolate down)
近似堆的定义是,二叉树根结点的左右子树均为堆,但整体不满足堆的顺序属性。下滤操作是把近似堆调整为堆的过程。
该过程也很简单:找到一个较大的儿子,如果该结点比儿子小,则和儿子交换位置。重复上述过程直到二叉树满足堆的顺序属性。
下图展示了对结点2进行下滤操作的过程。
堆2
实现代码:

[cpp]  view plain copy
  1. template<typename T>  
  2. void  BinaryHeap<T>::percolateDown(int hole)  
  3. {  
  4.     int child;  
  5.     T temp = theArray[hole];  
  6.     for (;hole*2<=theSize;hole=child)  
  7.     {  
  8.         child = hole*2;  
  9.         if(child!=theSize && theArray[child+1]<theArray[child])  
  10.             ++child;  
  11.         if(theArray[child]<temp)  
  12.             theArray[hole]=theArray[child];  
  13.         else  
  14.             break;  
  15.     }  
  16.     theArray[hole]=temp;  
  17. }  

我并没有单独列出删除操作(delete),这是因为删除操作主要过程就是下滤操作:删除根结点,将堆最后面的数据项复制到根结点,执行下滤操作。
实现代码:

[cpp]  view plain copy
  1. //删除操作在平均情况和最坏情况下都需要对数时间  
  2. template<typename T>  
  3. void BinaryHeap<T>::deleteMin()  
  4. {  
  5.     if(isEmpty())  
  6.         throw exception();  
  7.     theArray[1]=theArray[theSize--];  
  8.     percolateDown(1);  
  9. }  
  10.    

3、 建堆操作(build heap)
建堆操作是把一棵完全二叉树转换成堆。这是堆排序中的关键操作。该操作的具体过程为:从最后一个非叶子节点开始到根结点,依次执行下滤操作。
下图展示了构建一个最大堆的过程。
堆3
实现代码:

[cpp]  view plain copy
  1. template<typename T>  
  2. void BinaryHeap<T>::buildHeap()  
  3. {  
  4.     for(int i=theSize/2;i>0;--i)  
  5.         percolateDown(i);  
  6. }  

堆排序

顾名思义,堆排序是利用堆的特性来对一组数据进行排序。堆排序可分为以下个步骤(从小到大排序):

  1. 对所有n个数据进行建堆(最大堆)操作。
  2. 将堆顶元素和堆尾元素交换,对剩下的n-1个数据进行建堆(最大堆)操作。
  3. 重复步骤2直至堆的大小为1,此时数据完成排序。
有两个问题需要重视:
  1. 被排序的数据从0开始索引,所以父亲和孩子的位置都有了相应的变化。父亲的位置由i/2变为(i-1)/2,左孩子的位置由2i变为2i+1,右孩子的位置由2i+1变为2i+2。
  2. 下滤操作需要知道当前堆的大小。

实现代码:

[cpp]  view plain copy
  1. template<typename T>  
  2. void percolateDown(vector& vec,int i,int length)  
  3. {  
  4.     int child;  
  5.     T temp = vec[i];  
  6.     for(;2*i+1<=length-1;i=child)  
  7.     {  
  8.         child = 2*i+1;  
  9.         if(childtemp)  
  10.             vec[i] = vec[child];  
  11.         else  
  12.             break;  
  13.     }  
  14.     vec[i] = temp;  
  15. }  
  16.    
  17. template<typename T>  
  18. void heapSort(vector& vec)  
  19. {  
  20.     for(int i = vec.size()/2;i>=0;--i)  
  21.         percolateDown(vec,i,vec.size());  
  22.     for (int j = vec.size()-1;j!=0;--j)  
  23.     {  
  24.         swap(vec[0],vec[j]);  
  25.         percolateDown(vec,0,j);  
  26.     }  
  27. }  
  28.    
  29. void main()  
  30. {  
  31.     int a[]={312,33,44,5,6,4,2,3,4,5,6,76,8,6,88768,5345,43,432};  
  32.     vector<int> vec(a,a+sizeof(a)/sizeof(*a));  
  33.     for (int i=0;i!=vec.size();++i)  
  34.         cout<<vec[i]<<"  ";  
  35.     cout<<endl;  
  36.     heapSort(vec);  
  37.     for (int i=0;i!=vec.size();++i)  
  38.         cout<<vec[i]<<"  ";  
  39.     cout<<endl;  
  40. }  

在PercolateDown算法中,在每一个阶段中考虑的项的数目是前一阶段的一半。因此,与二叉查找树的情况类似,这个算法的最坏计算时间是O(logn)。由于BuildHeap操作执行了n/2次PercolateDown操作,其最坏计算时间是O(nlogn)。HeapSort执行一次BuildHeap操作和n-1次PercolateDown操作,因此其最坏计算时间是O(nlogn)。

声明:文中三幅插图来自《算法导论》第三版英文版。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值