【Java】选择排序,堆排序(动图详解和性能比较)

上一组参赛选手:
👉插入排序,希尔排序

1️⃣必备排序常识

稳定性:在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求能在内存和硬盘(外部存储器)之间移动数据的排序。
时间复杂度:一个排序算法在执行过程中所耗费的时间量级的度量。
空间复杂度:一个排序算法在运行过程中临时占用存储空间大小的度量。

本次讲解的排序都是内排序

2️⃣选择排序

1.单路选择排序

排序原理:

每一次从待排序的数据元素中选出最小(或最大)的一个元素,排在有序元素后,然后再从剩余的未排序元素中寻找到最小(大)元素,继续排在已排序元素后,直到数组整个有序。

过程展示:

代码实现:

public static void selectionSort(int[] arr) {
    for (int i = 0; i < arr.length; i++) {
        // min变量存储了当前的最小值索引
        int min = i;
        // 从剩下的元素中选择最小值
        for (int j = i; j < arr.length; j++) {
            if(arr[j] < arr[min]){
                min = j;
            }
        }
        // min这个索引一定对应了当前无序区间中找到的最小值索引,换到无序区间最前面i
        swap(arr,min,i);
    }
}

特性总结:

  1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
  2. 时间复杂度:O(n^2)
  3. 空间复杂度:O(1)
  4. 稳定性:不稳定。交换值的同时可能将其他相同值的位置改变了。
    例: 4 9 5 5 8 5 6 9 -> 4 9 5 9 8 5 6 5
    此时就有相同值的位置顺序改变了

2.双路选择排序

在遍历未排序元素时,可以一次找出最大和最小的元素,将其放在前(后)已排序元素的后(前)即可,当前后遍历的下标相同时,说明整个数组就有序了。

//双路选择排序
public static void selectionSortOp(int[] arr){
    int left = 0;
    int right = arr.length - 1;
    // low = high,无序区间只剩下一个元素,整个数组已经有序
    while(left < right){
        int min = left;
        int max = left;
        for (int i = left; i <= right; i++) {
            if(arr[min] > arr[i])
                min = i;
            if(arr[max] < arr[i])
                max = i;
        }
        // min索引一定是当前无序区间的最小值索引,与low交换位置
        swap(arr,min,left);
        if(max == left)
            // 最大值已经被换到min这个位置
            max = min;
        swap(arr,max,right);
        left++;
        right--;
    }
}

3.性能比较

测试使用的类:

/**
 * 排序的辅助类
 * 生成测试数组以及对排序算法进行测试
 **/
public class SortHelper {
    // 获取随机数的对象
    private static final ThreadLocalRandom random = ThreadLocalRandom.current();
    
    //在[left...right]上生成n个随机数
    public static int[] generateRandomArray(int n,int left,int right) {
        int[] arr = new int[n];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = random.nextInt(left,right);
        }
        return arr;
    }

    /**
     * 生成一个大小为n的近乎有序的数组
     * @param n
     * @param times 交换的次数,次数越小越有序,次数越大越无序
     * @return
     */
    public static int[] generateSoredArray(int n,int times) {
        int[] arr = new int[n];
        for (int i = 0; i < n; i++) {
            arr[i] = i;
        }
        // 交换部分元素,交换次数越小,越有序
        for (int i = 0; i < times; i++) {
            // 生成一个在[0..n]上的随机数
            int a = random.nextInt(n);
            int b = random.nextInt(n);
            int temp = arr[a];
            arr[a] = arr[b];
            arr[b] = temp;
        }
        return arr;
    }
    // 根据传入的方法名称就能调用这个方法,需要借助反射
    // 根据方法名称调用相应的排序方法对arr数组进行排序操作
    public static void testSort(String sortName,int[] arr) {
        Class<SevenSort> cls = SevenSort.class;
        try {
            Method method = cls.getDeclaredMethod(sortName,int[].class);
            long start = System.nanoTime();
            method.invoke(null,arr);
            long end = System.nanoTime();
            if (isSorted(arr)) {
                // 算法正确
                System.out.println(sortName + "排序结束,共耗时:" + (end - start) / 1000000.0 + "ms");
            }
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    // 生成一个arr的深拷贝数组
    // 为了测试不同排序算法的性能,需要在相同的数据集上进行测试
    public static int[] arrCopy(int[] arr) {
        return Arrays.copyOf(arr,arr.length);
    }

    public static boolean isSorted(int[] arr) {
        for (int i = 0; i < arr.length - 1; i++) {
            if (arr[i] > arr[i + 1]) {
                System.err.println("sort error");
                return false;
            }
        }
        return true;
    }
}

排序十万个随机数时:

public static void main(String[] args) {
        int n = 100000;
        int[] arr = SortHelper.generateRandomArray(n, 0, Integer.MAX_VALUE);
        int[] arrCopy1 = SortHelper.arrCopy(arr);
        
        SortHelper.testSort("selectionSort", arr);
        SortHelper.testSort("selectionSortOp", arrCopy1);
}

运行结果:

因为选择排序是O(n^2)的时间复杂度,所以排序起来非常慢,实际生活中也不怎么使用。

排序十万个接近有序数组时:

int n = 100000;
//将有序的数组元素进行随机交换100次就可构成一个接近有序的数组
int[] arr = SortHelper.generateSoredArray(n,100);

运行结果:

可以看出,因为选择排序稳定的O(n^2)的时间复杂度,即使排序接近有序的数组时也没有优势,甚至会更慢。

3️⃣堆排序

排序原理:

堆排序(Heap Sort)是利用堆进行排序的方法。其基本思想为:将待排序列构造成一个大堆(或小堆),整个序列的最大值(或最小值)就是堆顶的根结点,将根节点的值和堆数组的末尾元素交换,此时末尾元素就是最大值(或最小值),然后将剩余的n - 1个序列(数组末尾已排序元素除外)重新构造成一个堆,这样就会得到n - 1个元素中的次大值(或次小值),如此反复执行,最终得到一个有序序列。

堆有很多种存储形式,这里使用的堆是用数组表示的完全二叉树。要学习堆排序,首先要学习堆的向下调整算法,因为要用堆排序先得建堆,而建堆需要执行多次堆的向下调整算法。

堆的向下调整算法(使用前提)
大堆:堆中根结点值 >=子树中的结点值
小堆:堆中根结点值<=子树中的结点值
若想将其调整为小堆,那么根结点的左右子树必须都为小堆。
若想将其调整为大堆,那么根结点的左右子树必须都为大堆。

向下调整算法的基本思想(以建大堆为例)
 1.从根结点处开始,选出左右孩子中值较大的孩子。
 2.让大的孩子与其父亲进行比较。
若大的孩子比父亲还大,则该孩子与其父亲的位置进行交换。并将原来大的孩子的位置当成父亲继续向下进行调整,直到调整到叶子结点为止。
若大的孩子比父亲小,则不需处理了,调整完成,整个树已经是大堆了。

使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。而h = log2(n+1)(n为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logn)
此时需要找出最后一个有叶子结点的父结点,n为结点的总个数,(n - 2) / 2就是满足条件的下标,并以该结点从下往上依次向下调整。最后就可以建成一个大堆。
在这里插入图片描述

整个排序过程:

代码实现:

public static void heapSort(int[] arr){
    // 先将arr进行heapify调整为最大堆
    // 从最后一个非叶子节点开始进行siftDown操作
    for (int i = (arr.length - 1 - 1) >> 1; i >= 0; i--) {
        siftDown(arr,i,arr.length);
    }
    // 此时arr就被我调整为最大堆
    for (int i = arr.length - 1; i >= 0; i--) {
        // arr[0] 堆顶元素,就是当前堆的最大值
        swap(arr,i,0);
        siftDown(arr,0,i);
    }
}

/**
 * 元素下沉操作
 * @param arr
 * @param i 当前要下沉的索引
 * @param length 数组长度
 */
private static void siftDown(int[] arr, int i,int length) {
    int child = 2 * i + 1;
    if (child >= length) {
        return;
    }
    if (child + 1 < length && arr[child + 1] > arr[child]) {
        child++;
    }
    //child就是左子树最大的索引
    if (arr[i] < arr[child]) {
        swap(arr,i,child);
    }
    //将子结点赋值给需要下沉的索引,向下递归,直到下沉完毕
    i = child;
    siftDown(arr,i,length);
}

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

特性总结:

  1. 堆排序使用堆来选数,效率就高了很多。
  2. 时间复杂度为固定的O(n*logn)

前面提到向下建堆的时间复杂度为O(logn) ,相乘后的时间度就为O(n*logn)

  1. 空间复杂度:O(1)
  2. 稳定性:不稳定。因为向下调整的时候可能将相同的值位置顺序改变。
    例:

性能比较

排序十万个随机数时:

运行结果:

可以看出由于堆排序稳定的O(nlogn)的时间复杂度下,速度比O(n^2)快出不少。

排序十万个接近有序数组时:

运行结果:

最后,堆排序胜出本场比赛!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bruin_du

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值