学习了一下堆排序的思想,分享一下我的理解。

首先介绍一些概念。

堆(heap),最大堆(max heap),最小堆(min heap)

堆是一种特别的树状结构,普通的树结构,没有对子节点也特别的规定,但堆是一颗完全的树,除了最底层,上面的每一层都是满的。

如果一个堆中所有的节点,它有用子节点的话,并且这个节点大于它的子节点,那么这个堆就是最大堆

如果一个堆中所有的节点,它有用子节点的话,并且这个节点小于它的子节点,那么这个堆就是最小堆

堆的数据结构与特征

堆是一种树结构,但是我们却可以使用数组来表示,因为它是一个完全二叉树。所以我们给定一个数组,比如:[1, 2, 3, 4, 5, 6, 7, 8],就可以构造一个堆。

构造的方法,在图纸上,很简单,按顺序,以树的结构书写数字就好了。

       1
     /   \
    2     3
   / \   / \
  4   5 6   7
 /  
8

因此,它可以直接用数组来表示。

用数组表示的时候,index有一些特征。
节点index = i,它的左子节点的index = 2 * i + 1,它的右子节点的index = 2 * i + 2
如果数组长度为length,那么,它最后一个拥有子节点的节点index = length / 2 - 1

堆排序的基本流程

1.将给定的数组按照堆去理解(或者说构造成堆也行)
2.将这个堆进行调整,调整为大根堆或者小根堆,接下来,我们将堆的顶端的元素拿走放进一个新队列中保存起来,将最后一个元素放在堆的根节点上。
3.重复2
在每次对堆进行调整后,都可以得到最大值or最小值,每次取走这个值,堆会越变越小,最后也就排好了。

接下来我用一个图来表示堆排序的过程:
图片描述

算法的实现

注释我写的非常清楚了,大家自己看下理解一下吧。

enum SortType {
    Increase, Decrease
}

/**
 * 堆排序
 * @param input 输入数据
 * @param sortType 排序类型 升序还是降序
 * */
public static int[] heapSort(int[] input, SortType sortType) {

    int[] heap = input;
    int heapLength = input.length; // 初始化堆的大小,每次找到一个数后,堆的大小都减少1,不去操作最后一个已经排好的数
    for (int i = 0; i < input.length; i++) {
        heap = maxHeapify(heap, heapLength, sortType); // 进行堆调整,只调整heapLength的区域
        swap(heap, 0, heapLength - 1); // 调整完成后,将第一个元素后最后一个元素交换
        heapLength--; // 堆的大小减一
    }

    return heap;
}

/**
 * 堆调整
 * */
public static int[] maxHeapify(int[] heap, int heapLength, SortType sortType) {
    if (heap == null) {
        return null;
    }

    if (heap.length == 1) {
        return heap;
    }

    // 标记是否改变过,如果改变了,需要再次执行本方法,直到没有变化为止
    boolean isChange = false;

    for (int i = heapLength / 2 - 1; i >= 0; i--) { // 从最后一个有children的节点开始,向前遍历

        int leftIndex = i * 2 + 1; // left child index
        int rightIndex = i * 2 + 2; // right child index

        if (heapLength > leftIndex) {
            if (sortType == SortType.Increase) {
                if (heap[i] < heap[leftIndex]) { // 判断当前节点和左节点的大小,进行交换
                    swap(heap, i, leftIndex);
                    isChange = true; // 每次交换,需要标记,发生了改变,这个过程需要再来一次
                }
            } else if (sortType == SortType.Decrease) {
                if (heap[i] > heap[leftIndex]) {
                    swap(heap, i, leftIndex);
                    isChange = true;
                }
            }

            if (heapLength > rightIndex) { // 处理右节点,如果存在的话
                if (sortType == SortType.Increase) {
                    if (heap[i] < heap[rightIndex]) {
                        swap(heap, i, rightIndex);
                        isChange = true;
                    }
                } else if (sortType == SortType.Decrease) {
                    if (heap[i] > heap[rightIndex]) {
                        swap(heap, i, rightIndex);
                        isChange = true;
                    }
                }
            }
        }
    }

    if (isChange) {
        // 如果改变了,需要再来一次
        return maxHeapify(heap, heapLength, sortType);
    } else {
        // 如果每个节点都OK了,返回
        return heap;
    }
}

public static void swap(int[] arr, int i, int j) {
    int v = arr[i];
    arr[i] = arr[j];
    arr[j] = v;
}

最后测试一下效果:

用了三组数据来测试,一组最差的情况,一组最好的情况,另外一组随机的情况

public static void main(String[] args) {

    int loopCount = 100000;

    int[] result = null;

    long t = System.currentTimeMillis();
    for (int i = 0; i < loopCount; i++) {
        result = heapSort(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, SortType.Decrease);
    }
    System.out.println(System.currentTimeMillis() - t);
    System.out.println(Arrays.toString(result));

    t = System.currentTimeMillis();
    for (int i = 0; i < loopCount; i++) {
        result = heapSort(new int[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, SortType.Increase);
    }
    System.out.println(System.currentTimeMillis() - t);
    System.out.println(Arrays.toString(result));

    int[] arr = new int[10];
    for (int i = 0; i < 10; i++) {
        arr[i] = (int) (Math.random() * 100);
    }

    t = System.currentTimeMillis();
    for (int i = 0; i < loopCount; i++) {
        result = heapSort(arr, SortType.Increase);
    }
    System.out.println(System.currentTimeMillis() - t);
    System.out.println(Arrays.toString(result));
}

运行结果:

可以看到,随机的情况,耗时最少,最差的情况,耗时也很少,最好的情况,反而耗时最多。大概和调整为最大堆和最小堆的时候有关。当我需要从小到大排列的时候,我反而需要将排好的数据构造成最大堆,然后再取出最大的数据放到数组最后。

36 // 最差的情况
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
49 // 最好的情况
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
20 // 随机生成的数据
[4, 5, 11, 21, 22, 44, 66, 79, 93, 99]

如果使用额外的空间来存储,不关心最大堆还是最小堆,会怎么样呢?

总是得到最小堆,然后再用额外的空间来存储结果。

/**
 * 堆排序
 * @param input 输入数据
 * @param sortType 排序类型 升序还是降序
 * */
public static int[] heapSort(int[] input, SortType sortType) {

    if (input == null || input.length == 0) {
        return null;
    }

    int[] heap = input;
    int[] result = new int[input.length];
    int heapLength = input.length; // 初始化堆的大小,每次找到一个数后,堆的大小都减少1,不去操作最后一个已经排好的数
    for (int i = 0; i < input.length; i++) {
        heap = maxHeapify(heap, heapLength, SortType.Decrease); // 进行堆调整,只调整heapLength的区域

        // Decrease 排序的话,总是得到最小堆
        if (sortType == SortType.Increase) {
            result[i] = heap[0];
        } else {
            result[input.length - i - 1] = heap[0];
        }

        swap(heap, 0, heapLength - 1); // 调整完成后,将第一个元素后最后一个元素交换
        heapLength--; // 堆的大小减一
    }

    return result;
}

运行结果:

33
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
32
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
23
[12, 13, 18, 21, 38, 72, 73, 86, 91, 97]

最好情况所花的时间确实减少了,但对平均情况来说却没什么影响的,总得来说平均情况是非常稳定的。


krosshj
152 声望16 粉丝

Developer, Gamer, Artist