数据结构之七大排序2

目录

一、排序的稳定性

二、排序的辅助类

三、选择排序

1.选择排序

双向选择排序(选择排序的优化)

2.堆排序

四、插入排序

1.直接插入排序

​在近乎有序的数组下,插入排序的效率十分高效,甚至优于许多nlogN复杂度的算法。

1)插入排序和选择排序最大的不同

2)折半插入排序(直接插入的优化)

2.希尔排序

五、归并排序

1.归并排序的递归写法

⭐关于合并的细节:

⭐复杂度稳定性的分析

⭐归并排序的两点优化

⭐归并排序的核心就是merge操作 :merge操作可以很好的对物理不连续的元素集合进行排序

2.归并排序的非递归写法

六、海量数据的排序处理⭐⭐

七、快速排序


一、排序的稳定性

      两个相等的数据,经过排序后,排序算法能保证数据相对位置不发生变化,则我们称该算法是具有稳定性的排序算法。

      应用到的例子:某购物商城后台,需要按照订单金额排序,但要求排序后购买的时间的先后顺序不变,此时就需要用到稳定性的排序算法。

 

      这几个常见的排序算法都叫内部排序:一次性将所有待排序的数据放入内存中进行的排序基于元素之间比较的排序。除此之外,还有外部排序,是依赖硬盘(外部存储器)进行的排序算法,如如下三个排序【对于数据集合的要求非常高,只能在特定场合使用】:

写排序的代码,一定注意变量如何定义,以及未排序区间和已排序区间的定义

二、排序的辅助类

生成测试数组以及对排序算法进行测试

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 排序的辅助类
 * 生成测试数组以及对排序算法进行测试
 * @author 美女
 * @date 2022/03/16 15:18
 **/
public class SortHelper {
    //获取一个随机数的对象
    private static final ThreadLocalRandom random=ThreadLocalRandom.current();

    /**
     * 1.在[left,right]上生成n个随机数
     * @param n
     * @param left
     * @param right
     * @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;
    }

    /**
     * 2.生成一个大小为n的近乎有序的数组
     * 思路:先生成一个有序的数组,再随机交换部分数
     * @param n
     * @param times 交换的次数-交换次数越小越有序
     * @return
     */
    public static int[] generateSortedArray(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;
    }
    /**
     * 3.生成一个arr的深拷贝数组
     * 为了测试不同排序算法的性能,需要在相同数据集进行测试
     */
    public static int[] arrCopy(int[] arr){
        return Arrays.copyOf(arr,arr.length);
    }
    /**
     * 4.测试性能
     * 根据传入的方法名称就能调用这些方法,需要借助反射【因为这些测试方法都是static方法,都根据类名称访问,没有对象】
     * 根据方法名称调用相应的排序方法对arr数组进行排序操作
     */
    public static void testSort(String sortName,int[] arr){//()内是方法名称以及待排序的数组集合
        //获取一个类的反射对象:Class是个对象,它的类型是SevenSort类型
        Class<SevenSort> cls=SevenSort.class;
        try {
            //需要调用的方法对象
            Method method=cls.getDeclaredMethod(sortName,int[].class);
            //排序开始终止时间
            Long start=System.nanoTime();
            //排序方法的调用
            method.invoke(null,arr);//静态方法对象是null,在数组arr上进行排序
            Long end=System.nanoTime();
            if(isSorted(arr)){
                //排序算法正确
                System.out.println(sortName+"排序结束,共耗时:"+(end-start)/1000000.0+"ms");
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    /**
     * 判断数组是否有序
     * @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;
    }
}

三、选择排序

1.选择排序

核心思路:

      每次从无序区间中选择一个最大或最小值,存放在无序区间的最前或者最后的位置,直到所有数据都排序完为止。每经过一次排序,有序区间元素个数+1,无序区间元素个数-1。

选择排序是不稳定排序,原因分析如下图(以选择最小值为例):

代码实现:

    /**
     * 选择排序
     */
    public static void selectionSort(int[] arr){
        //最开始无序区间[0...n],有序区间[]
        //arr.length-1:当无需区间只剩一个元素时,整个集合已经有序,所以可以少走一次(-1)
        for (int i = 0; i < arr.length-1; i++) {
            //min变量存储了当前的最小值索引,先默认第一个元素是最小值
            int min=i;
            //j从i+1开始,因为第一个i元素已经被选择过了,从第二个元素开始走(自己和自己不比较)
            for (int j = i+1; j < arr.length; j++) {
                if(arr[j]<arr[min]){
                    min=j;
                }
            }
            //此时min索引一定对应了当前无序区间找到的最小值索引,将它换到无序区间最前面i索引处
            swap(arr,min,i);
        }
    }

Test测试:

 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);
        SortHelper.testSort("selectionSort",arr);
        SortHelper.testSort("bubbleSort",arrCopy1);//冒泡
        SortHelper.testSort("heapSort",arrCopy2);//堆排
    }

结果:

双向选择排序(选择排序的优化)

核心思路:

      一次排序过程中同时选出最大和最小值,放在无序区间的最后和最前。 【注意max在最前面low位置的情况】

public static void selectionSortOP(int[] arr){
        int low=0;
        int high= arr.length-1;
        //low==high,无序区间只剩一个元素,整个区间已经有序
        while (low<=high){
            int min=low;
            int max=low;
            for (int i = low+1; i <= high; i++) {
                if(arr[i]<arr[min]){
                    min=i;
                }
                if(arr[i]>arr[max]){
                    max=i;
                }
            }
            //此时min索引一定是当前无序区间的最小值索引,把这个最小值与low索引处的值交换
            swap(arr,min,low);
            if(max==low){
                //若最大值恰好在low位置,则最小值交换后,最大值已经被换到min这个位置了,max应该=min
                max=min;
            }
            swap(arr,max,high);
            low+=1;
            high-=1;
        }
    }

2.堆排序

数据结构之七大排序1—冒泡排序与堆排序

四、插入排序

1.直接插入排序

核心思路:

      将待排序集合分为两个区间:(i是当前遍历的元素),已经排序的区间[0..,i);待排序区间[i...n)。每次从待排序区间中将第一个元素“插入”到已排序区间的合适位置,直到整个数组有序。(类似打扑克牌排链子)

    public static void insertionSort(int[] arr){
        //从第二个元素开始,默认第一个元素是已排好序的元素
        for (int i = 1; i < arr.length; i++) {
            //待排序区间的第一个元素:arr[i]
            //从待排序区间的第一个元素向前看,找到合适插入位置
            /**
             * 1.
             */
//            for (int j = i; j >0 ; j--) {
//                if(arr[j]>=arr[j-1]){//arr[j]:待排序区间第一个元素;arr[j-1]:已排序区间最后一个元素
//                    //此时说明j索引对应元素比排序区间所有值都大,arr[j]已经有序了,直接进行下次循环
//                    //注意:相等也放在判断条件中,即相等也不进行交换,保证稳定性
//                    break;
//                }else{
//                    swap(arr,j,j-1);
//                }
//            }
             /**
             * 2.=》1等价于2
             */
//            for (int j = i; j >0&&arr[j]<arr[j-1]; j--) {
//                swap(arr,j,j-1);
//            }

   /**
             * 3.无需进行交换操作
             */
//            int j = i;
//            int val=arr[j];
//            for (; j >0 ; j--) {
//                if(val>=arr[j-1]){
//                    arr[j]=val;
//                    break;
//                }else{
//                    arr[j]=arr[j-1];
//                }
//            }
//            arr[j]=val;
            int j=i,val=arr[j];
            for(;j>0&&val<arr[j-1];j--){
                arr[j]=arr[j-1];
            }
            arr[j]=val;
        }
    }

在近乎有序的数组下再进行测试,观察插入排序性能:

在近乎有序的数组下,插入排序的效率十分高效,甚至优于许多nlogN复杂度的算法

1)插入排序和选择排序最大的不同

      当插入排序当前遍历元素>前驱元素时,此时可以提前结束内层循环。极端场景下,当集合是一个完全有序的集合,插入排序内层循环一次都不走,插入排序复杂度称为O(n)。插入排序经常用作高阶排序算法的优化手段之一

2)折半插入排序(直接插入的优化)

      因为插入排序中,每次都是在有序区间中选择插入位置,因此我们可以使用二分查找来定位元素的插入位置。

    /**
     * 折半插入排序
     */
    public static void insertionSortBS(int[] arr){
        //有序区间[0,i)
        //无序区间[i,n)
        for (int i = 1; i < arr.length; i++) {
            //当前遍历的元素
            int val=arr[i];
            int left=0;
            int right=i;//i取不到
            while(left<right){
                int mid=left+((right-left)>>1);
                if(val<arr[mid]){
                    right=mid;//不用mid-1,因为right本身是取不到的
                }else{
                    //val>=mid
                    left=mid+1;
                }
            }
            //搬移left...i的元素
            for (int j = i; j >left ; j--) {
                arr[j]=arr[j-1];//后一个元素等于前一个元素
            }
            //left就是val插入的位置
            arr[left]=val;
        }
    }

 Test: 

2.希尔排序

      ⭐缩小增量排序,先选定一个整数(gap,gap一般都选取数组长度的一半或者1/3),将待排序十足先按照gap分组,不同组之间内部使用插入排序,排序之后,再将gap/=2或gap/=3,重复上述流程,直到gap=1

      ⭐当gap=1时,整个数组已经被调整为近乎有序的数组,此时就是插入排序最好的场景,最后再在整个数组上进行一次插入排序即可

    /**
     * 希尔排序
     */
    public static void shellSort(int[] arr){
        int gap= arr.length>>1;
        while(gap>1){
            insertionSortByGap(arr,gap);
            gap=gap>>1;
        }
        insertionSort(arr);
    }

    /**
     * 按gap分组进行插入排序
     * @param arr
     * @param gap
     */
    private static void insertionSortByGap(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);
            }
        }
    }

思考:希尔排序中比较gap需要不断向前看,能否从0开始不断向后看gap步?

解析:不可

五、归并排序

1.归并排序的递归写法

归:原数组不断拆分为小数组,一直拆分到每个子数组只有一个元素。这是第一阶段,归而为一【采用递归归完开始合并】。

并:将相邻两个数组合二为一,这是第二阶段。

⭐关于合并的细节:

创建一个大小为合并后的数组大小的临时数组aux,将数组值拷贝过去。k:当前正在处理的arr的索引;i:需要合并的左侧小数组的开始的索引;j:需要合并的右侧小数组的开始的索引

为什么合并过程要创建一个临时数组aux?

      防止在合并过程中小元素要覆盖某些大元素,造成大元素的丢失。

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

    /**
     * 在arr[l...r]进行归并排序,整个arr经过函数后就是一个已经有序的数组
     * @param arr
     * @param l
     * @param r
     */
    private static void mergeSortInternal(int[] arr, int l, int r) {
        //归
        if(l>=r){
            //当前数组只剩一个元素,归过程结束
            return;
        }
        int mid=l+((r-l)>>1);
        //将原数组拆分成左右两个小区间,分别递归的进行归并排序
        mergeSortInternal(arr,l,mid);
        mergeSortInternal(arr,mid+1,r);
        //归过程结束,此时merge开始并
        merge(arr,l,mid,r);
    }

    /**
     * 合并两个子数组arr[l...mid],arr[mid+1...r]
     * 为一个大的有序数组arr[l...r]
     * @param arr
     * @param l
     * @param mid
     * @param r
     */
    private static void merge(int[] arr, int l, int mid, int r) {
        //先创建一个新的临时组数aux
        int[] aux=new int[r-l+1];//数组长度与索引有一个1的差值
        //将arr元素值拷贝到aux上
        for (int i = 0; i < aux.length; i++) {
            //arr的left不一定从索引0开始,它从left(l)开始,与aux的i刚好差了l个单位
            //如合并15、47举例,值1的索引是4,他要放在aux[0]位置,即aux[0]=arr[0+4],l恰好是4
            aux[i]=arr[i+l];
        }
        //此时临时数组aux的值已经拷贝好了
        //k表示当前正在合并的原数组的索引下标;i是左侧小数组的开始索引,j是右侧小数组的开始索引
        int i=l;
        int j=mid+1;
        for (int k = l; k <=r ; k++) {
            if(i>mid){
                //左侧区间已经处理完毕,只需要将右侧区间的值拷贝到原数组即可
                arr[k]=aux[j-l];//j表示原数组索引,对应aux上的索引是有l个单位的偏移量
                j++;
            }else if(j>r){
                //右侧区间已经被处理完毕,只需要将左侧区间的值拷贝到原数组即可
                arr[k]=aux[i-l];
            }else if(aux[i-l]<=aux[j-l]){
                //此时左区间元素较小//等于号放在左区间,保证排序的稳定性
                arr[k]=aux[i-l];
                i++;
            }else{
                //右侧区间元素值较小
                arr[k]=aux[j-l];
                j++;
            }
        }
    }

结果: 

希尔排序、堆排序、归并排序的效率比较:

⭐复杂度稳定性的分析

1.归并排序是一个稳定nlogN级别时间复杂度的排序算法

2.此处稳定指的是两个稳定:

1)时间复杂度稳定:无论集合中元素如何变化,它的时间复杂度一直都是nlogN,不会退化为O(n^2)。【logN:递归深度就是我们拆分数组所用时间,拆了logN次,就是树的高度问题;O(n):合并两个子数组的数组遍历的过程的时间复杂度(for(int k=l,k<=r;k++))】

2)是一个稳定性的排序算法:相同元素的相对位置不发生变化

⭐归并排序的两点优化

1)当左右两个子区间走完子函数后,左右两个区间已经有序了。如果此时arr[mid]<arr[mid+1],【arr[mid]已经是左区间最大值了,arr[mid+1]已经是右区间的最小值】,此时左边最大<右边最小,整个区间已经有序了,无需再走了。

mergeSortInternal(arr,l,mid);//走完这个函数之后arr[l...mid]已经有序

mergeSortInternal(arr,mid+1,r);//走完这个函数后[mid+1...r]已经有序

//只有两个子区间还有先后顺序不同时,才merge

if(arr[mid]>arr[mid+1]){

//左区间最大值>右区间最小值才merge

merge(arr,l,mid,r);

}

2 )在小区间上,我们可以直接使用插入排序来优化,没必要元素一直拆分到1位置。从实验得出,一般r-l<=15时,使用插入排序,性能是极好的。【减少了归并的递归次数】

if(r-l<=15){

insertionSort(arr,l,r);

return;

}

  /**
     * 在arr[l...r]使用直接插入排序
     * 归并排序中,一般r-l<=15时,使用插入排序,不必归并到1
     * @param arr
     * @param l
     * @param r
     */
    private static void insertionSort(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);
            }
            
        }
    }

优化后全部代码:

    /**
     * 归并排序
     */
    public static void mergeSort(int[] arr) {
        mergeSortInternal(arr,0,arr.length - 1);
    }
    /**
     * 在arr[l...r]进行归并排序,整个arr经过函数后就是一个已经有序的数组
     */
    private static void mergeSortInternal(int[] arr, int l, int r) {
        if(r-l<=15){
            insertionSort(arr,l,r);
            return;
        }
        int mid=l+((r-l)>>1);
        mergeSortInternal(arr,l,mid);
        mergeSortInternal(arr,mid+1,r);
        if(arr[mid]>arr[mid+1]){
            merge(arr,l,mid,r);
        }
    }

    /**
     * 在arr[l...r]使用直接插入排序
     * 归并排序中,一般r-l<=15时,使用插入排序,不必归并到1
     */
    private static void insertionSort(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[l...mid],arr[mid+1...r]
     * 为一个大的有序数组arr[l...r]
     */
    private static void merge(int[] arr, int l, int mid, int r) {
        int[] aux=new int[r-l+1];
        for (int i = 0; i < aux.length; i++) {
            aux[i]=arr[i+l];
        }
        int i=l;
        int j=mid+1;
        for (int k = l; k <=r ; k++) {
            if(i>mid){
                arr[k]=aux[j-l];
                j++;
            }else if(j>r){
                arr[k]=aux[i-l];
            }else if(aux[i-l]<=aux[j-l]){
                arr[k]=aux[i-l];
                i++;
            }else{
                arr[k]=aux[j-l];
                j++;
            }
        }
    }

⭐归并排序的核心就是merge操作 :merge操作可以很好的对物理不连续的元素集合进行排序

1.【链表】:leetCode148-排序链表

leetcode148-排序链表(应用到归并的merge方法)

2.求数组中的逆序对个数:剑指offer51

JZoffer51-数组中的逆序对(归并排序解决)

2.归并排序的非递归写法

      归并排序的核心:先将整个集合拆分为只有一个元素的集合(归),再将只有一个元素的集合合并为每个数组两个元素,四个元素...,直到整个数组合并(并)。

public static void mergeSortNonRecursion(int[] arr){
        //最外层循环表示每次合并的子数组的元素个数
        for (int sz=1;sz<= arr.length;sz+=sz){//先从1个元素开始合并
            //内层循环的变量i表示每次合并的开始索引
            //i+size就是右区间的开始索引,i+size<arr.length说明还存在右区间
            for (int i=0;i+sz< arr.length;i+=sz+sz){//sz+sz是下一次合并的开始索引
                merge(arr,i,i+sz-1,Math.min(i+sz+sz-1, arr.length-1));//传入arr,left,mid,right
                //Math.min取右区间与arr.length的最小值,因为有可能i+sz+sz-1会超过数组的最大索引
            }
        }
    }
 /**
     * 合并两个子数组arr[l...mid],arr[mid+1...r]
     * 为一个大的有序数组arr[l...r]
     * @param arr
     * @param l
     * @param mid
     * @param r
     */
    private static void merge(int[] arr, int l, int mid, int r) {
        //先创建一个新的临时组数aux
        int[] aux=new int[r-l+1];//数组长度与索引有一个1的差值
        //将arr元素值拷贝到aux上
        for (int i = 0; i < aux.length; i++) {
            //arr的left不一定从索引0开始,它从left(l)开始,与aux的i刚好差了l个单位
            //如合并15、47举例,值1的索引是4,他要放在aux[0]位置,即aux[0]=arr[0+4],l恰好是4
            aux[i]=arr[i+l];
        }
        //此时临时数组aux的值已经拷贝好了
        //k表示当前正在合并的原数组的索引下标;i是左侧小数组的开始索引,j是右侧小数组的开始索引
        int i=l;
        int j=mid+1;
        for (int k = l; k <=r ; k++) {
            if(i>mid){
                //左侧区间已经处理完毕,只需要将右侧区间的值拷贝到原数组即可
                arr[k]=aux[j-l];//j表示原数组索引,对应aux上的索引是有l个单位的偏移量
                j++;
            }else if(j>r){
                //右侧区间已经被处理完毕,只需要将左侧区间的值拷贝到原数组即可
                arr[k]=aux[i-l];
            }else if(aux[i-l]<=aux[j-l]){
                //此时左区间元素较小//等于号放在左区间,保证排序的稳定性
                arr[k]=aux[i-l];
                i++;
            }else{
                //右侧区间元素值较小
                arr[k]=aux[j-l];
                j++;
            }
        }
    }

结果: 

六、海量数据的排序处理⭐⭐

      假设现在待排序的数据只有100G,但是内存只有1GB,如何排序这100GB数据呢?(本质还是归并排序【200路归并过程】)

1.先将这100G数据分别存储在200个文件中(文件在硬盘),每个文件都是0.5G。

2.分别将这200个文件一次读取到内存中,使用任何一个内部排序算法对其进行排序(快排,堆排,归并都行),此时可以得到200个有序的文件。

3.分别对这200个文件进行merge操作合并即可。

      merge操作的具体思路呢?:这200个小文件已经有序,每次都取出200个文件的第一个元素放到内存中,内部排序这200个小值的最小值写回大文件,重复上述流程直到这200个文件所有内容全部写回即可得到一个排序好的大文件。

七、快速排序

数据结构之七大排序3—快速排序详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值