堆
今天看看堆,这里的堆不是操作系统堆栈的堆,而是一个二叉树的结构。它不仅可以用来实现堆排序,还可以构造一种有效的优先队列。
(二叉)堆是一个数组,也可以被看作是一个近似的完全二叉树。树上的每一个结点对应数组中一个元素。除了最底层外,该树是完全充满的,而且是从左向右填充。
这样我们很容易就能根据下标找到一个节点的父节点下标或子节点下标。
Parent(i):
return i/2;
LeftChild(i):
return 2*i;
RightChild(i):
return 2*i+1;
在计算机上,通常可以通过左移或右移快速实现 2*i 或 i/2 。
二叉堆有两种:
- 最大堆:所有节点都大于或等于它们的子节点。
- 最小堆:所有节点都小于或等于它们的子节点。
操作
对于有n个节点的堆,可以看成n个节点的完全二叉树,所以高度为 log(n) 。接下来介绍一些操作,这些操作的复杂度至多与树的高度成正比,即时间复杂度为 O(lg(n))。
下面以最大堆为例。
维护堆的性质(MAX-HEAPIFY)
输入一个数据A和下标 i,我们假定以 A[i]的左孩子和右孩子为根节点的二叉树都是最大堆,此时 A[i]有可能比它们小,这就违背了最大堆的性质。
因此,MAX-HEAPIFY通过让A[i]的值在最大堆中”逐级下降“,从而使下标为 i 的根节点的子树重新遵循最大堆的性质。
MAX-HEAPIFY(A,i):
r=RightChild(i);//获得右孩子下标
l=LeftChild(i);//获得左孩子下标
if l<=A.head-size and A[l]>A[i]
largest = l
else
largest = i
if r<=A.head-size and A[r]>A[largest]
largest = r
if largest != i //有交换
exchange(A[i],A[largest]) //交换
MAX-HEAPIFY(A,largest) //对largest这个下标再进行判断
一个例子
对于一个下标 i,顶多对比到一棵树的高度,所以复杂度为 O(log(n)) 。
建堆
我们可以用自底向上的方法调用 MAX-HEAPIFY函数将一个大小为 A = A.length 的数组转换为最大堆。
叶节点 :数组 A[n/2+1…n] 都是树的叶节点(下标从1开始), 这部分都可以看作是一个个独立的最大堆。所以我们可以通过 BUILD-MAX-HEAP 对树中的其他节点都调用一次 MAX-HEAPIFY 调整好这个堆。
BUILD-MAX-HEAP(A):
for i = A[A.length/2] downto 1
MAX-HEAPIFY(A,i)
这段的时间复杂度为 O(n), 计算涉及到循环不变量等概念,建议看 《算法导论》,俺还不会。
堆排序算法(HEAP-SORT)
堆排序就是将二叉堆上的结点取出来排好序。那么以最大堆为例, A[1]总是最大的,所以肯定先将它排好,所以有以下步骤:
- A[1] 与 A[A.heap-size] 交换, A.heap-size 减一
- MAX-HEAPIFY 调整A[1…heap-size]中的二叉堆
HEAP-SORT(A)
BUILD-MAX-HEAP(A)
for i = A.length downto 2
exchange(A[1],A[i])
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A,1)
不难看出是原址排序,复杂度 O(log(n))。
上浮法建堆:
无序数组建立堆最直接的方法是从左到右遍历数组进行上浮操作。
下沉法建堆:
一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。
叶子节点不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。
交换并下沉:
建好堆之后进行n-1次堆顶元素与最后一个元素的交换,每次交换之后需要从堆顶进行下沉操作维持堆的有序状态。
#include<bits/stdc++.h>
using namespace std;
//大顶堆
class heap{
private:
vector<int> arr;
int len;
public:
heap(vector<int> a):arr(a),len(a.size()-1){}
//上浮
void up(int k){
while(k/2>0){
int p=k/2;
if(arr[p]<arr[k])
swap(arr[p],arr[k]);
k=p;
}
}
//下沉,最重要的函数(必会)
void down(int k){
while(2*k<=len){
int j=2*k;
if(2*k+1<=len&&arr[2*k+1]>arr[j])
j=2*k+1;
if(arr[j]<=arr[k])
break;
swap(arr[k],arr[j]);
k=j;
}
}
void CreateHeap(){
//下沉调整,只用遍历一半
for(int i=len/2;i>=1;i--)
down(i);
//上浮调整
//for(int i=1;i<=len;i++)
// up(i);
}
void HeapSort(){
CreateHeap();
for(int i=1;i<arr.size()-1;i++){
swap(arr[1],arr[len]);
len--;
down(1);
}
}
void print(){
for(int i=1;i<arr.size();i++)
cout<<arr[i]<<" ";
}
};
int main(){
vector<int> arr;
srand(time(NULL));
arr.push_back(-1);
for(int i=0;i<10;i++)
arr.push_back(rand()%100);
heap target(arr);
target.HeapSort();
target.print();
}
优先队列
堆还有一种常见的应用就是优先队列。优先队列(priority-queue)是一种用来维护由一种元素构成的集合S的数据结构。 其中每一个元素都有一个 key值,一个优先队列包含以下操作:
- INSERT(S,x):将元素x插入到集合S中,等价于 S U {x}。
- MAXIMUM(S):返回 S中具有最大key的元素。
- EXTRACT-MAX(S):去掉并返回S中具有最大关键字的元素。
- INCREASE-KEY(S,x,k):将元素x的关键字增加到k,A[x]=k。
实现思路和最大堆挺像的。
参考文章
《算法导论》第三版
数据结构和算法总结 作者:字节CTO