堆 heap
利用完全二叉树的结构来维护一组数据,然后进行相关操作,一般的操作进行一次的时间复杂度在 O(1)~O(logn) 之间。
完全二叉树
- 若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。我们知道二叉树可以用数组模拟,堆自然也可以。
- 从图中可以看出,元素的父亲节点数组下标是本身的1/2(只取整数部分),所以我们很容易去模拟,也很容易证明其所有操作都为log级别~~
堆还分为两种类型:大根堆、小根堆
顾名思义,就是保证根节点是所有数据中最大/小,并且尽力让小的节点在上方
不过有一点需要注意:堆内的元素并不一定数组下标顺序来排序的!!很多的初学者会错误的认为大/小根堆中
下标为1就是第一大/小,2是第二大/小……
我们刚刚画的完全二叉树中并没有任何元素,现在让我们加入一组数据吧!
下标从1到9分别加入:{8,5,2,10,3,7,1,4,6}。
如下图所示
现在我就来介绍一下堆的几个基本操作:
- 上浮 shift_up;
- 下沉 shift_down
- 插入 push
- 弹出 pop
- 取顶 top
- 堆排序 heap_sort
以 小根堆 为例
从上述未处理过的数据中可以很容易得出,根节点1元素8绝对不是最小的
但是它的一个子节点3(元素2)比它小,我们可以将它放到最高点,直接进行交换。
此外,子节点3的子节点7(元素1)似乎更适合在根节点
此时,我们无法直接和根节点交换的,那么就是用上浮 shift_up操作来完成。
操作过程如下
从当前结点开始,和它的父亲节点比较,若是比父亲节点来的小,就交换,然后将当前询问的节点下标更新为原父亲节点下标;否则退出。
伪代码如下:
Shift_up( i )
{
while( i / 2 >= 1)
{
if( 堆数组名[ i ] < 堆数组名[ i/2 ] )
{
swap( 堆数组名[ i ] , 堆数组名[ i/2 ]) ;
i = i / 2;
}
else break;
}
上浮操作结束后,节点3(元素8)与其子节点7(元素2)的位置并不正确。
因此,需要节点3下沉
节点的下沉策略如下所述
小根堆是尽力要让小的元素在较上方的节点,而下沉与上浮一样要以交换来不断操作。
让当前结点的子节点(如果存在)作比较,哪个比较小就和它交换,并更新询问节点的下标为被交换的子节点下标,否则退出。
伪代码如下所示
Shift_down( i , n ) //n表示当前有n个节点
{
while( i * 2 <= n)
{
T = i * 2 ;
if( T + 1 <= n && 堆数组名[ T + 1 ] < 堆数组名[ T ])
T++;
if( 堆数组名[ i ] < 堆数组名[ T ] )
{
swap( 堆数组名[ i ] , 堆数组名[ T ] );
i = T;
}
else break;
}
插入操作
如何在插入的时候维护堆?
每次进行数据插入的时候,往堆的最后插入,然后使用上浮操作。
伪代码如下所示
Push ( x )
{
n++;
堆数组名[ n ] = x;
Shift_up( n );
}
弹出操作
使用根节点元素和尾节点进行交换,然后使现在的根元素下沉。
伪代码如下所示
Pop ( x )
{
swap( 堆数组名[1] , 堆数组名[ n ] );
n--;
Shift_down( 1 );
}
取顶操作
返回节点数组[0]
堆排序
new 新数组,每次取堆顶元素放进去,然后弹掉堆顶
伪代码如下所示
Heap_sort( a[] )
{
k=0;
while( size > 0 )
{
k++;
a[ k ] = top();
pop();
}
}
堆排序的时间复杂度是O(nlogn)
堆操作代码
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 100010 //这部分可以自己定义堆内存多少个元素
using namespace std;
struct Heap {
int size, queue[maxn];
Heap() { //初始化
size=0;
for(int i=0; i<maxn; i++)
queue[i]=0;
}
void shift_up(int i) { //上浮
while(i > 1) {
if(queue[i] < queue[i>>1]) {
int temp = queue[i];
queue[i] = queue[i>>1];
queue[i>>1] = temp;
}
i >>= 1;
}
}
void shift_down(int i) { //下沉
while((i<<1) <= size) {
int next = i<<1;
if(next < size && queue[next+1] < queue[next])
next++;
if(queue[i] > queue[next]) {
int temp = queue[i];
queue[i] = queue[next];
queue[next] = temp;
i = next;
} else return ;
}
}
void push(int x) { //加入元素
queue[++size] = x;
shift_up(size);
}
void pop() { //弹出操作
int temp = queue[1];
queue[1] = queue[size];
queue[size] = temp;
size--;
shift_down(1);
}
int top() {
return queue[1];
}
bool empty() {
return size;
}
void heap_sort() { //另一种堆排方式,由于难以证明其正确性
//我就没有在博客里介绍了,可以自己测试
int m=size;
for(int i = 1; i <= size; i++) {
int temp = queue[m];
queue[m] = queue[i];
queue[i] = temp;
m--;
shift_down(i);
}
}
};
int main()
{
Heap Q;
int n,a,i,j,k;
cin>>n;
for(i = 1; i <= n; i++) {
cin >> a;
Q.push(a); //放入堆内
}
for(i = 1; i <= n; i++) {
cout << Q.top() << " "; //输出堆顶元素
Q.pop(); //弹出堆顶元素
}
return 0;
}