扫码关注公众号,获取更多内容
目录
堆排序的时间复杂度非常稳定,是O(nlogn),并且还是原地排序算法,我们可以把堆排序的过程大致分解为两个步骤:建堆和排序。
一、建堆
首先将数组原地(不借助另外一个数组,就在原数组上操作)建成一个堆,建堆有两种思路:
第一种:借助在堆中插入一个元素的思路,数组中有n个数据,我们可以假设起初堆中只有一个数据(下标为1的数据)。然后我们调用上一节讲的插入操作,将下标为2到n的数据依次插入到堆中。这样就将包含n个数据的数组,组织成了堆。
第二种:第一种的建堆的处理过程是从前往后处理数据,并且每个数据插入堆中时,都是从下往上堆化。第二种实现思路是从后往前处理数组,并且每个数据都是从上往下堆化。
如下图所示,因为叶子节点往下堆化只能自己跟自己比较,所以我们直接从最后一个非叶子节点开始,依次堆化就行了。
代码实现如下:
private static void buildHeap(int[] a, int n) {
for (int i = n/2; i >= 1; --i) {
heapify(a, n, i);
}
}
private static void heapify(int[] a, int n, int i) {
while (true) {
int maxPos = i;
if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;
if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;
if (maxPos == i) break;
swap(a, i, maxPos);
i = maxPos;
}
}
上方代码中,对下标从n/2到1的数据进行堆化,从上方图中我们可以观察到,n/2+1 到 n的节点是叶子结点,我们不需要堆化。实际上,对于完全二叉树来说,下标从n/2+1到n的节点都是叶子结点。
二、排序
建堆完成后,数组中的数据已经符合大顶堆的特征,数组中的第一个元素就是堆顶,也就是最大的元素,我们把它跟最后一个元素交换,那最大元素就放到了下标为n的位置。这个过程有点类似“删除堆顶元素”的操作,当堆顶元素移除以后,我们把下标为n的元素放到堆顶,然后再通过堆化的方法,将剩下的n-1个元素重新构建成堆。堆化完成之后,我们再取堆顶的元素,放到下标是n-1的位置,一直重复这个过程,直到最后堆中只剩下标为1的一个元素,排序工作就完成了。
代码如下所示:
// n表示数据的个数,数组a中的数据从下标1到n的位置。
public static void sort(int[] a, int n) {
buildHeap(a, n);
int k = n;
while (k > 1) {
swap(a, 1, k);
--k;
heapify(a, k, 1);
}
}
整个堆排序的过程,都只需要极个别临时存储空间,所以堆排序是原地排序算法。堆排序包括建堆和排序两个操作,建堆过程的时间复杂度是O(n),排序过程的时间复杂度是O(nlogn),所以堆排序的整体时间复杂度是O(nlogn)。