一.堆排序的简单讲解
是一种基于二叉堆数据结构的比较排序算法。它利用了堆的性质来高效地完成排序任务。堆排序的主要思想是将待排序的数组构建成一个最大堆(或最小堆),然后通过不断提取堆顶元素(最大值或最小值)并重新调整堆,最终实现整个数组的有序排列。
个人感觉堆排序就是采用了先建一个大堆,然后使得堆顶和数组的尾部进行交换,然后堆再进行向下调整,不断进行,使得整个数组都变为升序。
基本原理
1. 堆的数组表示
二叉堆可以用数组表示,其中:
-
索引为
i
的节点的左子节点索引为2 * i + 1
。 -
索引为
i
的节点的右子节点索引为2 * i + 2
。 -
索引为
i
的节点的父节点索引为(i - 1) / 2
。
这种表示方式使得堆操作可以在数组上高效实现。
2. 堆操作
堆排序的核心操作包括:
-
下沉操作:将一个节点下沉到合适的位置,以确保子树满足堆的性质。
-
上浮操作:将一个节点上浮到合适的位置,以确保子树满足堆的性质(堆排序中较少使用)。
-
构建堆:将一个无序数组构建成一个最大堆。
-
提取堆顶元素:移除并返回堆顶元素(最大值或最小值),并重新调整堆。
二.代码实现
首先,我个人是喜欢先实现向上调整和向下调整,向上调整就是比较子节点和父节点的问题,如果子节点比父节点大,就进行交换,一直到堆顶 n = 0;
void up(int* an, int n)
{
while (n)
{
if (an[n] > an[n / 2])
{
int x = an[n];
an[n] = an[n / 2];
an[n / 2] = x;
n = n / 2;
}
else
{
break;
}
}
}
向下调整,就是和自己最大的子节点比较,如果子节点大于父节点,则进行交换,反正则调整结束,但是要注意,这里可能存在只有一个孩子的情况,这个要单独拿出来比较
void down(int* an, int n, int z)
{
if (n * 2 + 1 == z)
{
int g = n * 2 + 1;
if (an[n] < an[g])
{
int x = an[n];
an[n] = an[g];
an[g] = x;
n = g;
}
}
while (n*2+1 < z)
{
int g;
if (an[n * 2 + 1] > an[n * 2 + 2])
{
g = n * 2 + 1;
}
else
{
g = n * 2 + 2;
}
if (an[n] < an[g])
{
int x = an[n];
an[n] = an[g];
an[g] = x;
n = g;
}
else
{
break;
}
}
}
然后开始排序,首先是建堆,建堆就是进行最后一个叶子节点进行向下调整,直到所有的非叶子节点都进行向下调整以后,堆也就建好了,建好堆以后,肯定会得到一个最大值,在顶端,然后将最大值和数组尾部的值进行交换,然后向下调整,直到完成结束,就实现了堆排序
void swap(int& x, int& y)
{
int z = x;
x = y;
y = z;
}
void heapsort(int* an, int n)
{
int x = n / 2 -1;
while (x >= 0)
{
down(an, x,n-1);
x--;
}
for (int i = 0; i < n-1; i++)
{
swap(an[0], an[n - i-1]);
down(an, 0, n - i - 2);
}
}
三.堆排序的性质
1. 时间复杂度
-
构建最大堆:时间复杂度为 O(n)。
-
提取堆顶元素并调整堆:每次调整堆的时间复杂度为 O(log n),总共需要进行 n-1 次。
-
总时间复杂度:O(n log n)。
2. 空间复杂度
-
堆排序是原地排序算法,不需要额外的存储空间,空间复杂度为 O(1)。
3. 稳定性
-
堆排序是不稳定的排序算法。在堆化过程中,元素的相对顺序可能会改变。