十大排序算法 包含原理 举例和JAVA代码 个人向详细说明

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

参考文章:

https://blog.youkuaiyun.com/qq_51664685/article/details/124427443

菜鸟教程:https://www.runoob.com/

image-20240903151225346

1.冒泡排序

原理

​ 按顺序对比相邻两个数字的大小,将小的排在前面,大的排在后面

例子

举个例子:[5, 3, 8, 4, 2]

首先对比5和3,因为5>3 调换5和3的顺序 变为:

[3, 5, 8, 4, 2]

对比5和8 因为5<8 不调换顺序

对比8和4 因为8>4 调换8和4的顺序 变为:

[3, 5, 4, 8, 2]

对比8和2 因为8>2 调换8和2的顺序 变为:

[3, 5, 4, 2, 8]

=========================================至此第一轮顺序调换完毕 最大的数已被移至数组最右端

开始第二轮对比:

对比3和5 因为3<5 不用调换顺序

对比5和4 因为5>4 调换5和4的顺序 变为:

[3, 4, 5, 2, 8]

对比5和2 因为5>2 调换5和2的顺序 变为:

[3, 4, 2, 5, 8]

到第二轮可以不用再与最右端的数字做对比(因为最右端的数字已在上一轮中被证明为是该数组中最大的数字) 此时第二轮结束 5为整个数组中次大的数 在下一轮中也不必参与对比

开始第三轮对比:

对比3和4 因为3<4 不用调换顺序

对比4和2 因为4>2 调换4和2的顺序 变为:

[3, 2, 4, 5, 8]

开始第四轮的对比:

对比3和2 因为3>2 调换3和2的顺序 变为:

[2, 3, 4, 5, 8]

至此 该数组的冒泡排序结束

时间空间复杂度

数组长度为n

最短时间复杂度:按序查看相邻数字之间是否升序排序 每个都按序 时间复杂度为n

最长时间复杂度:直到最后一次排序才全部按序 第一次交换n-1个组合 第二次交换n-2个组合 …第n-1次交换1个组合 所以总次数为(n-1)(n-2)…(1) 由于n-1视为n,因为时间复杂度为O( n 2 n^2 n2)

由于不额外占用存储空间 因为空间复杂度为O(1)

代码
for(int j=0;j<array.length-1;j++) {
    boolean signal=true;
	for (int i = 0; i < array.length - 1-j; i++) {
    	if (array[i] > array[i + 1]) {
            signal=true;
    		int temp = array[i];
    		array[i] = array[i + 1];
    		array[i + 1] = temp;
    	}
    }
    if(!signal) break;
}

2. 选择排序

原理

不断遍历数组找到最小值从而排序

详细步骤:初始数组num视为未排序数字,遍历整个未排序数组找到最小的数与数组最左端的数即num[0]交换,此时第一个数为已排序数组,后面的数即num[1:]为未排序数组。继续遍历整个未排序数组找到最小的数和未排序数组最左端的数(此时为num[1])交换,此时第一个和第二个数为已排序数组(num[0:1]),后面的数即num[2:]为未排序数组。以此类推。

例子

举个例子:[5, 3, 8, 4, 2]为初始数组 也为未排序数组

第一次遍历未排序数组数组 最小的数为2 将2和数组最左端的数5换位置 变为:

[2, 3, 8, 4, 5]

第二次遍历遍历未排序数组 最小的数为3 将3和数组最左端的数3换位置 位置不变仍为:

[2, 3, 8, 4, 5]

第三次遍历未排序数组数组 最小的数为4 将4和数组最左端的数8换位置 变为:

[2, 3, 4, 8, 5]

第四次遍历未排序数组数组 最小的数为5 将5和数组最左端的85换位置 变为:

[2, 3, 4, 5, 8]

此时排序完成

时间空间复杂度

由于最好和最差的情况下,都要找到未排序数组的最小值,因此时间复杂度都为n(n-1)(n-2)…1 为O( n 2 n^2 n2)

由于不额外占用存储空间 因为空间复杂度为O(1)

代码
for(int j=0;j<array.length-1;j++) {
    int min=j;
    for (int i = j; i < array.length; i++) {
        if(array[i]<array[min]) {
            min=i;
        }
    }
    int tmp=array[j];
    array[j]=array[min];
    array[min]=tmp;
}

3. 插入排序

原理

从无序数组中依次取数放入有序数组的恰当位置

详细步骤:假设数组num的第一个数num[0]为已排序数组,其余的数构成未排序数组。取出未排序数组的第一个数即num[1],将其插入已排序数组的恰当位置,即如果num[1]比num[0]小,则num[1]置于位置0,num[1]后移

例子

举个例子:[5, 3, 8, 4, 2]为初始数组 5为排序数组,[3,8,4,2]为未排序数组

取出未排序数组的第一个数3与已排序数组挨个做对比,应该插在5前面,变为:

[3, 5, 8, 4, 2]

取出未排序数组的第二个数8与已排序数组挨个做对比,应该插在5后面,即不变

取出未排序数组的第三个数4与已排序数组挨个做对比,应该插在5前面,变为:

[3, 4, 5, 8, 2]

取出未排序数组的第四个数2与已排序数组挨个做对比,应该插在3前面,变为:

[2, 3, 4, 5, 8]

此时排序完成

时间空间复杂度

最坏情况的具体过程:

假设我们有一个长度为 n 的数组 [n, n-1, n-2, ..., 2, 1],这是一个完全逆序的数组。

  1. 第一步
    • 将第2个元素插入到已排序部分,插入排序需要与第1个元素比较并进行一次交换。
    • 移动次数:1次
  2. 第二步
    • 将第3个元素插入到已排序部分,需要与前两个元素进行比较,并将它们分别向后移动。
    • 移动次数:2次
  3. 第三步
    • 将第4个元素插入到已排序部分,需要与前三个元素进行比较,并将它们分别向后移动。
    • 移动次数:3次

最后,将第n个元素插入到已排序部分,需要与前面所有 n−1个元素进行比较和移动。

在最坏情况下,插入排序的比较和移动操作数可以表示为:

1 + 2 + 3 + . . . + ( n − 1 ) = n ( n − 1 ) 21 + 2 + 3 + . . . + ( n − 1 ) = n ( n − 1 ) 2 1 + 2 + 3 + . . . + ( n − 1 ) = 2 n ( n − 1 ) 1+2+3+...+(n−1)=n(n−1)21 + 2 + 3 + ... + (n-1) = \frac{n(n-1)}{2}1+2+3+...+(n−1)=2n(n−1) 1+2+3+...+(n1)=n(n1)21+2+3+...+(n1)=2n(n1)1+2+3+...+(n1)=2n(n1)

这个公式中的求和部分等于$ n(n−1)2\frac{n(n-1)}{2}2n(n−1)$,所以插入排序的最坏时间复杂度为:

O ( n 2 ) O(n^2) O(n2)

最好的情况就是每个都对比一遍但不用换位置,时间复杂度为 O ( n ) O(n) O(n)

平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)

代码
for(int i=1; i<array.length; i++) {
    for(int k=0; k<i; k++) {
        if(array[i]<array[k]) {
            int tmp = array[k];
            array[k] = array[i];
            array[i] = tmp;
        }
    }
}

4. 希尔排序

原理

每次通过一定间隔大小划分子序列后进行插入排序

详细步骤:对于数组num,将间隔为a的数构成子数列,进行插入排序,再变小间隔长度对字数做插入排序,最后整个数组做插入排序,此时由于前面子数列的调整需要移动的次数大大减少。

例子

原数组:[8, 5, 3, 2, 7, 1, 6, 4]

首先设间隔为a=num.length//2=8//2=4

即出现x1=[8,7] x2=[5,1],x3=[3,6],x4=[2,4]四个子数列,分别做插入排序变为:

x1=[7,8] x2=[1,5],x3=[3,6],x4=[2,4]

数组变为[7,1,3,2,8,5,6,4]

间隔变为4//2=2

出现x1=[7,3,8,6]和x2=[1,2,5,4]两个子数列,分别做插入排序变为:

x1=[3,6,7,8]和x2=[1,2,5,4]

数组变为:[3,1,6,2,7,5,8,4]

间隔变为1,整个数组做插入排序 变为

[1,2,3,4,5,6,7,8]

时间空间复杂度

l o g 2 n log_2n log2n个间隔值,每个间隔值下要进行n次对比 因此为 O ( n l o g n ) O(nlogn) O(nlogn)

代码
for(int i=0;i<gap_len;i++){
	int gap=(int)Math.pow(2,gap_len-i-1);
	for(int j=0;j<gap;j++){
		for(int k=j+gap;k<array.length;k+=gap){
			for(int g=0;g<k;g+=gap){
				if(array[k]<array[g]){
					int temp=array[k];
					array[k]=array[g];
					array[g]=temp;
				}
			}
		}
	}
}

5. 归并排序

原理

分解数组然后分别形成排序数组然后合并

将数组不断分解,直到最后分解的数组只包含一个数,将只包含一个数的两个数组进行排序合并,将得到若干包含两个数排序后数组,再将这些数组排序合并,最后合并为一个大数组。合并排序的方式为,给定数组a b,选取a、b中较小的数放入新数组c。

img

例子

给定数组[38, 27, 43, 3, 9, 82, 10]

先分解为[38, 27, 43, 3]和[9, 82, 10]

再分解为[38,27] [43,3] [9,82] [10]

再分解为[38] [27] [43] [3] [9] [82] [10]

开始合并:对比大小放入一个新矩阵

[38] [27]->[27,38] [43] [3] ->[3, 43] [9] [82] ->[9, 82]

继续合并:

[27,38] [3, 43]->[3,27,38,43] [9, 82] [10]->[9,10,82]

继续合并:

[3,27,38,43] [9,10,82]->[3,9,10,27,38,43,82]

排序完成

时间空间复杂度

时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn) 每次都要比较n次,共比较 l o g n logn logn

空间复杂度 O ( n ) O(n) O(n) 要使用一个新矩阵C

代码
public int[] sort(int[] array) {
    return sorted(array,0,array.length-1);
}
public int[] sorted(int[] a,int left,int right){
    if(left==right) return new int[]{a[left]};
    int mid=(left+right)/2;
    int[] arr1=sorted(a,left,mid);
    int[] arr2=sorted(a,mid+1,right);
    int n1=arr1.length,n2=arr2.length;
    int[] c=new int[n1+n2];
    for(int i=0,j=0,k=0;k<n1+n2;){
        if(i==n1){
            c[k++]=arr2[j++];
            continue;
        }
        if(j==n2){
            c[k++]=arr1[i++];
            continue;
        }
        if(arr1[i]<arr2[j]){
            c[k++]=arr1[i++];
        }else{
            c[k++]=arr2[j++];
        }
    }
    return c;
}

6. 快速排序

原理

选取一个基准数 将比基准数大的放在基准数右边 比基准数小的放在基准数左边 此时基准数位于中间,再对左右两个分区执行上述操作

例子

数组num[3, 6, 8, 10, 1, 2, 1]

首先选取第一个数即3为基准数(也可以选取分区范围内的随机数)

此时base=3

从第二个数开始找比3小的数,num[4]=1<3 则将num[0]处填为1 此时会有一个问题,下一步操作中num[5]=2也比3小,如果要将2往前方,将会需要 6 8 10全部后移操作 会带来时间浪费 更好的操作是:

i=1,j=num.length-1=6,base=3

从j=6开始向左找小于3的数,num[6]=1<3 将1填入num[0],(base变量保存基准数所以不用担心3的丢失),数组变为:

[1, 6, 8, 10, 1, 2, 1]

从i开始向右找大于3的数,num[1]=6>3 将6填入num[j] 数组变为

[1, 6, 8, 10, 1, 2, 6]

再从j=6开始向左找小于3的数,num[5]=2<3 将2填入num[i] ,数组变为

[1, 2, 8, 10, 1, 2, 6]

从i=1开始向右找大于3的数,num[2]=6>3 将6填入num[j] 数组变为

[1, 2, 8, 10, 1, 8, 6]

再从j=5开始向左找小于3的数,num[4]=1<3 将1填入num[i] ,数组变为

[1, 2, 1, 10, 1, 8, 6]

从i=2开始向右找大于3的数,num[3]=10>3 将10填入num[j] 数组变为

[1, 2, 1, 10, 10, 8, 6]

此时i+1=j 将base填入num[i] 数组变为

[1, 2, 1, 3, 10, 8, 6]

将数组划分为两个区 a=[1,2,1] b=[10,8,6]再进行如上操作

时间空间复杂度

时间复杂度:

最坏情况 O ( n 2 ) O(n^2) O(n2),当每次分区选择的基准元素都是最大或最小元素时,导致数组没有均匀分区,例如在已经排序的数组上使用快速排序时。

平均情况 O ( n l o g ⁡ n ) O(nlog⁡n) O(nlogn),在大多数情况下,快速排序的分区会将数组大致分为两半,因此每次递归的深度是 O ( l o g n ) O(logn) O(logn),每层递归需要 O ( n ) O(n) O(n) 时间。

空间复杂度:每轮都需要记录基准值,共 l o g n log n logn轮,因此空间复杂度为 O ( l o g n ) O(logn) O(logn)

代码
public int[] sort(int[] array) {
    return sort2(array, 0, array.length - 1);
}
public int[] sort2(int[] array,int left,int right) {
    if(left>=right) return array;
    int base=array[left];
    int index=left;
    int j=right;
    boolean signal=false;
    while(index<j){
        if(signal&&array[index]>base){
            array[j--]=array[index];
            signal=false;
            continue;
        }
        if(array[j]<base&&!signal){
            array[index++]=array[j];
            signal=true;
            continue;
        }
        if(!signal) j--;
        if(signal) index++;
    }
    array[j]=base;
    sort2(array,left,j-1);
    sort2(array, j+1, right);
    return array;
}

7. 堆排序

原理
  1. 先构建大堆顶(升序)或者小堆顶(降序),这里讲大堆顶,小堆顶同理。从最底层开始,对比子节点和父节点,比父节点大的子节点与父节点接环,使得父节点大于子节点,最后根节点为整个数组的最大值。
  2. 将父节点与最后一个节点交换,重复以上操作。
例子

数组num[4, 10, 3, 5, 1],构成二叉树:

        4
      /   \
     10    3
    /  \
   5    1

先构建大根堆:

对比子树 10 5 1,由于10>5>1 该子树没有变化

在对比子树 4 10 3,由于10>4>3 将10和4交换:

        10
      /   \
     4     3
    /  \
   5    1

此时大根堆构建完成,将根节点10和最后一个节点1交换

        1
      /   \
     4     3
    /  \
   5    10

对比子树 4 5 (10已经不参与对比)由于5>4,将5和4交换位置:

        1
      /   \
     5     3
    /  \
   4    10

对比子树 1 5 3 由于5>3>1,将5和1交换位置:

        5
      /   \
     1     3
    /  \
   4    10

此时形成新的大根堆 将根节点5和最后一个节点4 交换:

        4
      /   \
     1     3
    /  \
   5    10

对比子树 4 1 3 为大根堆不用变化,将根节点4和最后一个节点3交换:

        3
      /   \
     1     4
    /  \
   5    10

对比子树3 1为大根堆不用变化,将根节点3和最后一个节点1交换:

        1
      /   \
     3     4
    /  \
   5    10

至此,排序完成

时间空间复杂度

时间复杂度:构建最大堆,即最大数交换至堆顶需要走 l o g n logn logn步,每个数都会经过堆顶,共n个数即n轮迭代,因此时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

空间复杂度:不需要额外空间,空间复杂度为 O ( 1 ) O(1) O(1)

代码
public int[] sort(int[] array) {
    int n=array.length;
    for(int i=0;i<n;i++){
        int len=n-i;
        int level=(int)(Math.log(n)/Math.log(2)+1);
        for(int j=(int)(Math.pow(2,level))-2;j>=0;j--){
            if(2*j+1<len){
                if(array[j]<array[2*j+1]){
                    int temp=array[j];
                    array[j]=array[2*j+1];
                    array[2*j+1]=temp;
                }
            }
            if(2*j+2<len){
                if(array[j]<array[2*j+2]){
                    int temp=array[j];
                    array[j]=array[2*j+2];
                    array[2*j+2]=temp;
                }
            }
        }
        int tmp=array[len-1];
        array[len-1]=array[0];
        array[0]=tmp;
    }
    return array;

8. 计数排序

原理
  1. 找到数组num的最大值max和最小值min,创建一个大小为max-min+1的数组C
  2. 遍历数组num,利用数组C记录num中数字出现的次数 即C[num[index]]=count(num[index]);
  3. 遍历数组num,利用C中的记录得到num中每个数字对应的位置,实现排序
例子

数组num:[1, 4, 1, 2, 7, 5, 2]

遍历一遍数组得到最大值max为7 最小值min为1 构建一个数组C,大小为7 能够涵盖数组num中的所有值

遍历数组:C[num[i]-min]++

num[0]=1 则将C[1]=1;

遍历完毕后,数组C:[2,2,0,1,1,0,1]

遍历数组C,将非0位置的值累加到下一个数,

C[0]=2

C[1]=C[0]+C[1]=4

C[2]=C[1]+C[2]=4

数组C变为:[2,4,4,5,6,6,7]

遍历数组num:

num[0]=1; C[num[0]-min]-1=C[0]-1=2-1 为1在排序后数组的位置 同时C[1]处减去1

num[1]=4; C[num[1]-min]-1=C[3]-1=4; 为4在排序后数组的位置 同时C[3]处减去1

时间空间复杂度

时间复杂度:遍历一次数组得到最大最小值,遍历一次数组得到每个数的数目并填入计数数组C,遍历一次数组C得到数字排序后所在位置,最后遍历原数组得到每个数组的确切位置,并写入输出数组。因此复杂度为 o ( 3 ∗ n + k ) o(3*n+k) o(3n+k) O ( n + k ) O(n+k) O(n+k)

空间复杂度:使用了一个额外的数组C和一个额外的输出数组,为 O ( n + k ) O(n+k) O(n+k)

注意此处多加k是因为k和n没有联系。

代码
public int[] sort(int[] array) {
	int n=array.length;
	int max=Integer.MIN_VALUE,min=Integer.MAX_VALUE;
	for(int i=0;i<n;i++){
		max=Math.max(max,array[i]);
		min=Math.min(min,array[i]);
	}
	int[] count=new int[max-min+1];
	for(int i=0;i<n;i++){
		count[array[i]-min]++;
	}
	for(int i=1;i<max-min+1;i++){
		count[i]=count[i-1]+count[i];
	}
	int[] output=new int[n];
	for(int i=0;i<n;i++){
		output[(count[array[i]-min]--)-1]=array[i];
	}
	return output;
}

9. 桶排序

原理

桶排序是计数排序的升级版,将数组已某种方式放入不同的桶中,把每个桶中的数排序,然后把不同桶中的有序数组拼接

img

例子

数组num[29, 25, 3, 49, 9, 37, 21, 43]

构建5个桶

bucket1存储[0:10) bucket2存储[10:20) bucket3存储[20:30) bucket4存储[30:40) bucket5存储[40:50)

遍历数组num

将29放入bucket3 将25放入bucket3 将3放入bucket1 … 此时bucket的状态:

bucket1 [3,9] bucket2 [] bucket3 [29,25,21] bucket4 [37] bucket5 [49,43]

对以上每个桶内的数排序:

bucket1 [3,9] bucket2 [] bucket3 [21,25,29] bucket4 [37] bucket5 [43,49]

拼接每个桶内的有序数组:

[3,9,21,25,29,37,43,49]

时间空间复杂度
  • 时间复杂度:通常情况下为 O ( n ) O(n) O(n),极端情况下所有数都被放入一个桶,那就是该桶内数排序所用算法的时间复杂度
  • 空间复杂度:首先划分k个桶,初始空间都仅存桶变量,还没有开辟空间,需要将原数组n个数存入桶空间,则至少还需要n个空间。因此使用了几个桶和每个桶的空间作为 O ( k + n ) O(k+n) O(k+n)
代码
public int[] sort(int[] array) {
    ArrayList<List<Integer>> buckets = new ArrayList<>();
    for(int i=0;i<5;i++){
        buckets.add(new ArrayList<Integer>());
    }
    int gap=10;
    for(int num:array){
        int index=(int)(num/gap);
        buckets.get(index).add(num);
    }
    for(List<Integer> arr:buckets){
        Collections.sort(arr);
    }
    int index=0;
    for(List<Integer> arr:buckets){
        for(int num:arr){
            array[index++]=num;
        }
    }
    return array;
}

10. 基数排序

原理

将数组中的数按位排序,可以视为桶排序的升级,某一位对比时,利用一个桶来记录基数(比如0-9)的数出现的次数

例子

num [170, 45, 75, 90, 802, 24, 2, 66]

个位数分别为:[0,5,5,0,2,4,2,6] 按升序排序:

[170,90,802,2,2,24,45,75,66]

十位数分别为:[7, 9, 0, 0, 2, 4, 7, 6]按升序排序:

[802,2,24,45,66,120,75,90]

百位数分别为:[8, 0, 0, 0, 0, 1, 0, 0] 按升序排序:

[2,24,45,66,75,99,120,802]

排序完成

时间空间复杂度
  • 时间复杂度:k个位数,n个数字 则时间复杂度为 O ( k ∗ n ) O(k*n) O(kn)
  • 空间复杂度:每轮需要一个n大小数组存储位数信息,并且需要一个桶排序的数组大小为k, O ( n + k ) O(n+k) O(n+k)
代码
public int[] sort(int[] array) {
    int max=array[0];
    for(int num:array){
        max=Math.max(max,num);
    }
    int index=0;
    while(max>0){
        max/=10;
        index++;
    }
    for(int i=1;i<=index;i++){
        int[] output=new int[array.length];
        int[] bucket=new int[10];
        for(int num:array){
            int tmp=i-1==0?1:(int)Math.pow(10,i-1);
            bucket[num%(int)(Math.pow(10,i))/tmp]++;
        }
        for(int j=1;j<10;j++){
            bucket[j]=bucket[j]+bucket[j-1];
        }
        for(int j=array.length-1;j>=0;j--){
            int tmp=i-1==0?1:(int)Math.pow(10,i-1);
            output[((bucket[array[j]%(int)Math.pow(10,i)/tmp])--)-1]=array[j];
        }
        array=output;
    }
    return array;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值