「前言」文章内容是排序算法之堆排序的讲解。(所有文章已经分类好,放心食用)
「归属专栏」排序算法
「主页链接」个人主页
「笔者」枫叶先生(fy)
概念介绍
堆是一种特殊的树形数据结构,它满足以下两个性质:
堆排序是一种基于二叉堆数据结构的排序算法,堆排序一般都是使用数组(顺序表)的结构进行排序(顺序存储)
堆排序算法的核心就是利用堆的性质来实现排序,堆这里就不详细介绍了(在数据结构——堆中已经详细介绍)
堆排序采用的是堆的向下调整算法(为什么选这个,在数据结构——堆中已经详细介绍)
堆排序的构建步骤
注意:需要注意的是排升序要建大堆,排降序建小堆
下面介绍堆的向下调整
堆向下调整算法的基本思想是将堆中的某个节点按照堆的性质向下调整,使得以该节点为根的子树重新成为一个堆。具体步骤如下:
通过这样的向下调整操作,可以保持堆的性质不变,并且在插入或删除节点之后,可以快速地恢复堆的性质
例如(以小堆为例)
注意:向下调整算法有一个前提:左右子树必须是一个堆,才能调整
降序:小堆
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
// 堆的向下调整(下面是小堆:降序)
void AdjustDown(int* arr, int n, int parent) // n:arr数组的大小; parent:父节点的数组下标
{
// 左子节点下标为parent * 2 + 1; 右子节点的下标为parent * 2 + 2
int child = parent * 2 + 1; // 假设左孩子较大
//
while (child < n)
{
// 选出左右孩子中小的那个
if ((child + 1 < n) && arr[child] > arr[child + 1]) // child + 1:右孩子的下标;-- 右孩子存在,并且左孩子比右孩子大
{
++child;
}
// 孩子跟父亲比较
if (arr[child] < arr[parent])
{
Swap(&arr[child], &arr[parent]); // 交换数据
//迭代
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
注:若父节点下标为parent
,左子节点下标为parent * 2 + 1
,右子节点的下标为parent * 2 + 2
如果要改为升序,要建大堆
修改一下,判断条件即可
// ...
if ((child + 1 < n) && arr[child] < arr[child + 1])
// ...
if (arr[child] < arr[parent])
// ...
向下调整的时间复杂度计算
h-1
次(h为树的高度)h = log(N+1)
(N为树的总结点数,log以2为底)O(logN)
前面说到,使用堆的向下调整算法需要满足其根结点的左右子树均为大堆或是小堆才行,那么如何才能将一个任意树调整为堆呢?
只需要从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整即可
// 1.建堆
// 向下调整建堆方式时间复杂度:O(N)
// 从第一个非叶子结点开始向下调整,一直到根
for (int i = (n - 1 - 1) / 2; i >= 0; --i) // n-1:数组下标上限,(n-1-1)/2:孩子的父亲节点
{
AdjustDown(arr, n, i);
}
以建小堆为例,如图所示:
堆排序分两步:
堆排序代码如下:
//堆排序
void HeapSort(int* arr, int n) // n:数组大小
{
// 1.建堆
// 向下调整建堆方式时间复杂度:O(N)
// 从第一个非叶子结点开始向下调整,一直到根
for (int i = (n - 1 - 1) / 2; i >= 0; --i) // n-1:数组下标上限,(n-1-1)/2:孩子的父亲节点
{
AdjustDown(arr, n, i);
}
// 2.调整(排序的过程)
// 调整时间复杂度:O(N*logN)
int end = n - 1; // 记录堆的最后一个数据的下标
while (end > 0)
{
// 堆顶数据与堆的最后一个数据交换
Swap(&arr[0], &arr[end]);
// 进行调整
AdjustDown(arr, end, 0); // 对根进行一次向下调整
end--; // 堆的最后一个数据的下标减一
}
}
堆排序的时间复杂度计算
建堆的时间复杂度:
因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)
计算建堆过程中总共交换的次数:
① T ( n ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + . . . + 2 h − 3 ∗ 2 + 2 h − 2 ∗ 1 ① T(n)=2^0*(h-1)+2^1*(h-2)+2^2*(h-3)+...+2^{h-3}*2+2^{h-2}*1 ①T(n)=20∗(h−1)+21∗(h−2)+22∗(h−3)+...+2h−3∗2+2h−2∗1
两边同时乘2得:(等差数列乘以一个等比数列,使用裂项相消法进行化简)
② 2 T ( n ) = 2 1 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + 2 2 ∗ ( h − 3 ) + . . . + 2 h − 2 ∗ 2 + 2 h − 1 ∗ 1 ②2 T(n)=2^1*(h-1)+2^2*(h-2)+2^2*(h-3)+...+2^{h-2}*2+2^{h-1}*1 ②2T(n)=21∗(h−1)+22∗(h−2)+22∗(h−3)+...+2h−2∗2+2h−1∗1
②-①两式相减得:(错位相减)
T ( n ) = 1 − h + 2 1 + 2 3 + . . . + 2 h − 2 + 2 h − 1 T(n)=1-h+2^1+2^3+...+2^{h-2}+2^{h-1} T(n)=1−h+21+23+...+2h−2+2h−1
T ( n ) = 2 0 + 2 1 + 2 3 + . . . + 2 h − 2 + 2 h − 1 − h T(n)=2^0+2^1+2^3+...+2^{h-2}+2^{h-1}-h T(n)=20+21+23+...+2h−2+2h−1−h
运用等比数列求和得:
T ( n ) = 2 h − h − 1 T(n)=2^h-h-1 T(n)=2h−h−1
由二叉树的性质,有 N = 2 h − 1 N=2^h-1 N=2h−1和 h = l o g 2 ( N + 1 ) h=log_2(N+1) h=log2(N+1),于是:
T ( n ) = N − l o g 2 ( N + 1 ) ≈ N T(n)=N-log_2(N+1)≈N T(n)=N−log2(N+1)≈N
用大O的渐进表示法,即:
T ( n ) = O ( N ) T(n)=O(N) T(n)=O(N)
因此:建堆的时间复杂度为O(N)
排序时间复杂度:
O(logN)
O(nlogn)
堆排序的总时间复杂度为
O(n + nlogn) = O(nlogn)
O(nlogn)
,最坏情况下也为 O(nlogn)
O(1)
--------------------- END ----------------------
「 作者 」 枫叶先生
「 更新 」 2024.1.11
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
或有谬误或不准确之处,敬请读者批评指正。