排序算法总结

102 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第16天,点击查看活动详情

本次涉及排序算法有冒泡排序,选择排序,插入排序,希尔排序,归并排序,快排,堆排序,计数排序,桶排序,基数排序。

关于冒泡排序,桶排序,插入排序,快排参考文章: 关于排序算法的一些总结 - 掘金 (juejin.cn)

关于时间复杂度,空间复杂度

算法平均时间复杂最少时间最多时间空间稳定
冒泡O(n^2)O(n)O(n^2)O(1)
选择O(n^2)O(n^2)O(n^2)O(1)不稳定
插入O(n^2)O(n^2)O(n^2)O(1)
希尔O(n log n)O(n log^2 n)O(n log^2 n)O(1)不稳定
归并O(n log n)O(n log n)O(n log^2 n)O(n)
快排O(n log n)O(n log n)O(n^2)O(log n)不稳定
O(n log n)O(n log n)O(n log n)O(1)不稳定
计数O(n+k)O(n+k)O(n+k)O(k)
O(n+k)O(n+k)O(n^2)O(n+k)
基数O(nXk)O(nXk)O(nXk)O(n+k)

1.基数排序

基数排序基本思想是依次选择位数进行排序。 分为

  • MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序
  • LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序

以LSD为例,假设原来有一串数值如下所示: 73, 22, 93, 43, 55, 14, 28, 65, 39, 81

首先按照个位数值,在走访数值时将它们分配至编号0到9的桶子中

image.png

接下来将这些桶子中的数值重新串接起来,成为以下的数列: 81, 22, 73, 93, 43, 14, 55, 65, 28, 39

接着再进行一次分配,这次是根据十位数来分配:

image.png

接下来将这些桶子中的数值重新串接起来,成为以下的数列: 14, 22, 28, 39, 43, 55, 65, 73, 81, 93

这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。

LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。MSD的方式与LSD相反,是由高位数为基底开始进行分配,但在分配之后并不马上合并回一个数组中,而是在每个“桶子”中建立“子桶”,将每个桶子中的数值按照下一数位的值分配到“子桶”中。在进行完最低位数的分配后再合并回单一的数组中。

我们可以看出来:有几个位,就会进行几次排序,缺排序结果和排序之前的顺序没有任何关系。

其代码步骤如下:

得到数组中最大数的位数,确定需要排几轮;

定义一个长度为10的二维数组模拟10个桶,其中每个桶的长度为原数组的长度;

定义一个一维数组用来记录每一轮的每个桶中放了多少个数据;

每一轮先按每个数个位、十位、百位.的顺序放入桶中;放完之后再按桶的顺序将每个数放回到原数组。直至排完



/*
 * 基数排序
 */
 int[] array = { 73, 22, 93, 43, 55, 14, 28, 65, 39, 81 };
public static void radixSort(int[] array) {
//拿到最大数
int max=array[0];
for(int i=0;i<array.length;i++){
if(array[i]>max)
{
max=array[i];
}
}
//得到最大数的位数,确定循环几轮
int maxLength = (max + "").length();
//定义一个二维数组用来模拟10个
int[][] bucket=new int[10][array.length];
//定义一个一维数组用来记录每一轮的每个桶中放了多少个数据
int[] bucketEleCounts=new int[10];
//开始排序 n专门用来辅助得到每轮的位数
for(int i=0,n=1;i<maxLength;i++,n*=10){

for(int j=0;j<array.length;j++){
//得到需要的位数
int ele=array[j]/n%10;
//放进桶
bucket[ele][bucketEleCounts[ele]] = array[j];
//第ele个桶的数量+1
bucketEleCounts[ele]++;
}
//按桶的顺序依次把数据取出来
int index=0;
for(int k=0;k<bucket.length;k++){
if (bucketEleCounts[k] != 0) {
// 第k个桶里面的数据循环取出来
for (int l = 0; l < bucketEleCounts[k]; l++) {
array[index] = bucket[k][l];
index++;
}
//桶的数据个数清空
bucketEleCounts[k] = 0;
}
}

}


选择排序

每次从一端开始,选择最大或者最小的,存放到排序序列的起始位置。 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。看上去很像冒泡,是向前冒泡

image.png

代码实现:

public int[] sort(int[] arr){
for(int i=0;i<arr.length;i++){
int min=i;
for(int j=i+1;j<array.length;j++){
if(arr[j]<arr[min]){
min=j;
}
}
if (i != min) {  
int tmp = arr[i];  
arr[i] = arr[min];  
arr[min] = tmp;  
}
}
return arr;
}

希尔排序:

希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。 希尔排序又称缩小增量排序。 它通过比较相距一定间隔的元素来进行,各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止。

即:若增量为3,第i、i+3、i+6个元素都为有序序列(i=1,2,3)

代码实现: 希尔排序目的为了加快速度改进了插入排序,交换不相邻的元素对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。

在此我们选择增量 gap=length/2,缩小增量以 gap = gap/2 的方式,用序列  {n/2,(n/2)/2...1}  来表示。

初始增量第一趟 gap = length/2 = 4。 第二趟,增量缩小为 2。 第三趟,增量缩小为 1,得到最终排序结果。

public void sort(Comparable[] arr){
int j;
for(int gap=arr.length/2;gap>0;gap/=2){
for (int i = gap; i < arr.length; i++) {  
Comparable tmp = arr[i];  
for (j = i; j >= gap && tmp.compareTo(arr[j - gap]) < 0; j -= gap) {  
arr[j] = arr[j - gap];  
}  
arr[j] = tmp;
 }
}
}

归并排序:

采用分治法(Divide and Conquer)的一个非常典型的应用。

作为一种典型的分而治之思想的算法应用,归并排序的实现由两种方法:

  • 自上而下的递归(所有递归的方法都可以用迭代重写,所以就有了第 2 种方法);
  • 自下而上的迭代;

看上去有点模糊,图示如下

image.png

代码:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。
public int[] sort(int[] arr){
if(arr.length<2){
return arr;
}
int moddle=(int) Math.floor(arr.length / 2);
//分成两部分
int[] left = Arrays.copyOfRange(arr, 0, middle);  
int[] right = Arrays.copyOfRange(arr, middle, arr.length);
return merge(sort(left), sort(right));
}
int[] merge(int[] left,int[] right){
int[] res=new int[left.length+right.length];
while(left.length > 0 && right.length > 0) {
if(left[0]<=right[0]){
res[i++]=left[0];
left=Arrays.copyOfRange(left, 1, left.length);
}
else{
res[i++]=right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
while(left.length>0){
res[i++]=left[0];
left=Arrays.copyOfRange(left, 1, left.length);
}
while(right.length>0){
res[i++]=right[0];
right=Arrays.copyOfRange(right, 1, left.length);
}
return res;
}

堆排序

堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  1. 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  2. 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列

下面以大顶堆为例

image.png 第一步:把最后一个数和根交换,然后进行排序

image.png

代码:


//构建大根堆:将array看成完全二叉树的顺序存储结构
int[] buildMaxHeap(int[] array){
//从最后一个节点array.length-1的父节点(array.length-1-1)/2开始,直到根节点0,反复调整堆
      for(int i=(array.length-2)/2;i>=0;i--){ 
          adjustDownToUp(array, i,array.length);
}
     return array;
   }
//调整数:
void heapify(int[] arr, int i, int len) {  
        int left = 2 * i + 1;  
        int right = 2 * i + 2;  
        int largest = i;  
  
        if (left < len && arr[left] > arr[largest]) {  
            largest = left;  
        }  
  
        if (right < len && arr[right] > arr[largest]) {  
            largest = right;  
        }  
  
        if (largest != i) {  
            swap(arr, i, largest);  
            heapify(arr, largest, len);  
        }  
    }
    void swap(int[] arr, int i, int j) {  
        int temp = arr[i];  
        arr[i] = arr[j];  
        arr[j] = temp;  
    }
    
   //sort代码
   public int[] sort(int[] arr) {  
        int len = arr.length;  
        buildMaxHeap(arr, len);  
  
        for (int i = len - 1; i > 0; i--) {  
            swap(arr, 0, i);  
            len--;  
            heapify(arr, 0, len);  
        }  
        return arr;  
    }
   

不稳定算法:即当有两个相同数时候,相对位置就会变化

希尔,快排,堆,选择