世界上最好的排序算法是什么?

在计算机世界有很多种排序算法,比如冒泡排序、插入排序、选择排序、快速排序、基数排序、桶排序等等。我们通常可以把排序分为比较类排序、非比较类排序。
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破 O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
 这么多排序算法,你知道最好的排序算法是什么吗?目前世界上通常情况下最好的算法是一种叫做"快速排序(Quicksort)"的算法,他是由英国计算机科学家托尼·霍尔(Tony Hoare)于 1959 年想到的,1961 年发表的,这个算法也成了今天世界计算机产业中使用最多的排序算法,霍尔因此获得了爵士头衔,也成为第一个获得这种头衔的科学家。那么快速排序为什么快呢?原因很简单,少做事情,其原理大致是这样的:
首先,对于一大堆无序的数字,从中随机挑选一个,比如是 53,这个被随机选上的数字被称为枢值(枢纽的枢),接下来,将所有要排序的数字分成两部分,第一部分是大于等于枢值 53 的,第二部分是小于枢值 53 的。在第一步完成后,一大堆无序的数字就变得稍微有序一点了。
第二步,从上面得到的两堆数字,分别采用第一步的方法各自再找一个枢值。对于第一堆,由于所有的数字都比 53 大,至少也等于 53,因此,第二次随机挑选的枢值肯定是一个大于 53 的数字,比如 79;类似地,对于第二堆,由于所有的数字都小于 53,因此第二次随机挑选的枢值肯定小于它,比如 4。接下来,再把两堆数字各自分成大于等于相应枢值的数字序列,以及小于枢值的数字序列。这样做下来,原来的一大堆数就变成了四小堆,它们分别是小于 4 的数字,介于 4 到 53 之间的,介于 53 到 79 之间的,以及大于或等于 79 的。
再接下来,用同样的方法,四堆变八堆,八堆变十六堆,很快所有的数字就排好序了。这种算法通常情况下复杂度也是 N 乘以 log(N)。
 Talk is cheap, show you the code.
** * @program: Algorithm * @description: 排序算法演练 * @author: NickCheng * @create: 2021-03-07 18:45 **/
public class Sorts {
    /**     * 冒泡排序,a表示数组,n表示数组大小     * */    public static void bubbleSort(int[] a, int n) {        long beginTime = System.currentTimeMillis();        if (n <= 1) return;
        for (int i = 0; i < n; ++i) {            // 提前退出冒泡循环的标志位            boolean flag = false;            for (int j = 0; j < n - i - 1; ++j) {                if (a[j] > a[j+1]) { // 交换                    int tmp = a[j];                    a[j] = a[j+1];                    a[j+1] = tmp;                    flag = true;  // 表示有数据交换                }            }            if (!flag) break;  // 没有数据交换,提前退出        }        System.out.println("冒泡排序时间:"+ (System.currentTimeMillis() - beginTime) );    }
    /**     * 插入排序     *     * @param a 表示数组     * @param n 表示数组大小     */    public static void insertionSort(int[] a, int n) {        long beginTime = System.currentTimeMillis();        if (n <= 1) return;
        for (int i = 1; i < n; ++i) {            int value = a[i];            int j = i - 1;            // 查找插入的位置            for (; j >= 0; --j) {                if (a[j] > value) {                    a[j+1] = a[j];  // 数据移动                } else {                    break;                }            }            a[j+1] = value; // 插入数据        }        System.out.println("插入排序时间:"+ (System.currentTimeMillis() - beginTime) );    }
    /**     * 快速排序     * @param a     */    public static void quickSort(int[] a, int n) {        long beginTime = System.currentTimeMillis();        int len;        if(a == null                || (len = a.length) == 0                || len == 1) {            return ;        }        quickSortC(a, 0, len - 1);        System.out.println("快速排序时间:"+ (System.currentTimeMillis() - beginTime) );    }
    /**     * 快排核心算法,递归实现     * @param array 数字     * @param left 左下标     * @param right 右下标     */    public static void quickSortC(int[] array, int left, int right) {        if(left > right) {            return;        }        // base中存放基准数        int base = array[left];        int i = left, j = right;        while(i != j) {            // 顺序很重要,先从右边开始往左找,直到找到比base值小的数            while(array[j] >= base && i < j) {                j--;            }
            // 再从左往右边找,直到找到比base值大的数            while(array[i] <= base && i < j) {                i++;            }
            // 上面的循环结束表示找到了位置或者(i>=j)了,交换两个数在数组中的位置            if(i < j) {                int tmp = array[i];                array[i] = array[j];                array[j] = tmp;            }        }
        // 将基准数放到中间的位置(基准数归位)        array[left] = array[i];        array[i] = base;
        // 递归,继续向基准的左右两边执行和上面同样的操作        // i的索引处为上面已确定好的基准值的位置,无需再处理        quickSortC(array, left, i - 1);        quickSortC(array, i + 1, right);    }
    /**     * 计数排序,a是数组,n是数组大小。假设数组中存储的都是非负整数     */    public static void countingSort(int[] a, int n) {        long beginTime = System.currentTimeMillis();        if (n <= 1) return;
        // 查找数组中数据的范围        int max = a[0];        for (int i = 1; i < n; ++i) {            if (max < a[i]) {                max = a[i];            }        }
        int[] c = new int[max + 1]; // 申请一个计数数组c,下标大小[0,max]        for (int i = 0; i <= max; ++i) {            c[i] = 0;        }
        // 计算每个元素的个数,放入c中        for (int i = 0; i < n; ++i) {            c[a[i]]++;        }
        // 依次累加        for (int i = 1; i <= max; ++i) {            c[i] = c[i-1] + c[i];        }
        // 临时数组r,存储排序之后的结果        int[] r = new int[n];        // 计算排序的关键步骤,有点难理解        for (int i = n - 1; i >= 0; --i) {            int index = c[a[i]]-1;            r[index] = a[i];            c[a[i]]--;        }
        // 将结果拷贝给a数组        for (int i = 0; i < n; ++i) {            a[i] = r[i];        }        System.out.println("计数排序时间:"+ (System.currentTimeMillis() - beginTime) );    }
    /**     * 生成随机数组     * */    public static int[]  gennerateArray(int len,int max){        int[] arr=new int[len];        for(int i=0;i<arr.length;i++){            arr[i]=(int)(Math.random()*max);        }        return arr;    }
    public static void main(String[] args){        //随机生成一个数组        int[] array = gennerateArray(100000,100000);        int arraySize = array.length;        bubbleSort(array,arraySize);        insertionSort(array,arraySize);        quickSort(array,arraySize);        countingSort(array,arraySize);    }}如果你将上面代码跑一下你就会发现,当执行快速排序的时候会出现了堆栈溢出的情况。该例子中快速排序使用了递归,而使用递归则要警惕堆栈溢出。那么如何解决该问题?有如下两个方案:第一种是限制递归深度。一旦递归过深,超过了我们事先设定的阈值,就停止递归。第二种是通过在堆上模拟实现一个函数调用栈,手动模拟递归压栈、出栈的过程,这样就没有了系统栈大小的限制。接下来,我们看下优化后的快速排序方法。
3 种快排优化方法。
    /**     *     *优化:将第一个元素为基准值改为随机一个元素为基准值     *降低退化为O(n^2)的概率     *数学期望值为O(nlogn)     *这样就不会受“几乎有序的数组”的干扰了     *但是对“几乎乱序的数组”的排序性能可能会稍微下降,至少多了排序前交换的那部分     *乱序时这个交换没有意义     * */    public static void quickSort2(int[]arr,int n) {        quickSort2(arr,0,n-1);    }
    private static void quickSort2(int[] arr, int l, int r) {        if(l>=r)            return;        int p = midal(arr,l,r);        quickSort2(arr, l, p-1);        quickSort2(arr, p+1, r);
    }
    private static int midal(int[] arr, int l, int r) {        swap(arr,l,(int)Math.random()*(r-l+1)+l);        //满足arr[l+1,i)<=v,arr(j,r]>=v        int v=arr[l];        int i=l+1,j=r;        while(true) {            //从左到右扫描,扫描出第一个比base大的元素,然后i停在那里            while(arr[i]<v&&i<r)//arr[i]不能=v,会导致v聚集在一边                i++;            //从右到左扫描,扫描出第一个比base小的元素,然后j停在那里            while(arr[j]>v&&j>=l)                j--;            if(i>=j)                break;            swap(arr, i, j);            i++;            j--;        }        //将基准值交换到合适位置        swap(arr, l, j);        return j;    }
    private static void swap(int[] arr, int i, int j) {        if(i!=j) {            int temp=arr[i];            arr[i]=arr[j];            arr[j]=temp;        }    }
    /**     *二路快排:对于方一方二中如果存在大量重复元素,当基准值为重复元素时     *等于base的这些会聚集到右侧(或者稍微改改大小关系就会聚集到左侧)。     *总之就会聚集到一边。这样在数组中重复数字很多的时候,     *就又会导致两边子递归规模差距悬殊的情况,很有可能退化为O(n^2)     *这时想把等于base的那些数分派到base两边,而不是让他们聚集到一起。     *     *优化:可以将swap去掉     */    public static void quickSort3(int[]arr,int n) {        quickSort3(arr,0,n-1);    }
    private static void quickSort3(int[] arr, int l, int r) {        if(l>=r)            return;        int p=Midal(arr,l,r);        quickSort3(arr, l, p-1);        quickSort3(arr, p+1, r);
    }
    private static int Midal(int[] arr, int l, int r) {        swap3(arr,l,(int)Math.random()*(r-l+1)+l);        //满足arr[l+1,i)<=v,arr(j,r]>=v        int v=arr[l];        int i=l+1,j=r;        while(true) {            //从左到右扫描,扫描出第一个比base大的元素,然后i停在那里            while(arr[i]<v&&i<r)//arr[i]不能=v,会导致v聚集在一边                i++;            //从右到左扫描,扫描出第一个比base小的元素,然后j停在那里            while(arr[j]>v&&j>=l)                j--;            if(i>=j)                break;            swap3(arr, i, j);            i++;            j--;        }        //将基准值交换到合适位置        swap3(arr, l, j);        return j;    }    private static void swap3(int[] arr, int i, int j) {        if(i!=j) {            int temp=arr[i];            arr[i]=arr[j];            arr[j]=temp;        }    }
    /**     *三路快排:当大量数据,且重复数多时,用三路快排     *将整个数组分为小于v,等于v,大于v三部分     */    public static void quickSort4(int[]arr,int n) {        quickSort4(arr,0,n-1);
    }    private static void quickSort4(int[] arr, int l, int r) {        if(l>=r)            return;        //因为==v的部分是一个数组而非单独的一个数,所以直接处理,不调用函数        swap(arr,l,(int)Math.random()*(r-l+1)+l);        int v=arr[l];        //变量定义要保证初始空间为空        int i=l+1;//arr[lt+1,i)==v        int lt=l;//arr[l+1,lt]<v        int gt=r+1;//arr[gt,r]>v            while (i<gt) {            if(arr[i]==v) {                i++;            }else if (arr[i]<v) {                swap(arr, i, lt+1);//画示意图,arr[lt+1]==v                i++;                lt++;            }else {                swap(arr, i, gt-1);//arr[gt-1]为未处理的数据                gt--;            }        }        swap(arr, l, lt);        quickSort4(arr, l, lt-1);        quickSort4(arr, gt, r);    }     /**     * 生成随机数组     * */    public static int[]  gennerateArray(int len,int max){        int[] arr=new int[len];        for(int i=0;i<arr.length;i++){            arr[i]=(int)(Math.random()*max);        }        return arr;    }你可能会问,有没有可能发明一种比快速排序更好的算法?从科学上讲,答案是否定的。因为从数学上可以证明 N 个任意随机数的排序,复杂度不可能比 N 乘以 log(N)更低,这是数学给出的极限,或者边界。
参考:《谷歌方法论》、《数据结构与算法之美》
版权声明: 本文为 InfoQ 作者【Nick】的原创文章。
原文链接:【http://xie.infoq.cn/article/13e7d271c3ca39e839e861cd0】。文章转载请联系作者。
Nick
终身学习,向死而生 2020.03.18 加入
得到、极客时间重度学习者,来infoQ 是为了输出倒逼输入











    
评论