[排序算法] 8. 堆排序及模拟实现堆(选择排序,堆排序,向下调整)

1. 基本思想

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。

1.1 复习数据结构—堆

在此可以顺便把堆复习一下:

说到堆,就得谈谈二叉树的顺序结构,普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。

现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

在这里插入图片描述

1.2 堆的概念

  • 如果有一个关键码的集合K={K(0),K(1),K(2)……K(n-1)},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:K(i)<=k(2* i+1)且 K(i)<=k(2* i+2)(K(i)>=k(2* i+1)K(i)>=k(2*i+2)) i=0,1,2……则称为小堆(或大堆)。
    在这里插入图片描述

1.3 堆的性质

堆中某个节点的值总是不大于或不小于其父节点的值;堆总是一棵完全二叉树。
在这里插入图片描述
其中这样顺序存储产生最良好的一个性质就是:

  • 已知parent的下标,那么其逻辑结构的左孩子left、右孩子right在存储结构下数组中下标就能够计算得到,即:left = parent * 2 + 1right = parent * 2 + 1,反之已知任意一个孩子的数组下标就能够求得parent的数组下标,在这利用了int的作除法取整的技巧,将式子进行统一:parent =(child - 1) / 2,常用求解最后一个非叶子节点,即知道数组大小size时,可求解最后一个非叶子节点为parent = (size - 1 - 1 ) / 2

1.4 堆的实现

1.4.1 堆的AdjustDown向下调整算法

现在给出一个数组,逻辑上看做一颗完全二叉树。通过从根节点开始的向下调整算法可以把它调整成一个小堆。向下调整算法有一个前提:左右子树必须是一个堆,才能调整。
在这里插入图片描述
主要注意点及判断顺序如下:

  • 传入的根节点,首先判断是否有有孩子,即它是不是叶子节点,left = 2 * root + 1; right = 2 * r + 2;由于是完全二叉树,只判断左孩子子即可left >= size,即可说明它为叶子节点,向下调整算法结束,return 即可
  • 说明有孩子,再判断有没有右孩子,在这由于数组顺序存储的形式,需要同时判断一步是否越界,右孩子存在根据完全二叉树性质一定存在左孩子,若右孩子存在则找到左右孩子中最小的孩子,即 right < size && array[right] > array[left],也是利用与运算符的截断性质
  • 将根的值与最小的孩子进行比较,如果根的值小,那么直接 return即可,否则,交换根与最小孩子的值
  • 再将对根进行递归的向下调整即可AdjustDown(array, size, minchild)
1.4.2 堆的创建

下面给出一个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在通过算法,把它构建成一个堆。根节点左右子树不是堆,怎么调整呢?

int a[] = {1,5,3,8,7,6};

这里从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆,如下图所示:
在这里插入图片描述
上面图就很清楚的表明了,我们可以通过上面的方法递归调用向下调整算法将一个逻辑上的完全二叉树变成大根堆,这也是堆排序排升序序列的基础

1.4.3 堆的插入与AdjustUp向下调整算法

先插入一个80到数组的尾上,再进行向上调整算法,直到满足堆
在这里插入图片描述
这个向上调整算法就很简单了,就插入元素而言,根据插入进去的child数组下标找到父节点的数组下标,再根据大堆/小堆的性质进行元素交换即可,最后将交换过的数组child下标进行更新即可。注意对child == 0一开始是否需要判断,否则会出现数组越界问题。

1.4.4 堆的删除

要知道:删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。
在这里插入图片描述

1.4.5 堆排序

前面讲向下调整算法都已经很明白了,再拿这张图走一遍就ok了,这是建的大堆,实行递增排序:
在这里插入图片描述
就这几行主要思想和代码:

  • 首先排递增序,建个大堆,不能建小堆,会破坏堆的结构
  • 交换堆顶元素与数组从末尾到前的未经历Swap的元素,再对堆顶元素进行向下调整即可
void HeapSort(int array[], int size) {
	CreateHeap(array, size);

	for (int i = 0; i < size; ++i) {
		Swap(array, array + size - i - 1);
		AdjustDown(array, size - i - 1, 0);
	}
}
1.4.6 模拟实现堆
#include <stdlib.h>
#include <assert.h>

// array[size] 表示数组及大小
// root 表示要调整的结点的下标
// 前提是 [root] 所在的结点左右子树已经满足堆的性质了
void AdjustDown(int array[], int size, int root) {
	while (1) {
		int left = 2 * root + 1;
		int right = 2 * root + 2;
		int min;

		if (left >= size){
			// 越界
			return;
		}

		// 确定哪个是最小的孩子
		if (right < size && array[right] < array[left]) {
			min = right;
		}
		else {
			min = left;
		}

		if (array[root] <= array[min]) {
			return;
		}

		// 交换值
		int t = array[root];
		array[root] = array[min];
		array[min] = t;

		//AdjustDown(array, size, min);
		root = min;
	}
}

// 建堆
// O(n * logN)	--> O(n)
void CreateHeap(int array[], int size) {
	// 从最后一个非叶子结点开始,调整到 0 结束
	// 最后一个非叶子结点就是最后一个结点的双亲结点
	for (int i = (size - 2) / 2; i >= 0; i--) {
		AdjustDown(array, size, i);	// O(log(n))
	}
}

typedef int	HPDataType;

typedef struct Heap {
	HPDataType	*array;
	int			size;
	int			capacity;
}	Heap;


void HeapCreateHeap(Heap *heap, int array[], int size) {
	// 暂时不考虑扩容问题
	heap->capacity = size * 2;
	heap->size = size;
	heap->array = (int *)malloc(sizeof(int)* size);
	for (int i = 0; i < size; i++) {
		heap->array[i] = array[i];
	}

	CreateHeap(heap->array, heap->size);
}

// 小堆
void AdjustUp(int array[], int size, int child) {
	while (child != 0) {
		int parent = (child - 1) / 2;
		if (array[child] >= array[parent]) {
			return;
		}

		int t = array[parent];
		array[parent] = array[child];
		array[child] = t;

		child = parent;
	}
}

// 增加
void HeapInsert(Heap *heap, int val) {
	heap->array[heap->size] = val;
	heap->size++;

	AdjustUp(heap->array, heap->size, heap->size - 1);
}

// 删除(只能删除堆顶元素)
// O(Log(n))
void HeapPop(Heap *heap) {
	assert(heap->size > 0);

	heap->array[0] = heap->array[heap->size - 1];
	heap->size--;

	AdjustDown(heap->array, heap->size, 0);
}

// 返回堆顶元素,返回最值
HPDataType HeapTop(Heap *heap) {
	assert(heap->size > 0);

	return heap->array[0];
}

2. 代码实现

// 堆排序(递增)

// 大顶堆的向下调整
void AdjustDown(int array[], int size, int r) {
	int left = 2 * r + 1;
	int right = 2 * r + 2;	
	if (left >= size) {		// 是否为叶子节点
		return;			// 叶子节点直接结束
	}
	int m = left;		// 有左孩子
	// 是不是有右孩子,并找最大的孩子
	if (right < size && array[right] > array[left]) {
		m = right;
	}
	// 如果根的值大于最大孩子,直接返回
	if (array[r] >= array[m]) {
		return;
	}

	Swap(array + r, array + m);	 // 将最大值最为新的根
	AdjustDown(array, size, m);	 // 递归向下调整
}

void CreateHeap(int array[], int size) {
	// i=最后一个非叶子节点
	// 已知parent,则 left=2* parent+1,right=2*parent+2
	// 已知child,则parent=(child-1)/2,在此均为数组的下标
	// 故最后一个非叶子节点,就是数组最后一个下标size-1,再-1,结果除2即可
	for (int i = (size - 1 - 1) / 2; i >= 0; --i) {
		AdjustDown(array, size, i);
	}
}

// 堆排序(递增)
void HeapSort(int array[], int size) {
	CreateHeap(array, size);

	for (int i = 0; i < size; ++i) {
		Swap(array, array + size - i - 1);
		AdjustDown(array, size - i - 1, 0);
	}
}

测试数据:int array[] = { 3, 9, 1, 4, 2, 8, 2, 7, 5, 3, 6, 11, 9, 4, 2, 5, 0, 6 };
在这里插入图片描述

3. 性能分析

时间复杂度

  • 最坏 O ( n l o g n ) O(nlogn) O(nlogn) 对数据不敏感
  • 平均 O ( n l o g n ) O(nlogn) O(nlogn) 对数据不敏感
  • 最好 O ( n l o g n ) O(nlogn) O(nlogn) 对数据不敏感

空间复杂度

  • O ( n ) O(n) O(n)

排序稳定性

  • 不稳定

堆排序每一趟通过堆调整产生有序区,它是全局有序区,每次调整时间为 O ( l o g n ) O(logn) O(logn)

建堆的时间为 O ( n ) O(n) O(n),比较次数大约为 4 n 4n 4n次。

当然,了解STL同学能够知道:堆(heap)也被称为优先队列(priority-queue)这个应用很广泛,什么前K个高频元素、高频单词、TOP K问题等等,都能够运用到优先队列。

至于想检测一下掌握的怎么样了,推荐去刷刷OJ题目,如牛客、LeetCode的OJ,对知识的应用很有帮助,

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值