学习笔记——堆排序算法

今天把堆排序和堆相关的操作整明白了,记录一下

总地来说,一个堆排序由3步骤组成。

  1. 首先根据待排序数组(乱序)生成一个堆
  2. 将堆顶元素和堆底部元素交换,并将交换后的底部元素排除在堆外
  3. 重新调整堆以使其满足堆的特性

以上每一步都可以完成一个元素的排序,一直执行,直到堆空,则整个待排序数组就有序了。

以大顶堆为例,首先它是一个完全二叉树,并且所有非叶子节点的节点值都要比其左右子节点的节点值大。(相应的小顶堆则是父节点比子节点都要小)

堆的最主要操作:heapify,对一个已知的堆,当其堆顶元素发生变化时,可能不再满足堆的性质,需要进行堆整理。其操作为:在堆顶元素和其两个子节点之间选择一个最大值,与堆顶元素进行交换,并对交换后的子节点递归heapify。如堆顶和左孩子发生了交换,则对以左孩子为堆顶的堆进行递归的heapify。

heapify的代码如下:

void heapify(vector<int> &tree, int n, int i)
{
	// 以线性表存储完全二叉树结构,堆的大小为n,对第i个节点进行heapify操作
	// 注意,n可能和tree.size()不相同,因为在堆排序过程中,会逐渐产生已排序序列,堆的大小会逐渐缩小
	// 但n最大也只能是tree的大小,而i则是堆中的一个元素,因此n和i应该满足 i < n,n <= tree.size()
	if (i >= n || n > tree.size()) {
		return;
	}
	// 从该节点和两个子节点之间选择一个最大的
	int max = i;
	// 数组存储的完全二叉树,其子节点计算方式为2i+1, 2i+2
	int c1 = 2 * i + 1;
	int c2 = 2 * i + 2;
	// 注意判断子节点是否越界
	if (c1 < n && tree[c1] > tree[max]) {
		max = c1;
	}
	if (c2 < n && tree[c2] > tree[max]) {
		max = c2;
	}
	// 如果最大值不是当前节点,则需要进行交换并且递归整理被交换的子树(子堆)
	if (max != i) {
		int temp = tree[i];
		tree[i] = tree[max];
		tree[max] = temp;
		heapify(tree, n, max);
	}
}

需要注意的是,heapify总是用在已经整理好的堆上,并且被改变的元素总是堆顶元素。一次堆整理所需的时间为 O ( l o g 2 n ) O(log_2n) O(log2n),其中n是堆的大小。

那么对于一个初始无序的数组来说,它是不满足堆的性质的,因此,首先需要将一个乱序的数组变成一个堆。堆的构建需要从下往上构建,也就是说,构建完子堆后,才可以构建父堆,这样,在建堆的时候就可以很好地利用heapify函数。那么第一个需要建的堆在哪里呢?简单地说,可以直接从数组的末端开始。对于一个存储完全二叉树的数组来说,末端元素都是叶子节点,叶子节点是不需要调整的(没有子节点),而第一个需要调整的节点就是最后一个元素的父节点

假设数组长度为n,最后一个元素的节点编号为n-1,其父节点编号为(n-1-1)/2。而编号在该节点之前的节点都需要进行堆整理,即从该节点递减到0节点,逐一进行堆整理,则满足上述所说的子堆整理完后整理父堆的条件。

对一个乱序数组建立堆代码如下,用到了上面的heapify函数:

void build_heap(vector<int> tree) 
{
	if (tree.size() < 2) {
		return;
	}
	// 第一个需要调整的节点,也可以直接从tree.size()-1开始调整,但最后那些叶子节点是不需要调整的,可以省下一些操作时间
	int n = (tree.size() - 2) / 2;
	for (int i = n; i >= 0; i--) {
		// 从第一个要调整的节点开始,逐一递减。此处heapify的第二个参数n总是tree.size(),因为此时堆的大小并没有发生改变
		heapify(tree, tree.size(), i);
	}
}

由于进行了接近n/2次的heapify,build_heap的时间复杂度是 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
有了以上的准备后,就可以开始堆排序了。

按照文章开始所说的三步走,

  1. 首先根据待排序数组(乱序)生成一个堆
  2. 将堆顶元素和堆底部元素交换,并将交换后的底部元素排除在堆外
  3. 重新调整堆以使其满足堆的特性

可以写出如下代码:

void heap_sort(vector<int> & nums)
{
	// 此时参数名字不再用tree了,因为函数功能是将一个数组排序,而不是对一个树排序,所以改为nums逻辑比较清晰一些
	// 第一步,将乱序的数组生成一个堆
	build_heap(nums);
	for (int i = nums.size() - 1; i >= 0; i--) {
		// 第二步,将堆顶元素和最后一个元素交换,得到一个完成排序的元素
		int temp = nums[0];
		nums[0] = nums[i];
		nums[i] = temp;
		// 交换后堆顶元素发生了变化,需要对堆顶进行堆整理。
		// 但需要注意的是,此时数组的最后一个元素已经排好序,被移除到堆之外了,堆的大小不再是数组的长度,而是逐渐减小,而堆的长度恰是数字i的值,因此在此时调用heapify时,第二个参数为i
		heapify(nums, i, 0);
	}
}

这样,一个堆排序就完成了。现在来分析一下时间复杂度。第一步建堆时间复杂度 n l o g 2 n nlog_2n nlog2n,第二步循环heapify的复杂度也是 n l o g 2 n nlog_2n nlog2n,因此,堆排序的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),比一般的比较排序要优秀。

本文是在看了bilibili大神视频后写下的,在文末附上大神的视频链接,讲的是真滴好
bilibili-正月点灯笼-heapsort

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值