结合代码 图解 堆排序

参考链接

堆排序

概述

堆排序是利用 这种数据结构进行排序的一种排序算法,堆排序是一种选择排序(每次选出序列中 最大 / 最小 元素)
它的最好和最坏时间复杂度都是O(n logN)
升序->大顶堆
降序->小顶堆

堆是具有以下性质的完全二叉树

因为是完全二叉树,所以可以映射为一个一维数组

  1. 每个节点值都>=其左右孩子节点的值,称为大顶堆
  2. <=称为小顶堆

基本步骤

  1. 先将待排序列构造成一个大顶堆
  2. 将根节点(序列中最大的节点)与末尾元素进行交换
  3. 对剩余前 n+1 个元素重复上述步骤

图解

int nums[8] = { 5,23,16,88,47,65,32,9 }为例

图片名称

首先会将给到的数组看作是一颗完全二叉树

然后将它做一轮调整,将其调整为一个 大根堆 或者 小根堆

	// 视作一棵完全二叉树
	// 从最后一个非叶子节点开始,对每一个非叶子节点进行调整
	for (int i = size / 2 - 1; i >= 0; --i) {
		adjust(arr, size, i);
	}

具体的调整函数的动作是:

从最后一个 非叶子节点(因为是完全二叉树,所以这个索引号是可以确定计算出来的size/2-1)开始(也就是88)

比较当前节点和左右孩子节点的值,如果孩子节点中存在比当前节点更大的值,就交换

同时,需要对交换前的孩子节点位置做递归的调整(因为会影响到下面子树)

比如调整 16,发现左右孩子中有比它更大的,就会去交换 16 和 65 的位置,然后继续去调整交换后 16 节点的二叉树,因为下面在没有孩子了,所以结束

图片名称
void adjust(int arr[],int len,int index) {
	// 所要调整节点的左右孩子节点的索引
	int left = 2 * index + 1;
	int right = 2 * index + 2;
	int maxIdx = index;// 三个数中,最大的那个的下标

	// 如果孩子节点中有更大的,就把它和两个孩子中相对较大的交换
	// 如果子节点索引大于等于了数组长度,则这个孩子是不存在的
	
	// 值相等的情况可以有多种不同的实现方法,所以说是不稳定的排序
	if (left<len && arr[left]>arr[maxIdx]) maxIdx = left;
	if (right<len && arr[right]>arr[maxIdx]) maxIdx = right;

	// 不等就说明当前节点不是最大数
	if (maxIdx != index) {
		// 交换
		swap(arr[maxIdx], arr[index]);

		// 如果被交换前的孩子节点,还有子节点,那么交换后的 maxInx 位置(值为当前节点而不再是较大子节点),需要递归地去调整
		// 究竟有没有是在 adjust 函数内部判断的
		adjust(arr, len,  maxIdx);
	}
}

这样第一轮循环结束后我们就得到了一个 大根堆 / 小根堆

图片名称

我们再回过头来看这个数组[88,47,65,23,5,16,32,9],很明显对于我们需要实现的目标(有序序列)而言,我们只得到了序列中最大的数和它的位置是确定的,而剩余序列并不有序

也就是说对于构造一次大根堆而言,我们只能得到最大的元素

所以我们需要第二轮循环,它进行如下动作:

将已经确定位置的最大元素与队列末尾元素交换,然后对剩下的序列从根节点开始向下调整,重复这一过程直到所有的元素均被排序

	// 上面结束,就完成了一趟建大堆的过程,但是叶子节点从左到右并不是有顺序的
	// 接下来是把最大的丢到最后去,然后再重复建堆(调整)
	// 调到 1 就行,最后一个最小的就保留在最上面不用换

	for (int i = size - 1; i >= 1; --i) {
		// 将当前最大元素与数组末尾元素交换
		// 下标为0的根最大,i是当前末尾
		swap(arr[0], arr[i]);
		// 将未完成排序的部分继续进行堆排序
		adjust(arr, i, 0);
	}

这样,我们最终可以得到:排序完成的序列[5,9,16,23,32,47,65,88]

图片名称

完整代码

void adjust(int arr[], int len, int index) {
	int left = 2 * index + 1;
	int right = 2 * index + 2;
	int maxIdx = index;

	if (left<len && arr[left]>arr[maxIdx]) maxIdx = left;
	if (right<len && arr[right]>arr[maxIdx]) maxIdx = right;

	if (maxIdx != index) {
		swap(arr[maxIdx], arr[index]);
		adjust(arr, len, maxIdx);
	}
}

void heapSort(int arr[], int size) {

	for (int i = size / 2 - 1; i >= 0; --i) {
		adjust(arr, size, i);
	}

	for (int i = size - 1; i >= 1; --i) {
		swap(arr[0], arr[i]);
		adjust(arr, i, 0);
	}
}

int main() {
	int nums[8] = { 5,23,16,88,47,65,32,9 };
	heapSort(nums, 8);
	for (int num : nums) printf("%d ", num);

	return 0;
}

但是这里好像没有涉及到“优先队列”,我在力扣看到的一些涉及堆的题目都用到了“优先队列”

优先队列

上面是基于递归的,这里还有另外一种 基于迭代 的 adjust 函数的实现,更巧妙但也更不好理解

void adjustHeap(vector<int>& nums, int i, int len) {
	int temp = nums[i];// temp始终保存的是最初是要调整位置的值
	for (int k = i * 2 + 1; k < len; k = 2 * k + 1) {
		// 这里 k 的初始化就是左孩子节点,下面的 k+1 就表示了右孩子节点
		// k 的迭代规则也对应了下一个左孩子
		// 第一句先比出了左右孩子中较大的那个
		if (k + 1 < len && nums[k + 1] > nums[k]) k++;
		// 如果较大的孩子大于了当前节点,就更新值并更新索引i(对应了当前要调整的父节点值)
		// 注意这里只是更新但是没有交换,用子节点值去更新了父节点值,并重复这一过程
		// 但是更新子节点的过程被保留了
		if (nums[k] > temp) {
			// nums[k]代表了更大的孩子
			// 很明显这是一个从上至下的过程,nums[i]代表了这个过程中最大的值
			nums[i] = nums[k];
			i = k;
		}
		else break;// 不涉及更新就不再向下处理
	}
	// 这也是上面为什么要更新 i 的原因,i对应了每一轮被交换的子节点的索引
	// 但是调整 i 不会影响到整个循环吗?毕竟是循环条件
	// 不会,因为只有第一次初始化k用到了i,后面k的更新都与 i 无关
	nums[i] = temp;// 这一句是对上面只更新不交换的画龙点睛,它省去了定节点不断交换到最下层位置的路径消耗,一步到位更新最终位置的值
}

复杂度分析

堆排的空间复杂度是O(1),因为可以直接在原数组上操作,不需要额外的空间

然后时间复杂度:最好 = 最坏 = 平均 = O(N logN),这是怎么计算出来的呢?

首先是代码中的两个循环:

  1. 建堆阶段:

    首先执行一个外层循环,这个循环会遍历非叶子节点。在每次循环内,adjust() 函数会递归调用,对堆进行调整

    adjust() 本身的时间复杂度是O(log n),因为它在树的高度上进行操作

    这个外层循环的时间复杂度是O(n/2)

    因此,建堆阶段的总时间复杂度是O(n/2 * log n) = O(n * log n)

  2. 排序阶段:

    有一个外层循环,这个循环从数组的最后一个元素向前遍历。在每次循环内,执行一次 swap() ,交换堆顶元素和当前循环的元素,然后调用 adjust() 函数对剩余的元素进行堆调整

    这个外层循环执行了 n - 1 次,每次执行 adjust 的时间复杂度是O(log n),因此排序阶段的总时间复杂度是O((n - 1) * log n) = O(n * log n)

所以代码的总复杂度为2O(log n),也就是O(log n)级别

那为什么说它的 最好时间复杂度 = 最坏 = 平均呢?

无论输入数据是否有序、随机、或其他分布情况,都需要进行堆的构建和维护,所以它的性能在不同情况下表现都一致

这使得堆排序相对稳定,但它也有一些局限性,比如不适用于小规模数据或部分有序的数据,因为它在这些情况下的性能可能不如其他排序算法

posted @ 2022-08-11 15:34  YaosGHC  阅读(42)  评论(0)    收藏  举报