动画:一篇文章快速学会堆排序

514 阅读8分钟

内容介绍

堆排序简介

前面我们已经介绍了堆结构,也能够构造一个堆结构。如果对堆结构不了解的同学可以看一下之前的文章《动画学堆结构,一篇一看就懂的堆结构文章》。我们稍微回顾一下,根节点最大的堆叫做最大堆大根堆,如下图:

获取最大堆的最大值,其实就是获取堆顶的元素。对于堆这种数据结构通常是将堆顶的元素和堆中最后一个元素换位置,最大值就到了最后一个位置,然后从堆中排除这个最大的元素,当最后一个元素交换到最前面时,此时就不满足堆的性质了,我们需要将最前面这个元素通过ShiftDown(下沉)的手段让堆继续满足堆的规则。

堆获取最大值可以分成两个步骤:

  1. 将堆中最前面的最大值和最后一个元素交换位置。
  2. 使用ShiftDown让最前面的元素下沉到合适的位置,依然满足堆的性质。
    动画演示效果如下:

堆排序的思想

我们知道堆顶的元素就是堆中的最大值,因此我们每次从堆中取出并移除最大值(其实是将堆顶的最大值移动到了堆的最后面),然后ShiftDown使这个二叉树依然满足堆的规则,堆中第二大的数据就会到堆顶,再次取出堆顶的最大值,其实就是所有数据中的第二大值,依次类推,于是就完成了堆排序。

堆排序动画演示

一般没有特殊要求排序算法都是升序排序,小的在前,大的在后。数组由{5, 3, 1, 9, 7, 2, 8, 6} 这8个无序元素组成。

堆排序分析



如上图所示,当我们将堆顶的最大值9移除后,堆中的第二大值8就会到堆顶来。当我们再次将8移除后,堆中剩余元素的最大值会到堆顶来。

堆删除8后的效果:

堆删除7后的效果:

堆删除6后的效果:

堆删除5后的效果:

堆删除4后的效果:

堆删除3后的效果:

堆删除2后的效果:

堆删除1后的效果:

我们可以看到堆排序算法的步骤:

  1. 把无序二叉树构建成二叉堆。
  2. 循环删除堆顶元素,移到数组尾部,调整二叉堆,得到新堆中的最大值放到堆顶。

堆排序代码编写

public class HeapSort {
    public static void main(String[] args) {
        int[] arr = {637582149};

        heapSort(arr);
        System.out.println("堆排序后:" + Arrays.toString(arr));
    }

    /**
     * 堆排序
     * @param arr 待排序的数组
     */

    public static void heapSort(int[] arr) {
        heapify(arr);
        System.out.println("构建堆:" + Arrays.toString(arr));

        // 让堆顶元素和堆最后一个元素交换,其实就是数组最前面数据,和后面的数据交换
        for (int i = arr.length-1; i > 0; i--) {
            swap(arr, i, 0);
            shiftDown(arr, 0, i);
        }
    }

    /**
     * heapify将无序的完全二叉树调整为二叉堆
     * @param arr 待调整的数组
     */

    private static void heapify(int[] arr) {
        // 从非叶子节点开始,Shift Down将每个子树构建成最大堆
        for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
            shiftDown(arr, i, arr.length);
        }
    }

    /**
     * 下沉操作,将指定元素下沉到子树的合适位置,使这个颗树满足堆的规则。
     * @param arr 待调整的数组
     * @param index 要下沉的元素索引
     * @param count 堆的有效范围
     */

    private static void shiftDown(int[] arr, int index, int count) {
        // 下沉操作时减少赋值,先保存这个要下沉的元素,后面找到合适位置直接交换
        int temp = arr[index];
        // j表示左孩子索引
        int childIndex = 2*index + 1;
        // 循环找子孩子交换位置。左孩子不能越界
        while (childIndex < count) {

            // 判断是否有有孩子,并且右孩子是否大于左孩子
            if (childIndex+1 < count && arr[childIndex+1] > arr[childIndex]) {
                childIndex++; // 如果是,和右孩子交换
            }

            // 如果当前节点大于两个孩子,就不需要交换
            if (temp > arr[childIndex])
                break;

            // 当前节点小于子孩子,将当前节点和较大的子孩子交换
            // 无需真正交换,记录这个要交换的索引
            arr[index] = arr[childIndex];

            // 再判断下一层
            index = childIndex;
            childIndex = 2*index + 1;
        }
        arr[index] = temp;
    }

    public static void swap(int[] arr, int start, int end) {
        if (start == end) return;

        int temp = arr[start];
        arr[start] = arr[end];
        arr[end] = temp;
    }
}

堆排序的复杂度

堆排序的时间复杂度:堆排序的运行时间主要是消耗在开始构建堆和在取出最大值后重建堆时的数据下沉上。在构建堆的过程中,是从完全二叉树最后一个非叶子节点开始构建,最后一个非叶子节点为(n-1)/2,将它与其孩子进行比较和互换,对于每个非叶子节点最多会进行两次比较,所以构建堆的时间复杂度为0(n)。排序时,堆顶元素需要和堆中最后一个有效元素交换位置并下沉,时间复杂度是O(logi),并且有n-1次获取堆顶最大值的过程因此,在依次获取最大值时的时间复杂度是O(nlogn)。构建堆和获取堆顶最大值排序是两个前后操作,因此堆排序总体的时间复杂度是O(nlogn)。

我们可以看到堆排序对待排序的数据不敏感,无论数据怎么样,堆排序的最好,最坏,平均时间复杂度都是O(nlogn),没有优化的空间,因此真正在排序的时候不会选择堆排序,而是选择优化性能更好的快速排序。

堆排序的空间复杂度:在堆排序的过程中只需要一个额外的变量记录要交换的数据,因此堆排序的空间复杂度为O(1)。堆排序在得到一个最大值后,会让堆顶的元素下沉到合适的位置,因此堆排序是不稳定的排序算法。

总结

堆排序的过程:

  1. 将一颗完全二叉树构建成堆
  2. 循环获取堆顶的最大值,放到堆的后面,并重建堆。

---------- End ----------
原创文章和动画制作真心不易,您的点赞就是最大的支持!

想了解更多文章请关注微信公众号:表哥动画学编程