七大基于比较的排序[总结]

插入排序

插入排序是一种比较常见的排序方法,它主要包含直接插入排序,希尔排序和折半插入等集中常见的排序方法。

直接插入排序

首先,我们先来想象一个场景,一个有序的数组,现在需要往这个有序数组中添加一个新的数据,那么此时,需要找到这个数据的合适位置,将大于它的元素后移,将它直接插入就可以了。
在这里插入图片描述
这是一个动态的,一个一个往有序集合中添加数据的过程,我们可以通过这种方法保持集合中的数据一直有序。而对于一组乱序的静态数组,我们也可以借鉴这种方法来进行排序。
直接插入排序是如何实现的
首先,我们将数组中的数据分为两个区间,已排序区间未排序区间。初始时,已排序区间只有一个元素,及就是不断从第二个数据往后拿出元素来向前进行插入排序,直至未排序区间元素个数为0个,算法结束。
在这里插入图片描述
如图,描述为算法为:

  • 从第一个元素开始,认为该元素为已排序元素;
  • 取出下一个元素,在已排序元素中从后向前扫描;
  • 如果扫描的位置元素大于新元素,将该元素移到下一个位置;
  • 重复上一步,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5.

代码实现Java实现

    /**
     * 直接插入排序
     * 每次选择无需区间第一个元素,插入到有序区间的合适位置
     * @param arr
     */
    public void insertionSortBase(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            for (int j = i; j > 0; j--) {
                if (arr[j] < arr[j - 1]){
                    swap(arr,j,j - 1);
                }else {
                    break;
                }
            }
        }
    }

优化: 由于插入是在有序区间的插入,因此可以使用二分查找的办法来快速定位插入的位置。

    public void insertionSortBS(int[] arr){
        for (int i = 1; i < arr.length; i++) {
            int val = arr[i];
            int low = 0;
            int high = i;
            while (low < high){
                int mid = (low + high) / 2;
                //将相等的值放在左半区间,保证稳定性
                //不选中那个相等的值,就不会被移动
                if (val >= arr[mid]){
                    low = mid + 1;
                }else {
                    high = mid;
                }
            }
            //数据搬移
            for (int j = i; j > low; j--) {
                arr[j] = arr[j - 1];
            }
            arr[low] = val;
        }
    }

希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有
距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,
所有记录在统一组内排好序。

  1. 希尔排序是对直接插入排序的优化。
  2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很
    快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
    在这里插入图片描述
    下面通过动图来看希尔排序的执行流程
    在这里插入图片描述
    代码:
    /**
     * 希尔排序
     * 先选定一个gap,将待排序的数据中所有记录按gap分组,所有距离为gap的数据放在同一组,将组内元素排序。
     * 然后不断缩小gap的大小直到变为1,当gap变为1时,整个数组近乎有序。调用普通插入排序统一排序即可
     * (普通插入排序在近乎有序的数组中排序效率是非常高的)
     * @param
     */
    public void shellSort(int[] arr){
        int gap = arr.length >> 1;
        while (gap > 1){
            //不断按照gap分组,组内插入排序
            insertionSortGap(arr,gap);
            gap /= 2;
        }
        //整个数组的插入排序
        insertionSortGap(arr,1);
    }

    private void insertionSortGap(int[] arr, int gap) {
        //最外层从gap开始,不断走到数组末尾,
        //子数组分别交替进行,不是每个组每个组的比较
        for (int i = gap; i < arr.length; i++) {
            //比较
            for (int j = i; j - gap >= 0 && arr[j] < arr[j - gap]; j = j - gap) {
                    swap(arr,j,j - gap);
            }
        }
    }

选择排序

选择排序

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾
在这里插入图片描述
每次从未排序区间去找,定位最小的那个值,插在已排序区间的末尾。
代码:

    public void selectionSort(int[] arr){
        //每次从待排序数组中选择最小值放在待排序数组的最前面
        for (int i = 0; i < arr.length - 1; i++) {
            //min变量存储了最小值元素的下标
            int min = i;
            //每次从无序区间中选择最小值
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[min]){
                    min = j;
                }
            }
            swap(arr,i,min);
        }
    }

优化: 双向选择排序

    /**
     * 双向选择排序,每次选出最小值放前面,最大值放后面
     * @param arr
     */
    public void selectionSortOP(int[] arr){
        int low = 0;
        int high = arr.length - 1;
        //有序区间变成从[0,low+1)
        while (low < high){
            int min = low;
            int max = low;
            for (int i = low + 1; i < high; i++) {
                if (arr[i] > arr[max]){
                    max = i;
                }
                if (arr[i] < arr[min]){
                    min = i;
                }
            }
            //min存储了无需区间最小值,max存储无需区间最大值
            swap(arr,low,min);
            if (max == low){
                max = min;
            }
            swap(arr,max,high);
            low += 1;
            high -= 1;
        }
    }

堆排序

基本原理也是选择排序,只是不再使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数
注意: 排升序要建大堆;排降序要建小堆。
在这里插入图片描述
代码:

    /**
     * 原地堆排序,将最大堆的堆顶元素与数组最后一个元素进行交换
     * 数组长度-1后继续堆化
     * 注意向下调整从从后往前第一个非叶子节点开始,
     * 节点下标为:(arr.length - 1 - 1) / 2
     */
    public void heapSort(int[] arr){
        for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
            siftDown(arr,i,arr.length);
        }
        for (int i = arr.length - 1; i > 0; i--) {
            //开始进行元素向后交换
            swap(arr,0,i);
            //交换后堆顶元素下沉,继续堆化
            siftDown(arr,0,i);
        }
    }
    /**
     * 向下调整
     * @param n 数组中需要堆化的元素个数
     */
    private void siftDown(int[] arr, int i, int n) {
        while ((2 * i) + 1 < n) {
            int j = 2 * i + 1;
            if(j + 1 < n && arr[j + 1] > arr[j]){
                j = j + 1;
            }
            if(arr[i] >= arr[j]){
                break;
            }else {
                swap(arr,i,j);
                i = j;
            }
        }
    }

交换排序

冒泡排序

冒泡排序只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求。如果不满足就让他们俩呼唤。一次冒泡会让至少一个元素移动到它应该在的位置,重复n次,就完成了n个数据的排序工作。

下面用一个例子,带你看下冒泡排序的整个过程。我们要对一组数据 4,3,2,1,从小到到大进行排序。第一次冒泡操作的详细过程就是这样:
在这里插入图片描述
可以看出,经过一次冒泡操作之后,4 这个元素已经存储在正确的位置上。要想完成所有数据的排序,我们只要进行 4 次这样的冒泡操作就行了。
在这里插入图片描述
总过程如图:
在这里插入图片描述
代码:

    public void bubbleSort(int[] arr){
        for (int i = 0; i < arr.length - 1; i++) {
            boolean step = false;
            for (int j = 0; j < arr.length - i - 1; j++) {
                if(arr[j] > arr[j + 1]){
                    step = true;
                    swap(arr,j,j + 1);
                }
            }
            // 此时已经没有数据交换,停止循环,减少次数
            if(!step){
                break;
            }
        }
    }

快速排序

  1. 从待排序区间选择一个数,作为基准值(pivot);
  2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可
    以包含相等的)放到基准值的右边;
  3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度 == 1,代表已经有序,或者小区间
    的长度 == 0,代表没有数据。

递推公式: quick_sort(p…r) = quick_sort(p…q-1) + quick_sort(q+1, r)
终止条件:
p >= r

递归代码:

    public void quickSort(int[] arr){
        quickSortInternal(arr,0,arr.length - 1);
    }

    private void quickSortInternal(int[] arr, int l, int r) {
        if (r - l<= 15) {
            // 拆分后的小区间直接使用插入排序,不再递归
            insertBase(arr,l,r);
            return;
        }
        //选择一个基准值,返回他的下标
        int p = partition(arr,l,r);
        //在小于基准值的区间进行快速排序
        quickSortInternal(arr,l,p - 1);
        //在大于基准值的区间上进行快速排序
        quickSortInternal(arr,p + 1,r);
    }

那么对于快速排序,关键点就是分区函数patition的操作,我们先来看如下示例:

如图,v作为基准值,橙色部分的元素都是小于v的,蓝色部分元素都大于v,e为当前待分区元素,白色部分是还未分区的元素
在这里插入图片描述
我们每次选取待排序数组的第一个元素作为分区点,然后从第二个元素开始从前向后遍历,i指向当前正在遍历的元素,从array[l+1]…array[j]的元素均小于v,从array[j+1]…array[i-1]的元素均大于v。每当碰到比分区元素小的节点就与array[j+1]节点进行交换然后橙色区域的范围就扩大1个长度,这样走下来当i走完整个待排序分区后整个待排序数组如下:
在这里插入图片描述
最后只需要将l指向的元素与j指向的元素交换即可达到j之前的元素全部小于分区点,j之后的元素全部大于分区点,如图所示:
在这里插入图片描述
原地分区partition实现:

    private int partition(int[] arr, int l, int r) {
        int randomIndex = random.nextInt(l,r);
        swap(arr,l,randomIndex);
        int v = arr[l];
        int j = l;
        //arr[l + 1 ... j] < v
        //arr[j + 1 ... i) >= v
        for (int i = l + 1; i <= r; i++) {
            if (arr[i] < v){
                swap(arr,j + 1,i);
                //小于v的元素值新增一个
                j ++;
            }
        }
        //此时j下标对应的就是最后一个小于v的元素。交换j和v的值,就把基准值放到了最终位置
        swap(arr,l,j);
        return j;
    }

在这里插入图片描述

随机化快排

当待排序数组近乎有序甚至完全有序时,快排会退化为O(n^2)的排序算法。造成这个现象的主要原因是每次分区后,两个分区大小完全不均衡,甚至完全只有一个分区,如下图:
在这里插入图片描述
因此我们可以每次在整个待排序数组中随机选取一个元素作为分区点来优化我们的分区问题,代码如下:

// 随机选取待排序数组中的任意一个元素
int randomIndex = (int) (Math.random()*(r-l+1) + l);
swap(array,l,randomIndex);
int v = array[l];

双路快排

还记得上面我写的快排的子过程么,考虑到了e>v,e<v,而e=v的情况没有考虑。看了代码理解了的同学应该清楚,其实我是把等于v这种情况包含进了大于v的情况里面了,那么会出现什么问题?**不管是当条件是大于等于还是小于等于v,当数组中重复元素非常多的时候,等于v的元素太多,那么就将数组分成了极度不平衡的两个部分,因为等于v的部分总是集中在数组的某一边。**此时快排的时间复杂度再次退化为O(n^2)级别

那么一种优化的方式便是进行双路快排。

和单路快排不同的是此时我们将小于v和大于v的元素放在数组的两端,那么我们将引用新的索引j的记录大于v的边界位置。如图:
在这里插入图片描述
i索引不断向后扫描,当i的元素小于v的时候继续向后扫描,直到碰到了某个元素大于等于v。j同理,直到碰到某个元素小于等于v。如图:
在这里插入图片描述
然后绿色的部分便归并到了一起,而此时只要交换i和j的位置的元素就可以了,然后i++,j–就行了。如图:

在这里插入图片描述
二路快排分区函数实现:

    private int partition2(int[] arr, int l, int r) {
        int randomIndex = random.nextInt(l, r);
        swap(arr, l, randomIndex);
        int v = arr[l];
        //arr[l + 1..i) < v
        int i = l + 1;
        //arr(j..r] > v
        int j = r;
        while (i <= j){
            // i从前向后扫描碰到第一个 >= v的元素停止
            while (i <= r && arr[i] < v){
                i++;
            }
            // j从后向前扫描碰到第一个 <= v的元素停止
            while (j >= l + 1 && arr[j] > v){
                j--;
            }
//            if(i > j){
//                //整个数组已经全部扫描完毕
//                break;
//            }
            swap(arr, i, j);
            i++;
            j--;
        }
        //此时j落在最后一个 <= v的元素,因为终止条件i>j
        swap(arr, i, j);
        return j;
    }

三路快排

双路快排将整个数组分成了小于v,大于v的两部分,而三路快排则是将数组分成了小于v,等于v,大于v的三个部分,当递归处理的时候,遇到等于v的元素直接不用管,只需要处理小于v,大于v的元素就好了。某一时刻的中间过程如下图:
在这里插入图片描述
当元素e等于v的时候直接纳入绿色区域之内,然后i++处理下一个元素。如图:
在这里插入图片描述
当元素e小于v的时候,只需要将元素e与等于e的第一个元素交换就行了,这和刚开始讲的快速排序方法类似。同理,当大于v的时候执行相似的操作。如图:
在这里插入图片描述
当全部元素处理完之后,数组便成了这个样子:
在这里插入图片描述
三路快排实现:

    //三路快排
    public void quickSort3(int[] arr){
        quickSortInternal3(arr, 0, arr.length - 1);
    }

    private void quickSortInternal3(int[] arr, int l, int r) {
        if (r - l<= 15) {
            // 拆分后的小区间直接使用插入排序,不再递归
            insertBase(arr,l,r);
            return;
        }
        int randomIndex = random.nextInt(l, r);
        swap(arr, l, randomIndex);
        int v = arr[l];
        // arr[l + 1..lt] < v
        int lt = l;
        // arr[gt..r] > v
        int gt = r + 1;
        //arr[lt + 1..i)
        int i = l + 1;
        while (i < gt){
            if(arr[i] < v){
                //因为i = l + 1, 此时lt+1这个值已经是处理过的
                swap(arr, lt + 1, i);
                lt ++;
                i ++;
            }else if(arr[i] > v){
                swap(arr,gt - 1, i);
                gt --;
                //gt-1这个元素还没有处理
            }else{
                // ==v
                i ++;
            }
        }
        swap(arr, l, lt);
        // arr[l..lt - 1] < v
        quickSortInternal3(arr, l, lt -1);
        // arr[gt.. r] > v
        quickSortInternal3(arr, gt,r);
    }

归并排序

归并排序和快速排序都用到了分治思想,非常巧妙。我们可以借鉴这个思想,来解决非排序的问题,比如:如何在 O(n) 的时间复杂度内查找一个无序数组中的第 K 大元素?

归并排序的核心思想还是蛮简单的。如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了。
在这里插入图片描述
归并排序使用的就是分治思想。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决。小的子问题解决了,大问题也就解决了。

那么提到分治,必然就是递归了。写递归代码的技巧就是,分析得出递推公式,然后找到终止条件,最后将递推公式翻译为递归代码。所以,要想写出归并排序的代码,我们先写出归并排序的递推公式。

归并排序的主要方法就是merge过程

//递推公式:
merge_sort(l…r) = merge(merge_sort(l…mid), merge_sort(mid+1…r))

//终止条件:
l >= r 不用再继续分解

mergeSort(l…r) 表示,给下标从 l 到 r 之间的数组排序。我们将这个排序问题转化为了两个子问题,merge_sort(l…mid) 和 merge_sort(mid+1…r),其中下标 mid 等于 l 和 r 的中间位置,也就是 (l+r)/2。当下标从 l 到 mid 和从 mid+1 到 r 这两个子数组都排好序之后,我们再将两个有序的子数组合并在一起,这样下标从 l 到 r 之间的数据就也排好序了。

    /**
     * 在数组的[l...r]上进行归并排序
     * @param arr
     * @param l
     * @param r
     */
    public void mergeSortInternal(int[] arr, int l, int r) {
//        if (r - l<= 15) {
//            // 拆分后的小区间直接使用插入排序,不再递归
//            insertBase(arr,l,r);
//            return;
//        }
        if(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);
        }
    }


大家可能已经发现了,merge(arr[l…r], arr[l…mid], arr[mid+1…r]) 这个函数的作用就是,将已经有序的 arr[l…mid]和 arr[mid+1…r]合并成一个有序的数组,并且放入 arr[l…r]。那这个过程具体该如何做呢?

如图所示,我们每一次merge的过程都申请一个临时数组 temp,大小和当前合并后数组大小相同的数组。我们用两个游标 i 和 j,分别指向 arr[l…mid]和 arr[mid+1…r]的第一个元素。比较这两个元素 arr[i] 和 arr[j],如果 arr[i]<=arr[j],我们就把 arr[i] 放入到临时数组 temp,并且 i 后移一位,否则将 arr[j] 放入到数组 tmp,j 后移一位。

继续上述比较过程,直到其中一个子数组中的所有数据都放入临时数组中,再把另一个数组中的数据依次加入到临时数组的末尾,这个时候,临时数组中存储的就是两个子数组合并之后的结果了。最后再把临时数组 tmp 中的数据拷贝到原数组 arr[l…r] 中。
merge的代码:

```java
    /**
     * 将已经有序的两个区间合并为一个大的有序区间
     * @param arr
     * @param l
     * @param mid
     * @param r
     */
    private void merge(int[] arr, int l, int mid, int r) {
        //开辟一个大小和合并后数组大小相同的数组
        int[] temp = new int[r - l + 1];
        for (int i = l; i <= r; i++) {
            //temp的索引是从0开始的,而arr拷贝过来的只是原数组的一小部分区间
            //有l个单位的偏移量
            temp[i - l] = arr[i];
        }
        //遍历temp,更改arr的部分区间
        //i对应左半区间索引
        int i = l;
        //j对应右半区间起始索引
        int j = mid + 1;
        //k表示处理到的下标索引
        for (int k = l; k <= r; k++) {
            if(i > mid){
                //左半区间元素放完了
                arr[k] = temp[j - l];
                j ++;
            }else if (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 ++;
            }
        }
    }

下面是一段归并排序非递归版本的代码,merge过程还是沿用上述代码

    /**
     * 归并排序非递归,自底向上写法
     * @param arr
     */
    private void mergeSortNonRecursion(int[] arr){
        //sz表示每次合并的元素个数,sz区间不断成倍扩大
        for (int sz = 1; sz <= arr.length; sz = sz + sz) {
            //merge过程,i表示每次merge开始索引下标
            for (int i = 0; i + sz < arr.length; i += sz + sz) {
                merge(arr,i,i + sz - 1,Math.min(i + 2 * sz - 1,arr.length - 1));
            }
        }
    }

在这里插入图片描述

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值