七大排序算法

本文目标

掌握七大基于比较的排序算法基本原理及实现
掌握排序算法的性能分析
掌握 java 中的常用排序方法

概念

排序

排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。 平时的上下文中,如果提到排序,通常指的是排升序(非降序)。
通常意义上的排序,都是指的原地排序(in place sort)。

稳定性(重要)

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

在这里插入图片描述
例如此处有两个5,5(a)这个数字在排序前就一直在5(b)的前面,在排序后,5(a)这个数字仍在5(b)的前面,就说说明这写的排序是稳定性排序
来记住两个结论:

1:我们认定如果当前这个排序中,在排序的过程当中,没有发生跳跃式的交换,那么我们就认为这个排序是稳定的排序
例如之前的堆排序就不是一个稳定的排序,因为它发生了跳跃式的交换
2:
如果一个排序是稳定的排序,那么它也可以被实现为一个不稳定的排序
但是如果一个排序本身就是不稳定的排序,那么就不可能实现为一个稳定的排序

七大基于比较的排序-总览

在这里插入图片描述

插入排序

直接插入排序–原理

现在假设我们要对如下的数组进行排序:

10,6,3,1,8

直接插入排序的思想是这样的:
1:先定义一个变量i来遍历我们的数组,此时要注意的是数组的第一个元素为10,已经为有序的,所以i此时指向我们的第二个元素6
在这里插入图片描述
2:此时我们就要想啦,6这个元素到底插入到哪里呢?我们发现需要6<10,所以需要插入到10的前面,但是10这个元素总的需要被记录下来把,于是我们就定义j这个下标,它的值等于i-1:如下所示:
在这里插入图片描述
3:此时再定义一个tmp变量用于存储我们i下标的值,然后每次判断j下标的值与tmp存储的值的大小:此时tmp存储的值为6,如下所示:
在这里插入图片描述
4:此时10>6,则让10往前走一步,到了1下标的位置,然后让j- -,我们会发现此时j=-1,-1的位置是没有元素的,所以就把tmp中的6放入到我们0下标的位置,如下图所示:
在这里插入图片描述
此时我们认为已经发生了一次直接插入排序了.
5:此时6和10就已经有序了,那么就让i++,此时i走到了2下标的位置,同时让2下标的位置的3元素放入到我们tmp变量中,让j指回我们的i-1下标处,也就是我们1下标处:如下图所示:
在这里插入图片描述
6:然后此时我们j所指向的元素10>3,所以10就往后移动一格到了2号下标处,然后j- -,后指向了1号下标处,此时1号下标处的6>3,那么6就移动到了1号下标位置处,然后j- -,后到了-1下标处,-1下标处此时没有元素,所以就把3放入到0号下标处:如下图所示:
在这里插入图片描述
7:此时接着往下走,i此时++后指向了我们的3下标处,j此时指向的下标为i-1=2处,然后把3下标处的值放入tmp中,如下所示:
在这里插入图片描述
8:还是重复刚才的步骤,只要j指向的元素大于1,这个元素就往后移动一个格子,然后j–,直到j=-1后,把我们tmp中所存储的值插入到空的格子当中即可,最后的示意图如下所示:
在这里插入图片描述
9:此时i继续++后指向了我们4下标处,j此时指向的下标的值为4-1=3,然后将i所指向的值8放入到我们的tmp当中去,如下图所示:
在这里插入图片描述
10:然后j下标所对应的元素为10,10>8,所以10移动到了4下标处,然后j- -后到了2下标处,此时6<8,那就直接把8放到j+1的位置即可
在这里插入图片描述
至此我们这个数组就完成了由小到大的排序。

代码实现
public class TestSort {
    public static void insertSort(int[] array) {
        for (int i = 1; i < array.length; i++) {
            int tmp = array[i];
            int j = i - 1;
            for (; j >= 0; j--) {
                //注意此处应该为>,不应该为大于等于,因为大于等于就不是稳定排序了
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                } else {
                    break;
                }
            }
            //写到外面是以防循环走完了tmp中的元素还没有插入到对应的位置
            array[j + 1] = tmp;
        }
    }

    public static void main(String[] args) {
        int[] array = {1, 14, 35, 7, 68, 79};
        //排序前的数组,结果为:[1, 14, 35, 7, 68, 79]
        System.out.println(Arrays.toString(array));
        insertSort(array);
        //排序后的数组,结果为[1, 7, 14, 35, 68, 79]
        System.out.println(Arrays.toString(array));
    }
}
注意事项

一.
首先我们再写比较的时候要注意是array[j] > tmp,原因是如果写成大于号就能确保我们直接插入排序是一个稳定的排序算法,我来举个例子:
就拿刚才的数组来说把:假设我们把数组变换一下,变换成如下数组:

10,6(a),3,1,6(b)

那么还是按照刚才的步骤一直走,走到最后一步的示意图如下所示:
在这里插入图片描述
此时我们最后一个元素要插入到j+1下标处的时候先要进行array[j]与tmp的值的判断,此时array[j]=6(a),tmp=6(b),假设6(a)在排序前就一直在6(b)的前面,那么我们直接插入法为了保证是一个稳定排序,也一定要保证排序后6(a)仍在6(b)的前面,当array[j]>tmp的时候,是可以保证6(a)仍在6(b)的前面,但是当array[j]>=tmp的时候,此时我们6(a)就直接跑到了array[j+1]处,然后我们的6(b)就跑到了aray[j]处,很明显这就是一个不稳定排序.
所以直接插入排序我们是可以把它实现成一个稳定排序的,既然可以实现成一个稳定排序,就不要实现成非稳定排序,如果面试官问直接插入排序是一个稳定排序还是非稳定排序,答案当然是稳定排序
二.
我们在i循环内,j循环外最后要写上array[j+1]=tmp,因为按照正常逻辑的话这句话应该写在j循环的else语句内部,但是会有一种特殊情况,当j循环完毕后可能tmp中的元素还没有插入到array[j+1]处,所以此时应该把这句话写到i循环内,j循环外部.
三.
关于直接排序算法的时间复杂度和空间复杂度:
此时需要分为两种情况:
(1)当需要排序的数组为无序的情况
时间复杂度:O( N 2 N^{2} N2)
空间复杂度: O(1)
(2)当需要排序的数组为有序的情况
例如这样一组数组:2,3,4,5,6
我们会发现当我们定义i和j的时候,j根本用不上,只有i一直在遍历,所以在需要排序的数组是有序的情况下,时间复杂度可以达到O(N),空间复杂度仍为O(1),在后面的排序中在最好的情况下时间复杂度可以达到O(N)的恐怕也只有直接插入法排序了
所以童鞋们要注意啦,假如此时笔试题出了这样一道题目:
问当前有一组待排序的序列,但是这组待排序序列大部分是有序的,请问,哪个排序更合适?

答:当然是选择直接插入排序

另外直接插入排序还会用到很多排序的优化上,例如快速排序
.性能分析
在这里插入图片描述

希尔排序(了解,基本不考)

前提

在这里插入图片描述
假设10000份数据如果直接使用希尔排序的话,其时间复杂度为O(N^2),也就是10000 * 10000 = 1 0000 0000
但是如果我们将这10000个数据分成100份的话,每份相当于有100个数据,那么对每份数据进行插入排序的话,每份数据的时间复杂度为O(N^2),最终每份数据相当于100100,一共100份数据就是100100*100 = 1 0000 00,

我们会发现使用了分组法之后的时间复杂度明显下降了非常多,而分组法就是对希尔排序的核心思想

原理

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

1.希尔排序是对直接插入排序的优化
2.当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。

假设有一组需要使用希尔排序的数据如下所示:
在这里插入图片描述
此时一共是15个数据.
1:一开始我们就分为五组:每组三个数据,所以图中有五条彩线

注意每个数据的中间差四个数据

在这里插入图片描述
然后先对红色线的数字进行直接排序,先排12和8
在这里插入图片描述
此时注意,按照之前的直接排序我们肯定会直接将i指向7然后,j指向12,然后再进行排序,但是希尔排序此处并不是这样的,此时我们的i应该指向12的下一个数字27,然后j指向8的下一个数字5,我们会发现此时到了蓝色线所对应的数字,然后再进行直接排序,排序完成后继续往下走,i指向58,j指向9,然后在进行直接排序,就这样依次往下循环往复,最终第一轮按照五组的排序的结果如下所示:
在这里插入图片描述
2:此时我们再分为三组,每组五个数据:
此时就有了三个颜色的线
在这里插入图片描述
然后再将i指向16,j指向7后进行直接排序,排完序后让i指向0,j指向4后继续进行直接排序,直到最终完成第二轮的排序,结果如下所示:
在这里插入图片描述
3:此时将整体看为一组再进行最终的排序
也就是将i指向0,j指向5,然后按照之前我们所讲的直接排序方法进行排序即可,因为整体已经趋于有序了,所以此时整体看为一组,速度更快.

在这里解释一下为什么我们要使用5,3,1作为我们的分组
1:首先5,3,1叫做增量数据,并且希尔排序中的这个取值也一定是一个素数,也就是我们的质数.假设我们使用2就不行,因为2不是质数.
并且注意增量序列的值没有除1之外的公因子,并且最后一个增量值必须等于1.
2:在解释下什么叫做增量,例如我们分为五组的话,每组是三个数据,如果分为三组的话,每组是五个数据,最后分为一组的话。每组是十五个数据,所以最终我们所获取的每组的数据是保持上升态势的,这样的情况就叫做增量.
在这里插入图片描述

代码示例
public static void shell(int[] array ,int gap) {
        for (int i = gap; i < array.length; i++) {
            int tmp = array[i];
            int j = i-gap;
            for (; j >= 0 ; j = j-gap) {
                //如果这里是一个大于等于号 此时这个排序就不稳定了
                if(array[j] > tmp) {
                    array[j+gap] = array[j];
                }else {
                    //array[j+1] = tmp;
                    break;
                }
            }
            array[j+gap] = tmp;
        }
    }

    /**
     * 了解:
     * 时间复杂度:O(1.5)  O(n^2)
     * 空间复杂度:O(1)
     * 稳定性:不稳定
     * @param array
     */
    public static void shellSort(int[] array) {
        int[] drr = {5,3,1};//增量数组-->   16   5     3     1
        for (int i = 0; i < drr.length; i++) {
            shell(array,drr[i]);
        }
    }
性能分析

在这里插入图片描述

选择排序

原理

假设此时有一组需要通过选择排序的数据如下所示:
在这里插入图片描述
1:首先定义两个下标,i和j,i指向第一个元素,j指向i+1所对应的元素,假设此时j所指向的元素大于i的话,那么就交换两个数据,如下所示:
在这里插入图片描述
2:然后将j这个下标继续向后移动,发现9比5大,那就继续朝后移动,到了16发现比5大,继续往后移动到了6,发现6比5大,欧克此时j继续往后走以后,这一趟就结束了,不再动了。
在这里插入图片描述

3:此时i继续往下走,走到了12这个位置,j走到i+1的位置也就是我们的9这个位置:
在这里插入图片描述
此时9<12,欧克交换位置:
在这里插入图片描述
然后j继续往后走发现6比9小,那就继续交换位置:
在这里插入图片描述
这一趟走完了,继续下一趟:
4:此时i指向12,j指向16,16大于12,j继续往下走,发现12大于9,则交换:
在这里插入图片描述

这一趟结束
5:继续下一趟,i指向16,j指向12,然后16>12,则进行交换,最终排序完毕.
在这里插入图片描述

代码示例
public class MapDemo1 {
    public static void chooseSort(int[] array) {
        for(int l = 0;l < array.length-1;l++){
            for(int k = l+1;k < array.length;k++) {
                if(array[l]>array[k]) {
                    int tmp = array[l];
                    array[l] = array[k];
                    array[k] = tmp;
                }
            }
        }

    }

    public static void main(String[] args) {
        int[] array = {12,5,9,16,6,23,1,45,24,13};
        chooseSort(array);
        System.out.println(Arrays.toString(array));
    }
}
性能分析

在这里插入图片描述

堆排序

之前在对应文章也讲到了堆排序的写法,这里就不再做过多的阐述,放上对应的链接:
点我进入对应文章
关于堆排序的时间复杂度,空间复杂度:

在这里插入图片描述

冒泡排序

时间复杂度:O(N^2)
空间复杂度:O(1)
稳定性:稳定

点我进入对应文章

快速排序(重要,常考)

概念

快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,递归地以达到整个序列有序的目的。

快速排序的三个步骤

(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot)

(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大

(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。

选择基准的方式
方式1 固定位置(书本上介绍的内容)

快速排序采用的是分治思想:下面我先拿一个来举例子:
在这里插入图片描述
1:先定义两个下标,low下标和high下标,low下标指向0下标处,high下标指向9下标处,此时这个low对应的值6就是我们的基准值.我们把这个基准值待会先放到tmp变量当中.
在这里插入图片描述
2.然后开始从后向前寻找比6小的数字, 此时5比6小,就把5给我们的low
在这里插入图片描述
3:然后从前向后开始寻找比6大的数字,此时low指向了我们的7,就把7放到high所在的位置.
在这里插入图片描述
4.然后让high继续往前走,走到了6下标的位置,发现6下标的4要比基准值6小,就把4放到low的位置处,然后low继续往后走,走到了4下标处.
在这里插入图片描述
5:然后此时9大于6,就将9放到high所指向的位置处.
在这里插入图片描述
6:然后high继续往前走到了5下标处,此时的3小于6,所以将3这个数字放到我们的low所指向的位置:
在这里插入图片描述
7.然后low此时指向了high所指向的位置,那么我们将这个6这个基准放到这个下标处:此时pivot指向我们的基准.
在这里插入图片描述
此时我们会发现6左边的元素都是比自己小的元素,6右边的元素都是比自己大的元素.从而这个时候就可以采用分治的思想了
8:先对6的左边采用分治思想在这里插入图片描述
此时high指向pivot-1,low指向第一个元素然后先对6左边的区域进行分治
9:对于左边的区域的思想跟刚才的思想其实是一样的,拿出一个基准值5,然后从左到右找比5大的数字,从右到左找比5小的数字,交换方式还是跟刚才的方式相同,这里就不再做过多的赘述.,最后的结果如下:
在这里插入图片描述

10.然后low指向0下标处,high指向privot-1,也就是新的位置(下标3处)
在这里插入图片描述

11.然后继续进行调整,调整完毕后如下所示:
在这里插入图片描述
12.此时继续往下走,将low调整到0下标处,将high调整到1下标处,然后再进行调整,此时我们6的左区间排序完毕.

13.此时我们对6的左边的区间全部排序完毕,按照相同的做法对6右边的区间全部进行一遍排序,方法还是跟之前一样,low指向9,high指向8,然后重复之前的操作便可以完成排序.

在这里分治的思想其实就是对左右区间做之前相同的排序操作.只不过每次需要找到我们的基准值.
其实快速排序的核心思想就是寻找我们的基准,然后再对这个基准的左右区间采用分治的思想,所以整体来说,思想跟二叉树的前序遍历是一个思想,所以其实可以将这个过程转换为二叉树遍历的写法来编写我们快速排序的代码.

代码示例
public class MapDemo1 {
    //pivot方法的作用就是对每趟的数组进行对应的排序和寻找我们最终的基准值
    //pivot方法是我们的核心方法
    public static int pivot(int[] array, int start, int end) {
        int tmp = array[start];
        while (start < end) {
            //注意这里有两个判断条件,条件1 start<end是针对当我们使用我们的end是不能超过start的,否则就数组越界了
            //第二点就是我们从右往左找是要寻找比当前tmp中的值小的元素的,所以判断条件要注意.
            //并且要注意的是,假设此时我们的tmp中的值与array[end]中的值相等的话,此时end也是要往前--的,所以最终我们的array[end]要>=tmp,不要忘记等号
            while (start < end && array[end] >= tmp) {
                end--;
            }
            //假设此时end不再往左--且start<end的时候,说明找到了比tmp小的数字,则进行交换
            array[start] = array[end];
            //这里的两个判断条件以及原因跟上述相同
            while (start < end && array[start] <= tmp) {
                start++;
            }
            //假设此时start不再往右++且start<end的时候,说明找到了比tmp大的数字,则进行交换
            array[end] = array[start];
        }
        //最后start下标会与我们的end下标重合,代表这一趟的快排结束了,此时需要将tmp中的值赋给我们的array[start]
        array[start] = tmp;
        //此时我们start和end重合的位置就是我们每一趟排序的基准,返回start
        return start;
    }


    //时间复杂度空间复杂度在quick方法这里看
    public static void quick(int[] array, int low, int high) {
        if (low < high) {
            //先对原数组进行一遍排序后找到第一个基准值
            int piv = pivot(array, low, high);
            //以这个基准值为准开始先对这个基准值的左空间进行排序
            //使用递归
            quick(array, low, piv - 1);
            //以这个基准值为准开始再对这个基准值的右空间进行排序
            quick(array, piv + 1, high);
        }
    }

    //快速排序quickSort方法
    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }

    public static void main(String[] args) {
        int[] array = {12, 5, 9, 16, 6, 23, 1, 45, 24, 13};
        quickSort(array);
        System.out.println(Arrays.toString(array));
    }
}
性能分析

在这里插入图片描述

注意:时间复杂度,空间复杂度在quick方法这里看

最好的时间复杂度为O(nlogn)的原因就是因为快速排序算法的时间复杂度一般为递归的次数乘以我们当前遍历的次数,递归的次数就相当于我们递归左右区间的次数,相当于一个二叉树的遍历,而这个递归的次数又跟我们二叉树的高度有关,所以说递归的次数相当于二叉树的高度为logn,而我们遍历的次数就是n次,因为每递归一次遍历的次数都是n次,所以最好情况下的快排的时间复杂度为O(nlogn).
最坏情况下的时间复杂度为O(N^2),前提是这个需要采用快速排序的数组是一个有序的数组:例如1,2,3,4,5,6,7,8.

ok,到了这里我们就可以对这种有序的情况去进行优化:此时就有以下种方式去进行优化

方式2 随机选取基准(不重要)

引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴

做法:就是随机找到后边的一个下标,然后和low下标的数据进行交换,最后以low下标交换后的值作为基准

方式3 三数取中(median-of-three)(优化有序的数据)

三数取中法就是找到这组数据的第一个,最后一个以及中间数据,这三个数据对应的下标分别为low,high,mid,最终我们是要保证array[mid] <= array[low] <= array[end],也就是说假设一个有序的数组1,2,3,4,5,6,7,8.此时拿我们的三数取中法的话,一开始的基准值为array[3] = 4,不再是1,所以一开始假如我们的array[low]<array[mid]的话,就需要将两者进行交换,假如array[low] > array[high]也需要进行交换,假如array[mid] > array[high]的话,也同样需要进行交换.下面来看代码:

代码示例

假设此时我们不使用三数取中法的话,来看我们针对1万个数字的排序时间:

public class MapDemo1 {
    public static int pivot(int[] array, int start, int end) {
        //一开始的基准值等于数组的第一个元素
        int tmp = array[start];
        while (start < end) {
            //注意这里有两个判断条件,条件1 start<end是针对当我们使用我们的end是不能超过start的,否则就数组越界了
            //第二点就是我们从右往左找是要寻找比当前tmp中的值小的元素的,所以判断条件要注意.
            //并且要注意的是,假设此时我们的tmp中的值与array[end]中的值相等的话,此时end也是要往前--的,所以最终我们的array[end]要>=tmp,不要忘记等号
            while (start < end && array[end] >= tmp) {
                end--;
            }
            //假设此时end不再往左--且start<end的时候,说明找到了比tmp小的数字,则进行交换
            array[start] = array[end];
            //这里的两个判断条件以及原因跟上述相同
            while (start < end && array[start] <= tmp) {
                start++;
            }
            //假设此时start不再往右++且start<end的时候,说明找到了比tmp大的数字,则进行交换
            array[end] = array[start];
        }
        //最后start下标会与我们的end下标重合,代表这一趟的快排结束了,此时需要将tmp中的值赋给我们的array[start]
        array[start] = tmp;
        //此时我们start和end重合的位置就是我们每一趟排序的基准
        return start;
    }

    public static void quick(int[] array, int low, int high) {
        if (low < high) {
            //然后再开始排序和找基准值
            int piv = pivot(array, low, high);
            //对左序列进行快速排序
            quick(array, low, piv - 1);
            //对右序列进行快速排序
            quick(array, piv + 1, high);
        }
    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }


    public static void main(String[] args) {
        int[] array = new int[1_0000];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        long startTime = System.currentTimeMillis();
        quickSort(array);
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }
}

运行时间为13s
在这里插入图片描述
再来看我们使用三数取中法后:

public class MapDemo1 {
    public static void swap(int[] array, int k, int i) {
        int tmp = array[k];
        array[k] = array[i];
        array[i] = tmp;
    }

    /**
     * 三数取中法medianOfThree
     * @param array
     * @param low
     * @param high
     */

    public static void medianOfThree(int[] array, int low, int high) {
        int mid = (low + high) / 2;
        //我们的目标是要保证后面的关系:array[mid] <= array[low] <= array[end]

        //array[mid] <= array[low]
        if (array[low] < array[mid]) {
            swap(array, low, mid);
        }

        //array[low] <= array[high]
        if (array[low] > array[high]) {
            swap(array, low, high);
        }

        //array[mid] <= array[high]
        if (array[mid] > array[high]) {
            swap(array, mid, high);
        }
    }

    public static int pivot(int[] array, int start, int end) {
        //一开始的基准值等于数组的第一个元素
        int tmp = array[start];
        while (start < end) {
            //注意这里有两个判断条件,条件1 start<end是针对当我们使用我们的end是不能超过start的,否则就数组越界了
            //第二点就是我们从右往左找是要寻找比当前tmp中的值小的元素的,所以判断条件要注意.
            //并且要注意的是,假设此时我们的tmp中的值与array[end]中的值相等的话,此时end也是要往前--的,所以最终我们的array[end]要>=tmp,不要忘记等号
            while (start < end && array[end] >= tmp) {
                end--;
            }
            //假设此时end不再往左--且start<end的时候,说明找到了比tmp小的数字,则进行交换
            array[start] = array[end];
            //这里的两个判断条件以及原因跟上述相同
            while (start < end && array[start] <= tmp) {
                start++;
            }
            //假设此时start不再往右++且start<end的时候,说明找到了比tmp大的数字,则进行交换
            array[end] = array[start];
        }
        //最后start下标会与我们的end下标重合,代表这一趟的快排结束了,此时需要将tmp中的值赋给我们的array[start]
        array[start] = tmp;
        //此时我们start和end重合的位置就是我们每一趟排序的基准
        return start;
    }

    public static void quick(int[] array, int low, int high) {
        if (low < high) {
            //先使用三数取中法
            medianOfThree(array, low, high);
            //然后再开始排序和找基准值
            int piv = pivot(array, low, high);
            //对左序列进行快速排序
            quick(array, low, piv - 1);
            //对右序列进行快速排序
            quick(array, piv + 1, high);
        }
    }

    public static void quickSort(int[] array) {
        quick(array,0,array.length-1);
    }


    public static void main(String[] args) {
        int[] array = new int[1_0000];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        long startTime = System.currentTimeMillis();
        quickSort(array);
        long endTime = System.currentTimeMillis();
        System.out.println(endTime-startTime);
    }
}

我们会发现此时运行的时间为1s.
在这里插入图片描述

方式4

快速排序在一直执行的过程中是逐渐趋于有序的,那么对于逐渐趋于有序的数字来说使用直接插入排序会更加的提升效率.
我们在方式3的基础上再进行一次优化,当high与low的距离小于50的时候,这时候我们的数组一定是趋于有序的数组了,所以可以直接使用直接插入排序对我们的代码进行优化

优化前:假设此时我们不使用直接插入排序的话:我们拿方式3中的代码对100万条数据排序的耗时是17s
在这里插入图片描述

优化后:使用直接插入排序后的算法的耗时是10s

public class MapDemo1 {
    public static void swap(int[] array, int k, int i) {
        int tmp = array[k];
        array[k] = array[i];
        array[i] = tmp;
    }

    /**
     * 三数取中法medianOfThree
     *
     * @param array
     * @param low
     * @param high
     */

    public static void medianOfThree(int[] array, int low, int high) {
        int mid = (low + high) / 2;
        //我们的目标是要保证后面的关系:array[mid] <= array[low] <= array[end]

        //array[mid] <= array[low]
        if (array[low] < array[mid]) {
            swap(array, low, mid);
        }

        //array[low] <= array[high]
        if (array[low] > array[high]) {
            swap(array, low, high);
        }

        //array[mid] <= array[high]
        if (array[mid] > array[high]) {
            swap(array, mid, high);
        }
    }

    public static int pivot(int[] array, int start, int end) {
        //一开始的基准值等于数组的第一个元素
        int tmp = array[start];
        while (start < end) {
            //注意这里有两个判断条件,条件1 start<end是针对当我们使用我们的end是不能超过start的,否则就数组越界了
            //第二点就是我们从右往左找是要寻找比当前tmp中的值小的元素的,所以判断条件要注意.
            //并且要注意的是,假设此时我们的tmp中的值与array[end]中的值相等的话,此时end也是要往前--的,所以最终我们的array[end]要>=tmp,不要忘记等号
            while (start < end && array[end] >= tmp) {
                end--;
            }
            //假设此时end不再往左--且start<end的时候,说明找到了比tmp小的数字,则进行交换
            array[start] = array[end];
            //这里的两个判断条件以及原因跟上述相同
            while (start < end && array[start] <= tmp) {
                start++;
            }
            //假设此时start不再往右++且start<end的时候,说明找到了比tmp大的数字,则进行交换
            array[end] = array[start];
        }
        //最后start下标会与我们的end下标重合,代表这一趟的快排结束了,此时需要将tmp中的值赋给我们的array[start]
        array[start] = tmp;
        //此时我们start和end重合的位置就是我们每一趟排序的基准
        return start;
    }

    //插入排序
    public static void insertSortBount(int[] array, int low, int high) {
        for (int i = low + 1; i <= high; i++) {
            int tmp = array[i];
            int j = i - 1;
            for (; j >= low; j--) {
                if (array[j] > tmp) {
                    array[j + 1] = array[j];
                } else {
                    break;
                }
            }
            array[j + 1] = tmp;
        }
    }

    public static void quick(int[] array, int low, int high) {
        if (low >= high) {
            return;
        }
        //对距离为50以内的实行插入排序
        if (high - low + 1 <= 50) {
            //使用插入排序
            insertSortBount(array, low, high);
            //记着这里一定要return  这里说明 这个区别范围有序了 直接结束
            return;
        }
        //先使用三数取中法
        medianOfThree(array, low, high);
        //然后再开始排序和找基准值
        int piv = pivot(array, low, high);
        //对左序列进行快速排序
        quick(array, low, piv - 1);
        //对右序列进行快速排序
        quick(array, piv + 1, high);


    }

    public static void quickSort(int[] array) {
        quick(array, 0, array.length - 1);
    }


    public static void main(String[] args) {
        int[] array = new int[100_0000];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        long startTime = System.currentTimeMillis();
        quickSort(array);
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
    }
}

在这里插入图片描述

归并排序

概念

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
在这里插入图片描述
在这里插入图片描述

原理讲解

其实关于分割其实非常好理解,因为分割其实就是拆分,而我们归并的原理是这样的,当我们已经拆分完毕后就必须开始使用归并了,归并的时候是这样进行归并的
1:假如此时我们要归并下面的数组,就需要分别再两个数组内部定义一个s1,e1,s2,e2,s1s2分别指向我们两个需要归并的数组的头,e1e2分别指向我们两个需要归并的数组的尾
在这里插入图片描述
2:然后开始向后遍历,此时如果s1>s2,就把s2先放入到我们的tmp数组当中去,然后说s2往后走一步
在这里插入图片描述
3:然后再比较s1和s2,发现s1<s2,于是就把s1放入到tmp数组当中去,然后s1向后移动
在这里插入图片描述
4:然后继续比较s1和s2的大小,发现s1>s2,于是7入数组,s2向后移动
在这里插入图片描述
此时s2>e2了,说明第二段已经没有数据了,没有数据的话就将第一段剩下的值全部放入我们的tmp数组当中去.
此时1,6,7,10完成了排序.

代码示例

注意代码中的注释:里面写出了为什么要那么做

import java.util.Arrays;

public class MapDemo1 {
    public static void merge(int[] array, int start, int mid, int end) {
        //s1的值就是递归进入的时候的start下标,而s2的值就是递归进入的时候的mid+1,e1就是mid,e2是end
        int s1 = start;
        int s2 = mid + 1;

        //注意我们数组最终大小为end-start+1
        int[] tmp = new int[end - start + 1];
        //k代表tmp数组的下标
        int k = 0;

        while (s1 <= mid && s2 <= end) {
            if (array[s1] <= array[s2]) {
                //注意这里是小于等于,因为归并两个有序组的时候,s1和s2指向的数字可能是相同的,而为了保证s1所指向的数字在最终插入到tmp数组的时候是在s2所指向的数字的前面的,要小于等于
                //而这个小于等于也保证了最终的我们的归并排序是一个稳定的排序
                tmp[k++] = array[s1++];
            } else {
                tmp[k++] = array[s2++];
            }
        }
        //此处代表第一段还有数据,但是第二段没有数据了
        while (s1 <= mid) {
            //那就将第二段中的剩余数据插入tmp当中
            tmp[k++] = array[s1++];
        }
        //此处代表第二段还有数据,但是第一段没有数据了
        while (s2 <= end) {
           //那就将第一段中的剩余数据插入到tmp当中去
            tmp[k++] = array[s2++];
        }

        for (int i = 0; i < tmp.length; i++) {
            //注意这里我们将排序好的数组现在要归并到原数组当中去了,但是有个问题就是该怎样按照顺序将排序好的元素替换到原数组当中去,有些同学想了,那这个简单了直接使用array[i]=tmp[i]
            //但是注意这样的写法是错误的,原因是当我们左区间递归合并完毕后是没有问题的,但是假如到了右边呢?此时如果还是array[i]=tmp[i]相当于原数组对应的左区间的元素都被替换了,原数组右边的元素还是没有被替换
            //
            array[i + start] = tmp[i];
        }
    }


    //时间复杂度和空间复杂度需要看我们的mergeSortInternal方法
    public static void mergeSortInternal(int[] array, int low, int high) {
        if (low >= high) {
            return;
        }
        int mid = (low + high) / 2;
        mergeSortInternal(array, low, mid);
        mergeSortInternal(array, mid + 1, high);
        //合并的操作
        merge(array, low, mid, high);
    }


    public static void mergeSort(int[] array) {
        mergeSortInternal(array, 0, array.length - 1);
    }

    public static void main(String[] args) {
        int[] array = new int[1_0000];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }

        mergeSort(array);
        System.out.println(Arrays.toString(array));
    }
}

下面是对代码的部分图示讲解:
在这里插入图片描述

性能分析

在这里插入图片描述
至于为什么是稳定排序代码里面已经有了注释,这里就不再做过多的赘述,但是要注意的一点是,空间复杂度为O(N)是因为我们在merge方法中定义了一个大小为N的tmp的数组去存储排序后的元素.,而时间复杂度为O(N*log(N))的原因是mergeSortInternal方法中我们既有递归方法也有merge中对于元素的遍历,归并排序算法的时间复杂度一般为递归的次数乘以我们当前遍历的次数.

海量数据的排序问题

外部排序:排序过程需要在磁盘等外部存储进行的排序前提:内存只有 1G,需要排序的数据有 100G
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序
1.先把文件切分成 200 份,每个 512 M
2.分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以
3.进行 200 路归并,同时对 200 份有序文件做归并过程,最终结果就有序了
这块只需要知道思想,可以给面试官说出来,代码不需要掌握.

排序的总结

在这里插入图片描述

在这里插入图片描述

此时假设规定只能使用空间复杂度为O(1) 的算法的话,那就只能堆排序
如果要保证排序最快的话就使用快排
然后最主要掌握的是堆排序,快速排序以及归并排序,希尔排序不用掌握,然后前三个排序是基础.

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值