详解堆的应用---从堆的建立到调整再到堆排序

不同于内存分配的堆,这里的堆指的是数据结构中的一种类型

如上图,堆具体来看是一个完全二叉树的数据结构(为什么是完全二叉树后面讲),分为大顶堆和小顶堆,大顶堆,也就是顶比较大,所以就是父节点大于两个子节点,小顶堆也就是反过来。

那么如何利用这样的一个数据结构?

例题:

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;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值