不同于内存分配的堆,这里的堆指的是数据结构中的一种类型
如上图,堆具体来看是一个完全二叉树的数据结构(为什么是完全二叉树后面讲),分为大顶堆和小顶堆,大顶堆,也就是顶比较大,所以就是父节点大于两个子节点,小顶堆也就是反过来。
那么如何利用这样的一个数据结构?
例题:
Description
用已知的数据建立一个小顶堆,再对询问输出相应的结果。
Input
输入数据有多组
对于每组测试数据,第一行两个整数n和m ( n,m<= 10000),表示要用n个整数要建立堆,有m个询问。第二行有n个整数。接下来m行,每行为“pop”或者“push x”表示取出堆顶元素和将x插入堆中
Output
对于“pop”询问输出一行,为堆顶元素
Sample Input
5 5
2 1 3 4 5
pop
push 3
pop
push 1
pop
Sample Output
1 2 1
题目要求我们先根据一串数字构造一个小顶堆,然后再执行一些命令,插入数字和弹出堆顶
先来看如何构造一个小顶堆
首先给出一个序列,你需要从给定序列的前往后一个一个去插入堆中,并且在插入过程中要不断维护小顶堆的性质,就用2 1 3 4 5举例
先进来了一个2,然后你把它直接放到堆中,这当然天然符合小顶堆的条件,然后这时候进来了一个1,这时候1跟父亲节点2一比较,发现比1要小,根据小顶堆的性质,子节点是一定要大于父节点的,所以这时候就需要交换与父节点的位置,于是二叉树重新变回了堆结构
这时候读取下一个位置的数字,是3,放在1的后面,发现还是一个小顶堆的结构,所以不用管
下面是4和5,插入之后如下图
于是一个小顶堆就构造完成
回到开始的问题,为什么是一棵完全二叉树,因为这里的构造就是从原始序列的第一个一直到最后一个,每次都是将一个数字插入一棵树最下面一层的最后一个,像图中箭头这样的顺序这样插入(接下来会插入到3下面)。
而每次交换,都只涉及到和父亲节点的交换,虽然可能会交换多次,但并不会改变树的形态,所以最终一定是完全二叉树,所以在代码中树的结构也体现为数组形式,比较适用于完全二叉树。
下面是对于数组从st到ed区间构造一个小顶堆的代码的实现
void CreateHeap(int st,int ed) {
for (int i = st; i <=ed ; i++) {
int j = i;
while (j > 1 && a[j] < a[j / 2]) {//a也就是需要构造堆的数组
//j大于1代表还有父亲节点,j==1时已经是根节点了,已经无意义了
swap(j, j / 2);//swap(x,y)交换下标为x,y的两个数字
j /= 2;
}
}
}
插入一个数字实际上就是类似于前面的其中任意一个过程,只要把区间看做长度为1就好了
接下来是关于堆的调整
如果需要在堆中删除一个数字,那么就需要把末尾的数字换过去,数组长度-1,也就是删除最后一个节点,然后维护堆。
这里就以简单的删除堆顶为例,删除堆顶,需要完成两个操作,先把堆顶元素换到最后,然后长度-1,这时候原来的末尾元素到了堆顶
那么如图,这时候的堆就不符合条件了,这时候就需要调整堆。
主要就是要调整换上去的那个,因为其他的子树没有任何变化。
令 x 指向 5 , j 指向左孩子,因为要保证比两个孩子都小,所以先要比较两个孩子,找出小的那个,这里就是2(否则令j++指向右孩子)
比较发现j的更小,所以需要把5换下来
然后x下来,j重新指向x的左边孩子,找出左右孩子的小的那个比较,如果父节点大了就交换,重复这样的操作,到了不需要交换或者j越界了,那么也就是找到了正确的位置
如图,这时候,x下移,j=x*2,超出了范围,也就是代表没有左孩子了,结束调整。
代码如下
void adjust(int x,int n) {
int j = x*2;
while (1) {
if (j > n)break;
if (j + 1 <= n && a[j] > a[j + 1])j++;
if (a[j] < a[x])swap(j, x);
else break;//没有交换就是正确位置
x = j;
j *= 2;
}
}
完整代码(针对例题的)
#include<stdio.h>
int n, m;
int a[100010];
char s[100];
void swap(int x,int y) {
int t = a[x];
a[x] = a[y];
a[y] = t;
}
void CreateHeap(int st,int ed) {
for (int i = st; i <=ed ; i++) {
int j = i;
while (j > 1 && a[j] < a[j / 2]) {
swap(j, j / 2);
j /= 2;
}
}
}
void adjust(int x,int n) {
int j = x*2;
while (1) {
if (j > n)break;
if (j + 1 <= n && a[j] > a[j + 1])j++;
if (a[j] < a[x])swap(j, x);
else break;//没有交换就是正确位置
x = j;
j *= 2;
}
}
int main() {
while(~scanf("%d%d", &n,&m)) {
for (int i = 1; i <= n; i++)scanf("%d", a + i);
CreateHeap(1,n);//从1到n建立堆
//for (int i = 1; i <= n; i++)printf("%d ", a[i]);
while (m--) {
scanf("%s", s);
if (s[1] == 'o') {//代表删除堆顶
printf("%d\n", a[1]);
swap(1, n);//把最后一个元素提升上来
n--;//删除最后一个
adjust(1, n);//维护堆
}
else {//代表插入一个数,
n++;
scanf("%d", a + n );
CreateHeap(n,n);//只需要调整一个的位置
}
}
}
return 0;
}
然后讲一讲堆排序
堆排序实际上就是在原来建好一个堆的基础上进行的,涉及到的基本操作前面都讲过了。
首先是对于序列生成一个堆,具体类型无所谓,跟前面一样。
然后是排序,如果是小根堆,那么最顶端的一定无序序列(也就是排序中的有效序列)中的最小的,跟最后一个交换,然后类似于上面的删除操作,唯一不同的就是把换下来的还是放在堆中,但是堆的有效长度变化,因为最后一个顶替了第一个,那么堆就可能不符合条件了,这时候就需要调整,然后重新变成一个堆,再取出堆顶跟最后一个交换。
需要说明一下对于最后一个的定义,不同于删除,删除时把堆顶换下来之后,数组长度就可以-1了,这时候换下来的堆顶元素已经没用了,但是排序不一样,堆顶元素是当前无序序列中最大或者最小的,最终这些数字还要输出,还是有用的,但是对于排序来说,已经有序的放在最后,暂时用不到了,在排序中的最后一个实际上是指无序序列的最后一个。
可以看一下这篇博客,对于排序过程的描述更加详细
堆排序算法(图解详细流程)_阿顾同学的博客-CSDN博客_堆排序过程图解
然后归到有序序列中后就实现有序了。
最后附上一个参考代码
#include<stdio.h>
int a[] = { 0,1,5,7,4,2,3,6,6,7,5,3,2,6,0,1,1,1,9,2,1,1,8,9,3,2,1 };
int n = sizeof(a) / sizeof(a[0]) - 1;
void print(){
for (int i= 1; i<=n; i++)
printf("%d ", a[i]);
printf("\n");
}
void swap(int i, int j) {
int x = a[i];
a[i] = a[j];
a[j] = x;
}
void adjust(int x,int maxn) {//maxn定义有效位置
int j = x * 2;//左孩子
while (1) {
if (j > maxn)break;
if (j + 1 <= maxn && a[j] < a[j + 1])
j++;
if (a[x] < a[j])swap(x, j);//如果孩子大那么交换
else break;//否则就可以退出
x = j;//跟踪
j = x * 2;
}
}
//升序排序,生成大根堆
void heap_sort(){
//堆排序首先要生成一个堆
for (int i = 1; i <= n; i++) {
while ((i>1 && a[i] > a[i / 2])) {//变为1就是已经到根节点了
swap(i, i / 2);
i /= 2;
}
}
//接下来开始调整
for (int i = n; i >= 1; i--) {
swap(1, i);//把第一个换到最后,也就是最大的值
//接下来要调整堆
//要找到属于堆顶数字的位置
adjust(1,i-1);//调用函数
}
}
int main()
{
printf("排序前:");
print();
heap_sort();
printf("排序后:");
print();
return 0;
}