堆排序
1. 算法原理
堆排序的原理与部分没听说过该排序方法的同学猜想的不同,它并不是将堆顶元素逐一弹出后压入数组中来得到一个有序的数组。这种方法在初始建堆时需要使用一个数组空间,在得到有序的数组时需要使用另一个数组空间。而堆排序则是在初始数组的基础上直接进行排序,相较于上一种方法节省了一倍的空间。
堆排序的做法时:
以从小到大排序数组为例,首先使用初始数组建立一个大顶堆,记作a[1…n],然后将堆顶元素a[1]与堆尾元素a[n]进行交换,将此次操作看作是一次堆顶元素弹出操作,据此调整堆数组范围和元素位置,记新堆为a[1…n - 1]。不断重复以上操作,直到堆中只剩一个元素,最终的数组便排好了序。
2. 算法模拟
对于数组:3 5 8 4 3 2 10 1 9,进行从小到大排序
由图可知,堆排序仅仅使用了建堆时的数组空间,最终排序结果仍存储在该数组中。
3. 线性建堆法
知道了堆排序的算法原理,但如何将一个待排序数组建成一个堆呢?
在这里简单介绍一下线性建堆法。
线性建堆法的算法原理是:将当前数组看成一个完全二叉树,再将该完全二叉树从后到前一次性扫描所有拥有子节点的节点,最终将数组调整成堆。
4. 算法分析
(1)排序稳定性:
对于数组a[1…n],假设第3~第n号元素已排序好,且a[1] = a[2],则对于第n - 1次循环,a[1]将与a[2]互换,且在第n次循环时循环结束,此时a[1]与a[2]不满足排序前的相对位置,即最终排序好的数组不满足排序稳定性,因此堆排序是不稳定的。
(2)时间复杂度:
建堆时,需要深搜遍历完全二叉树的每一个节点,并按照堆性质进行调整,即需要遍历整个数组,时间复杂度为O(n)
弹出后调整堆时,只需要对一个节点按照堆性质进行调整,即调整堆层数次数,时间复杂度为O(logn)
每次弹出后都需要进行调整,共弹出n次、调整n次。
因此,堆排序算法的时间复杂度为: O ( n log n ) O(n\log n) O(nlogn)
5. 代码演示
// 线性建堆
int* buildheap(int n, int* a) // n为数组元素个数,a为数组,函数返回建成堆的数组
{
for(int i = n / 2; i >= 1; --i)
{
int j = i;
while(j <= n / 2) // 限定节点有子节点,不断dfs
{
if(a[j] < a[2 * j] || (a[j] < a[2 * j + 1] && 2 * j + 1 <= n)) // 节点向下调整
{
int ind = 0;
if(a[2 * j] < a[2 * j + 1] && 2 * j + 1 <= n)
{
ind = 2 * j + 1;
}
else
{
ind = 2 * j;
}
swap(a[j], a[ind]);
j = ind;
}
else // 节点无需向下调整,break
break;
}
}
return a;
}
// 堆排序
void heap_sort(int n, int* a) // n为数组元素个数,a为数组,函数返回排好序的数组
{
a = buildheap(n, a);
int num = n; // num用来控制当前堆中元素个数
for(int i = n; i > 1; --i) // 只需执行n - 1次循环
{
swap(a[i], a[1]);
num -= 1;
if(num == 1) // 当本次循环堆中只有一个元素时,直接结束即可
break;
int j = 1;
while(j <= num / 2)
{
if(a[j] < a[2 * j] || (a[j] < a[2 * j + 1] && 2 * j + 1 <= num))
{
int ind = 0;
if(a[2 * j] < a[2 * j + 1] && 2 * j + 1 <= num)
{
ind = 2 * j + 1;
}
else
{
ind = 2 * j;
}
swap(a[j], a[ind]);
j = ind;
}
else
break;
}
}
return;
}
标签:int,元素,堆排序,算法,数组,ind,排序 From: https://blog.csdn.net/weixin_71402055/article/details/140232530