📂排序
🔖排序
2022-08-11 15:34阅读: 33评论: 0推荐: 0

结合代码 图解 堆排序

参考链接

堆排序

概述

堆排序是利用 这种数据结构进行排序的一种排序算法,堆排序是一种选择排序(每次选出序列中 最大 / 最小 元素)
它的最好和最坏时间复杂度都是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)级别

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

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

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

本文作者:YaosGHC

本文链接:https://www.cnblogs.com/yaocy/p/16576230.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   YaosGHC  阅读(33)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起