数据结构与算法之美-08 |排序

一、什么是排序

所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。

二、怎么分析一个排序算法

一.排序算法的执行效率

1、最好情况,最坏情况,平均情况时间复杂度分析

为什么要区分这三种时间复杂度呢?第一,有些排序算法会区分,为了好对比,所以最好都做一下区分。第二,对于要排序的数据,有的接近有序,有的完全无序。有序度不同的数据,对于排序的执行时间肯定是有影响的,要知道排序算法在不同数据下的性能表现。

2、时间复杂度的系数、常数 、低阶

对于不同规模的数据,排序算法的性能不同。

3、比较次数和交换(或移动)次数

基于比较的排序算法的执行过程,会涉及两种操作,一种是元素比较大小,另一种是元素交换或移动。所以,如果在分析排序算法的执行效率的时候,应该把比较次数和交换(或移动)次数也考虑进去。

4、有序度和逆序度

有序度是数组中具有有序关系的元素对的个数。
逆序度的定义正好跟有序度相反(默认从小到大为有序)
逆序度 = 满有序度 - 有序度
比如一个数组[0,1,2,3,4,5,6,7,8,9],从小到大排列,这个就是完全有序的数组,有序度是9+8+…+3+2+1=N*(N-1)/2=45
 

二.排序算法的内存消耗

算法的内存消耗可以通过空间复杂度来衡量

三.排序算法的稳定性

仅仅用执行效率和内存消耗来衡量排序算法的好坏是不够的。针对排序算法,还有一个重要的度量指标,稳定性。这个概念是说,如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变。

三、常见排序算法

排序算法 是否原地排序 是否稳定 最好 最坏 平均 适用于
冒泡排序 O(N) O(N^2) O(N^2) 小数据量
插入排序 O(N) O(N^2) O(N^2) 小数据量
选择排序 O(N^2) O(N^2) O(N^2) 小数据量
归并排序 O(NlogN) O(NlogN) O(NlogN) 大数据量
快速排序 O(NlogN) O(N^2) O(NlogN) 大数据量
桶排序 不一定,看桶内排序 O(N) O(NlogN) O(N) 特定数据类型
计数排序 O(N) O(NlogN) O(N) 特定数据类型
基数排序 O(DN) O(DN) O(DN) 位排序

一.冒泡排序(Bubble Sort,适合较小规模数据排序)

冒泡排序是进行循环比较,每次对比的是第X个元素和第X+1个元素的大小(X及X+1小于元素总数N)
如果不满足大小关系需求,则进行数据交换,一次冒泡至少会让一个元素移动到它该存在的位置。
 

1、冒泡排序代码

// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
  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;  // 没有数据交换,提前退出
  }
}

2、冒泡排序是原地排序算法吗?

冒泡的过程只涉及相邻数据的交换操作,只需要常量级的临时空间,所以它的空间复杂度为 O(1),是一个原地排序算法。

3、冒泡排序是稳定的排序算法吗?

在冒泡排序中,只有交换才可以改变两个元素的前后顺序。为了保证冒泡排序算法的稳定性,当有相邻的两个元素大小相等的时候,不做交换,相同大小的数据在排序前后不会改变顺序,所以冒泡排序是稳定的排序算法。

4、冒泡排序的时间复杂度是多少?

冒泡排序包含两个操作原子,比较和交换。每交换一次,有序度就加 1。不管算法怎么改进,交换次数总是确定的,即为逆序度,也就是n*(n-1)/2–初始有序度。
对于包含 n 个数据的数组进行冒泡排序,平均交换次数是多少呢?最坏情况下,初始状态的有序度是 0,所以要进行 n*(n-1)/2 次交换。最好情况下,初始状态的有序度是 n*(n-1)/2,就不需要进行交换。可以取个中间值 n*(n-1)/4,来表示初始有序度既不是很高也不是很低的平均情况。
这种算法不严格,但是其实挺好用的,因为根据实际数据通过概率论去分析数据太过复杂,不如取个期望值进行计算。

二.插入排序(Insertion Sort,适合较小规模数据排序)

一个有序的数组,往里面添加一个新的数据后,如何继续保持数据有序呢?只要遍历数组,找到数据应该插入的位置将其插入即可,这个思路就是插入排序。
插入排序具体是如何借助上面的思想来实现排序的呢?
首先将数组中的数据分为两个区间,已排序区间和未排序区间。初始已排序区间只有一个元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区间中元素为空,算法结束。

插入排序的核心思想是将数据分成两部分,第一部分是已排序部分,第二部分是未排序部分。最开始设置INDEX(0)为已排序部分,然后再和INDEX(1)进行比较。
以下面的数组为例:4,5,6,1,3,2。
初始已排序是:4,未排序是5,6,1,3,2。
4和5进行比较后,已排序部分是4,5。未排序部分是6,1,3,2。
第三次,6和4,5进行比较之后(会和4和5都进行比较),已排序部分是4,5,6。未排序部分是1,3,2。
第四次,1和4,5,6进行比较(和4进行比较),比较之后就插在了456之前。

 

1、插入排序的代码

// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
   
     
  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; // 插入数据
  }
}

2、插入排序是原地排序算法吗?

从实现过程可以很明显地看出,插入排序算法的运行并不需要额外的存储空间,所以空间复杂度是 O(1),也就是说,这是一个原地排序算法。

3、插入排序是稳定的排序算法吗?

在插入排序中,对于值相同的元素,可以选择将后面出现的元素,插入到前面出现元素的后面,这样就可以保持原有的前后顺序不变,所以插入排序是稳定的排序算法。

4、插入排序的时间复杂度是多少?

对于插入排序来说,每次插入操作都相当于在数组中插入一个数据,循环执行 n 次插入操作,所以平均时间复杂度为 O(n^2)。

三.选择排序(Selection Sort,适合较小规模数据排序)

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。
选择排序
比如有N个元素,会在N个元素中先去找最小的元素,然后将最小的元素和INDEX(0)的元素进行交换,之后再在N-1个元素里找最小的元素,一直找到最后一个。
 

1、选择排序的代码

public class SelectSort {
   
     
    public static void main(String[] args) {
   
     
        int [] arr = {
   
     49,38,65,97,76,13,27,49};
        selectSort(arr,arr.length);
    }
 
    public static void selectSort(int [] arr,int n){
   
     
        for (int i = 0; i < n - 1; i++) {
   
     
            int index = i;
            int j;
            // 找出最小值得元素下标
            for (j = i + 1; j < n; j++) {
   
     
                if (arr[j] < arr[index]) {
   
     
                    index = j;
                }
            }
            int tmp = arr[index];
            arr[index] = arr[i];
            arr[i] = tmp;
            System.out.println(Arrays.toString(arr));
        }
 
    }
}

2、选择排序是原地排序算法吗?

选择排序空间复杂度为 O(1),是一种原地排序算法。

3、选择排序是稳定的排序算法吗?

选择排序并不是稳定的排序算法,比如数据时5,6,5,7,8,9,1。
第一次循环查找最小数据的时候,会将1和第一个5进行交换,这样,两个相同的5的前后顺序就发生了变化,因此不是稳定的个排序算法。

4、选择排序的时间复杂度是多少?

选择排序的最好情况时间复杂度、最坏情况和平均情况时间复杂度都为 O(n^2)

四.归并排序(Merge Sort,适合大规模数据排序,比较难理解)

归并排序的核心思想不复杂,是将需要排序的数组,从中间进行拆分,然后对前后两部分分别进行排序,如果还可以进行拆分那么再进行拆分和排序。一直拆到两个元素为一对,然后通过对比在进行排序,两组已排序好的两个元素再进行对比排序。
 
下面是大佬给出的另一个例子(临时数据为局部变量),图描述的是根据指针来合并两个已经排好顺序的数组,将其合并为一个数组,我按照这个思路描述一下上面的。
首先申请一个临时数组(可以为局部变量也可以为全局变量,此时代码中设置的是局部变量,此时假定为全局变量),将数组{11,8,3,9,7,1,2,5}(8个元素,长度为7),先是将其拆分为两个数组{11,8,3,9}和{7,1,2,5}(这个地方并不是实际的拆分为两个数组,而是根据下标数,将其一分为二。)。
再将这两个数组分别拆分为{11,8},{3,9},{7,1},{2,5},当发现无法再进行细化拆分的时候,数组中元素进行比较,从小到大排序,则得出以下四个数组{8,11},{3,9},{1,7},{2,5},此时为第一次排序,然后对{8,11}和{3,9},及{1,7},{2,5}进行合并。

(1)是对下标[0,1]进行排序

对{11,8}进行排序时,临时数组为{8,11,0,0,0,0},排序完成之后将数据赋值给原数组,原数组变为为{8,11,3,9,7,1,2,5}。

(2)是对下标[2,3]进行排序

对{11,8}进行排序时,临时数组为{8,11,3,9,0,0},排序完成之后将数据赋值给原数组,原数组变为为{8,11,3,9,7,1,2,5}。

(3)是对下标[0,3]进行排序后合并

临时数组为:{3,7,9,11,0,0,0,0},原数组为:{3,7,9,11,7,1,2,5}。

(4)是对下标[4,5]进行排序

临时数组为:{3,7,9,11,1,7,0,0},原数组为:{3,7,9,11,1,7,2,5}。

(5)是对下标[6,7]进行排序

临时数组为:{3,7,9,11,1,7,2,5},原数组为:{3,7,9,11,1,7,2,5}。

(6)是对下标[4,7]进行排序后合并

临时数组为:{3,7,9,11,1,2,5,7},原数组为:{3,7,9,11,1,2,5,7}。

(6)是对下标[0,7]进行排序后合并

合并的思路如下图
 

1、归并排序的代码

package mergesortdemo;
public class MergeSortDemo {
   
     
    public static void main(String[] args) {
   
     
        //我乱按的数组,我也不知道有几个元素
        int[] MergeArray = {
   
     4, 3, 2, 1};//, 55, 88, 33, 66, 54, 242, 432, 564, 756, 2342, 623, 856, 31};

        printArray("排序前", MergeArray);
        //排序方法
        Mergesort(MergeArray);

        printArray("排序后", MergeArray);

    }

    //打印方法
    private static void printArray(String pre, int[] MergeArray) {
   
     
        System.out.print(pre + "\n");
        //完成排序后进行输出
        for (int element : MergeArray) {
   
     
            System.out.println(element);
        }
    }
    //排序方法
    public static void Mergesort(int[] array) {
   
     
        System.out.println("开始排序");

        Sort(array, 0, array.length - 1);

    }

    private static void Sort(int[] array, int left, int right) {
   
     
        if (left >= right)
            return;
        int mid = (left + right) / 2;
        //二路归并排序里面有两个Sort,多路归并排序里面写多个Sort就可以了
        Sort(array, left, mid);

        Sort(array, mid + 1, right);

        merge(array, left, mid, right);

    }
    //合并数组
    private static void merge(int[] MergeArray, int left, int mid, int right) {
   
     

        int[] tmpArray = new int[MergeArray.length];
        //
        int ponitI = left;

        int pointJ = mid + 1;

        //拷贝索引
        int pointCopy = left;
        // 逐个归并
        while (left <= mid && pointJ <= right) {
   
     
            if (MergeArray[left] <= MergeArray[pointJ])
                tmpArray[ponitI++] = MergeArray[left++];
            else
                tmpArray[ponitI++] = MergeArray[pointJ++];
        }
        // 将左边剩余的归并
        while (left <= mid) {
   
     
            tmpArray[ponitI++] = MergeArray[left++];
        }
        // 将右边剩余的归并
        while (pointJ <= right) {
   
     
            tmpArray[ponitI++] = MergeArray[pointJ++];
        }

        // TODO Auto-generated method stub
        //从临时数组拷贝到原数组
        while (pointCopy <= right) {
   
     
            MergeArray[pointCopy] = tmpArray[pointCopy];
            //输出中间归并排序结果
            System.out.print(MergeArray[pointCopy] + "\t");
            pointCopy++;
        }
        System.out.println();
    }

}

代码的核心部分有两块

1.递归拆分原数组(非实际意义拆分,而是逻辑意义拆分)
    private static void Sort(int[] array, int left, int right) {
   
     
        if (left >= right){
   
     
            return;
        }
         else {
   
     
            int mid = (left + right) / 2;
            //二路归并排序里面有两个Sort,多路归并排序里面写多个Sort就可以了
            Sort(array, left, mid);

            Sort(array, mid + 1, right);

            merge(array, left, mid, right);
        }
    }

2.比较大小后,将值赋给临时数组,再通过临时数组将值赋给原数组。
    //合并数组
    private static void merge(int[] MergeArray, int left, int mid, int right) {
   
     

        int[] tmpArray = new int[MergeArray.length];
        //
        int ponitI = left;

        int pointJ = mid + 1;

        //拷贝索引
        int pointCopy = left;
        // 逐个归并
        while (left <= mid && pointJ <= right) {
   
     
            if (MergeArray[left] <= MergeArray[pointJ])
                tmpArray[ponitI++] = MergeArray[left++];
            else
                tmpArray[ponitI++] = MergeArray[pointJ++];
        }
        // 将左边剩余的归并
        while (left <= mid) {
   
     
            tmpArray[ponitI++] = MergeArray[left++];
        }
        // 将右边剩余的归并
        while (pointJ <= right) {
   
     
            tmpArray[ponitI++] = MergeArray[pointJ++];
        }
        
        //从临时数组拷贝到原数组
        while (pointCopy <= right) {
   
     
            MergeArray[pointCopy] = tmpArray[pointCopy];
            //输出中间归并排序结果
            System.out.print(MergeArray[pointCopy] + "\t");
            pointCopy++;
        }
        System.out.println();
    }

归并排序比较难理解的一点还是递归,明白递归的执行顺序,那么归并算法也就不难理解了。

2、归并排序是原地排序算法吗?

在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。
递归代码的空间复杂度并不能像时间复杂度那样累加。尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)。

3、归并排序是稳定的排序算法吗?

在合并的过程中,如果元素值大小相同,是不会进行位置互换的,所以归并排序是一个稳定的排序算法。

4、归并排序的时间复杂度是多少?

归并排序涉及递归,时间复杂度的分析比较复杂,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情况,还是平均情况,时间复杂度都是 O(nlogn)。

五.快速排序(Quicksort,广泛应用,大数据量)

快速排序,简称快排。利用的也是分治思想,快排的分治思想是从数组中选择一个元素,作为分区点,从小到大排序,然后分区点循环对比其他的元素,小的放在分区点左边,大的放在分区点的右边。
循环利用这个方法,可以很快的进行排序,确实是很好的排序方法。

我看了好多人写的快速排序,思路各有不同。我觉得这个写的比较好,特别是他发的那几张图,认真看一下就能够更加深入的理解快速排序的思想。链接
我用我的理解大概的描述一下这个的思路,借一张图。
 
设置一个分区点,这个分区点可以随意设置。
假定设置的分区点为index(3)=7
分别有两个哨兵,从左右两端出发,右端先走,左端后走。
右端哨兵从右往左走,寻找比7小数字,然后停下来,会停在5上面。
左端哨兵从左往右出发,寻找比7大的数字,然后停下来,会停在9的上面。
然后为了避免增加空间复杂度,也为了避免进行数组位移操作,直接将7和5进行交换,交换后的数组就是{6,1,2,7,5,3,4,9,10,8}
然后右端哨兵再次出发,寻找比7小的数字,然后停下来,会停在4上面。
左端哨兵从左往右出发,寻找比7大的数字,但是从7到4一直没找到,会和右端哨兵相遇在4上,4比7小,于是4和7进行位置的交换,交换后的数组就是{6,1,2,4,5,3,7,9,10,8},此时第一遍探测正式接数,比7大的都在7的右边,比7小的都在7的左边。
此时第一遍探测结束,数组被分为了逻辑的三个部分左端小于7,7,右端大于7。
左端部分再从中选择一个分区点,右端部分也选择一个分区点,此时数组是{6,1,2,4,5,3,7,9,10,8}。
假设左端选择分区点为1,右端选择分区点为9。然后根据递归算法的思想,先求{6,1,2,4,5,3}再求{9,10,8}的排序(此时并没有实际拆分数组,只是从逻辑上拆分了数组)

对{6,1,2,4,5,3}进行排序,分区点是1,哨兵从两端出发。
右端哨兵寻找比1小的数字,然后停下来,会停在1上面。
左端哨兵寻找比1大的数字,然后停下来,会停在6上面。
1和6进行交换得到{1,6,2,4,5,3}。
再次出发,最后两边都停在了1上。

对{9,10,8}进行排序,分区点是9,哨兵从两端出发。
右端哨兵寻找比9小的数字,然后停下来,会停在8上面。
左端哨兵寻找比9大的数字,然后停下来,会停在10上面。
交换得到{9,8,10}
右端哨兵在交换之后再次进行出发,停下来,会停在8上面。
左端哨兵寻找比9大的数字,但是没用,然后停下来,会停在8上面.。
交换最后落点8和分区点9得到{8,9,10},这部分排序完毕

1、快速排序代码实现

package quicksortdemo;

//非随机快速排序。
public class QuickSortDemo {
   
     

    public static void main(String[] args) {
   
     
        int[] QuickSortArray = {
   
     5, 4, 3, 2, 1, 55, 88, 33, 2, 4, 55, 75, 534, 66};
        printArray("排序前", QuickSortArray);

        System.out.println("开始排序");
        QuickSort(QuickSortArray, 0, QuickSortArray.length - 1);

        printArray("排序后", QuickSortArray);
    }

    //打印方法
    private static void printArray(String pre, int[] MergeArray) {
   
     
        System.out.print(pre + "\n");
        //完成排序后进行输出
        for (int element : MergeArray) {
   
     
            System.out.println(element);
        }
    }

    //这里是利用递归进行分组排序
    private static void QuickSort(int[] QuickSortArray, int indexLeft, int indexRight) {
   
     

        System.out.println("indexLeft :" + indexLeft);
        System.out.println("indexRight :" + indexRight);
        if (indexLeft >= indexRight) {
   
     
            return;
        }
        int left = indexLeft;

        int right = indexRight;

        int temp = 0;

        int randomIndex = left;// new Random().nextInt(right - left) + left;//随机选取点
        System.out.println("randomIndex :" + randomIndex);
        int randomPivot = QuickSortArray[randomIndex];

        //条件是右端元素大于左端,即两端哨兵相遇时候终止。
        while (left < right) {
   
     
            //右侧哨兵从右向左寻找
            //while循环,找到第一个符合条件点即退出当前循环
            while (randomPivot <= QuickSortArray[right] && left < right) {
   
     
                right--;
            }

            //左侧哨兵向右寻找
            //while循环,找到第一个符合条件点即退出当前循环
            while (randomPivot >= QuickSortArray[left] && left < right) {
   
     
                left++;
            }
            //如果满足条件则交换
            if (left < right) {
   
     
                temp = QuickSortArray[right];
                QuickSortArray[right] = QuickSortArray[left];
                QuickSortArray[left] = temp;
            }
        }
        //最后将两个哨兵相遇点和分区点进行交换。
        QuickSortArray[indexLeft] = QuickSortArray[left];
        QuickSortArray[left] = randomPivot;
        //递归调用左半数组,
        if (left > 0) {
   
     
            QuickSort(QuickSortArray, indexLeft, left - 1);
        }
        if (right < indexRight) {
   
     
            //递归调用右半数组
            QuickSort(QuickSortArray, randomIndex + 1, indexRight);
        }
    }
}

2、快速排序是原地排序算法吗?

快苏排序并不需要额外开拓空间,所以是原地排序算法。

3、快速排序是稳定的排序算法吗?

快速排序在交换的过程中可能会有相同元素进行前后位置交换,所以不是稳定的个算法。

4、快速排序的时间复杂度是多少?

快排也是用递归来实现的,快排的时间复杂度也是 O(nlogn)。

六.桶排序(Bucket sort,先行排序)

桶排序,顾名思义,会用到“桶”,核心思想是将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序。桶内排完序(在桶内使用别的排序算法)之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
 
桶排序是比较优秀的排序算法,但是有较为严苛的实现前置条件
首先,要排序的数据需要很容易就能划分成 m 个桶,并且,桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。
其次,数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均,那桶内数据排序的时间复杂度就不是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。

七.计数排序(Counting sort)

计数排序和桶排序的思路相似,区别在于计数排序对桶内的数据不进行排序,而是计数,计算桶内元素的个数。
计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

记数排序有一个很巧妙地表示算法。
比如原数据是:2,5,3,0,2,3,0,3
从小到大进行排序后是0,0,2,2,3,3,3,5
 也可以以这种形式进行表现
 
该数组内存储的不是具体的数据,而是该数据的个数。计数方法是对数组进行顺序求和,数组中C[INDEX]里的存储的是小于等于值INDEX的元素的个数,比如C[2]=4指的是小于等于2的元素有四个,C[3]=7表示小于等于3的元素个数有7个,相减就可以知道=3的个数有三个。

计数排序只能用在数据范围不大的场景中,如果数据范围 k 比要排序的数据 n 大很多,就不适合用计数排序了。而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

八.基数排序(Radix sort)

比如对手机号码或者身份证号进行排序,需要按照每一位进行排序,比如两个身份证A和B从小到大排序。如果A身份证的第一位号码已经比B大了,那么就直接拍为B,A,而不需要去比较AB后面的那些数据,等第一位数据排序完成之后,再进行第二位排序,这样的排序算法称之为基数排序。
对于长短不一的数据,可以将所有数据补齐到相同长度,长度不够的以0填充,因为根据ASCII 值,所有字母都大于“0”,所以补“0”不会影响到原有的大小顺序。这样就可以继续用基数排序了。
基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进的关系,如果 a 数据的高位比 b 数据大,那剩下的低位就不用比较了。除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到 O(n) 了。

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: