【算法】七大排序详解(下篇)

5.希尔排序

排序思路:又称缩小增量排序,给定一个数值gap,将原数组按照gap分为若干个组,组内再进行插入排序,直到gap=1,整个数组已经被调整的接近有序,最后再全集和上进行一次插入排序即可;
时间复杂度:O(n^1.3~1.5)
稳定性:不稳定

排序思路:

1.将待排序数组按gap分组
在这里插入图片描述
2.将分组后的数组组内进行插入排序,最后再合并
在这里插入图片描述
3.继续以上操作,直至gap=1
在这里插入图片描述
4.最后gap=1,整个数组已经接近有序,最后在数组内进行一次插入排序,整个数组的排序完成
在这里插入图片描述

代码实现:

 /**
     * 希尔排序
     * 时间复杂度:O(N^1.3-1.5)   不稳定
     * @param arr
     */
    public static void shellSort(int[] arr){
        int gap =arr.length>>1;
        while (gap>1){
            //预处理阶段,将原数组按gap分组,组内进行插入排序
            insertSortByGap(arr,gap);
            gap /=2;
        }
        //此时gap=1,直接插入排序即可
        insertSort(arr);
    }
    /**
     * 直接插入排序
     * 每次从无序区间取一个值插入到已排序的区间,直到整个序列有序
     * 时间复杂度 O(N^2) 性能稳定
     * @param arr
     */
    public static void insertSort(int[] arr){
        //已排序区间:[0,i)   未排序区间:[i,n]
        for (int i=1;i<arr.length;i++){
            //简易写法
            for (int j=i;j>0 &&arr[j]<arr[j-1];j--){
                swap(arr,j,j-1);
            }
        }
    }
    /**
     * 按gap分组进行插入排序
     * @param arr
     * @param gap
     */
    private static void insertSortByGap(int[] arr, int gap) {
        for (int i=gap;i<arr.length;i++){
            for (int j=i; j-gap >=0 && arr[j]<arr[j-gap]; j-=gap){
                swap(arr,j,j-gap);
            }
        }
    }
    /**
     * 交换数组arr中索引 i 和 j 的索引位置
     */
    private static void swap(int[] arr, int i, int j) {
        int temp =arr[i];
        arr[i] =arr[j];
        arr[j]=temp;
    }

6. 归并排序

排序思路:归并排序,分为两个过程,一是归而为一,就是将原数组不断拆分,直至数组元素个数为1;二是合而为整,就是将拆分的数组不断合并,同时合并的过程中将数组有序化。
时间复杂度:O(nlogN)
稳定性:稳定
排序过程:
在这里插入图片描述

代码实现:

    /**
     * 归并排序
     * @param arr
     */
    public static void mergeSort(int[] arr){
        //递归函数
        mergeSortInternal(arr,0,arr.length-1);
    }

    /**
     * 在区间 [l , r]上进行归排序
     * @param arr
     * @param l
     * @param r
     */private static void mergeSortInternal(int[] arr, int l, int r) {
        if (r-l <=15){
            //小区间上直接使用插入排序
            insertSort(arr,l,r);
        }
        int mid =l+((r-l)>>1);            //相当于(l+r)/2
        //递归再左区间[l,mid]上归并排序
        mergeSortInternal(arr,l,mid);
        //在右区间上[mid+1,r]上归并排序
        mergeSortInternal(arr,mid+1,r);
        //走完上述递归,左右区间已经有序,最后进行合并
        if (arr[mid] >arr[mid+1]){
            merge(arr,l,mid,r);
        }
    }

    /**
     * 在区间[l , r]上进行插入排序
     * @param arr
     * @param l
     * @param r
     */
    private static void insertSort(int[] arr, int l, int r) {
        for (int i=l+1;i<=r;i++){
            for (int j=i; j>l&& arr[j]<arr[j-1];j--){
                swap(arr,j,j-1);
            }
        }
    }
    /**
     * 交换数组arr中索引 i 和 j 的索引位置
     */
    private static void swap(int[] arr, int i, int j) {
        int temp =arr[i];
        arr[i] =arr[j];
        arr[j]=temp;
    }

    /**
     * 合并两个子数组arr[l...mid]和arr[mid+1,r]
     * 为一个大的有序数组
     * @param arr
     * @param l
     * @param mid
     * @param r
     */private static void merge(int[] arr, int l, int mid, int r) {
        //1.先创建一个临时数组
        int[] temp=new int[r-l+1];
        //2.再将原数组的值拷贝到临时数组
        for (int i=0;i<temp.length;i++){
            //临时数组索引从0开始,原数组从l开始,两数组索引差就是l+i;
            temp[i] =arr[i+l];
        }
        // i 表示左侧小区间的开始索引
        int i=l;
        // j 表示右侧区间的开始索引
        int j=mid+1;
        //k 表示当前正在遍历的索引小标
        for (int k=l;k<=r;k++){
            //向后遍历的过程中,i++,当i >mid时,说明左侧区间已经遍历完
            if ( i >mid){
                //此时,左侧区间已经有序,只需拷贝右侧数组
                arr[k]=temp[j-l];
                j++;
            }else if (j > r){
                //向后遍历的过程中,j++,当j >r时,说明右侧区间已经遍历完
                //此时,右侧区间已经被处理完,只需拷贝左侧区间
                arr[k] =temp[i-l];
                i++;
            }else if (temp[i-l] <= temp[j-l]){
                //此时,左侧区间的值较小,相等元素放在左侧,保证稳定性
                arr[k]=temp[i-l];
                i++;
            }else {
                //右侧区间的元素值较小
                arr[k] =temp[j-l];
                j++;
            }
        }

7.快速排序

时间复杂度:O(nlogN)
稳定性:不稳定

排序思路:

  • 1.首先选定待排序数组的第一个元素为基准值val

其中 l 是数组的初始位置,蓝色区域表示在区间[l+1,j]上所有的元素值都是小于v的,红色区域表示在区间[j+1,i-1]上,所有的元素值都大于等于v,i 索引表示当前正在遍历的元素索引位置

  • 2.当 i 向后遍历过程中,分情况讨论

<2.1>如果当前遍历到的元素arr[i] > v 时,i++,红色区域增加即可;
在这里插入图片描述
i++后,就把大于等于v的元素加入到红色区域,i继续向后遍历;
<2.2>如果在遍历的过程中,arr[i] <v 时:
在这里插入图片描述

 for (int i = l+1; i <=r ; i++) {
            if (arr[i] <val){
                swap(arr,i,j+1);
                j++;
            }
        }
  • 3.整个集合扫描完毕,整个区间就被我们分隔为以下情况:
    在这里插入图片描述
    4.经过以上步骤,arr[j]左侧元素均小于v,arr[j]之后的元素均 大于等于v,分区完成。继续在小于v的区间和大于v的区间继续递归上述过程,直至整个集合有序。

代码实现:

/**
 * 快速排序
 * 时间复杂度:Nlogn
 * 不稳定的排序算法
 */
public class QuickSort {
    /**
     * 快速排序
     * @param arr
     */
    public static void quickSort(int[] arr){
        quickSortInternal(arr,0,arr.length-1);
    }

    /**
     * 在区间【left,right】上进行快速排序
     * @param arr
     * @param low
     * @param high
     */
    private static void quickSortInternal(int[] arr, int low, int high) {
        if (low < high){
            int mid = partition(arr, low, high);
            quickSortInternal(arr, low,mid-1);
            quickSortInternal(arr,mid+1,high);
        }
    }

    /**
     * 分区函数
     * @param arr
     * @param low
     * @param high
     * @return
     */
    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high];
        int i = low;
        for (int j = low; j < high; j++){
            //比pivot小的,全部都换到前面
            if (arr[j] < pivot){
                swap(arr,j, i);
                i++;
            }
        }
        //此时i指向的元素一定大于等于pivot
        swap(arr,high, i);
        return i;
    }

    private static void swap(int[] arr,int i, int j) {
        int temp =arr[i];
        arr[i]=arr[j];
        arr[j]=temp;
    }
    ----------------------------------------------------
    /**优化方案
     * 在区间[l , r]上进行插入排序
     * @param arr
     * @param l
     * @param r
     */
    private static void insertSort(int[] arr, int l, int r) {
        for (int i=l+1;i<=r;i++){
            for (int j=i; j>l&& arr[j]<arr[j-1];j--){
                swap(arr,j,j-1);
            }
        }
   	 }
    }

8.算法性能测试

以上算法中,有的时间复杂度是O(N^2),有的则是O(NlogN),这两种时间复杂度的算法,在实际应用中的性能到底是怎样的呢?下面,我们设计算法性能测试用例,来实际感受一下不同算法性能上的优略。

  • 定义一个算法测试辅助类,包含生成随机数组、判断数组是否有序、深拷贝数组等方法
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

public class SortHelper {
    //获取生成随机数的对象
    private static final ThreadLocalRandom random =ThreadLocalRandom.current();

    /**
     * 在[left,right]的区间上生成一个大小为N的随机数组
     * @return
     */
    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;
    }

    /**
     * 生成一个近乎有序的数组
     * @param n
     * @param times     交换的次数,次数越小,数组有序
     * @return
     */
    public static int[] generateSortedArray(int n,int times){
       int[] arr =new int[n];
        for (int i = 0; i < arr.length; i++) {
            arr[i]=i;
        }
        //交换部分元素,使有序数组变得近乎有序
        for (int j = 0; j < times; j++) {
            //生成[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数组进行排序
     * @param sortName
     * @param 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 e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }

    }
    /**
     * 生成一个深拷贝的arr数组
     * @param arr
     * @return
     */
    public static int[] arrCopy(int[] arr){
        return Arrays.copyOf(arr,arr.length);
    }
    /**
     * 判断数组是否有序
     * @param arr
     * @return
     */
    public static boolean isSorted(int[] arr){
        for (int i = 0; i < arr.length-1 ; i++) {
            if (arr[i]>arr[i+1]){
                System.out.println("Sort error");
                return false;
            }
        }
        return true;
    }
}
  • 选择不同的算法,性能测试
/**
 * 在50000个[0,整形最大值]区间上排序算法的性能测试
 */
public class SortTest {
    public static void main(String[] args) {
        int n=50000;
        int[] arr =SortHelper.generateRandomArray(n,0,Integer.MAX_VALUE);
        int[] arrCopy1 =SortHelper.arrCopy(arr);
        int[] arrCopy2 =SortHelper.arrCopy(arr);
        int[] arrCopy3 =SortHelper.arrCopy(arr);
        int[] arrCopy4 =SortHelper.arrCopy(arr);
        int[] arrCopy5 =SortHelper.arrCopy(arr);
        int[] arrCopy6 =SortHelper.arrCopy(arr);
        SortHelper.testSort("heapSort",arrCopy2);
        SortHelper.testSort("bubbleSort",arrCopy3);
        SortHelper.testSort("selectionSort",arrCopy1);
        SortHelper.testSort("selectionSortOP",arrCopy4);
        SortHelper.testSort("insertionSort",arrCopy5);
        SortHelper.testSort("insertionSortBS",arrCopy6);
    }
}
  • 测试结果
    在这里插入图片描述

简单总结一下,在长度为50000的数组,数值在【0~整形最大值】的数组上进行排序测试,O(N^2)的算法和O(NlogN)算法性能上确实是差了很多,包括优化后的排序算法也比优化前的性能好了很多,故在某些场景下,选择合适的排序算法才是成功之道。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值