提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
参考文章:
https://blog.youkuaiyun.com/qq_51664685/article/details/124427443
菜鸟教程:https://www.runoob.com/
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]
,这是一个完全逆序的数组。
- 第一步:
- 将第2个元素插入到已排序部分,插入排序需要与第1个元素比较并进行一次交换。
- 移动次数:1次
- 第二步:
- 将第3个元素插入到已排序部分,需要与前两个元素进行比较,并将它们分别向后移动。
- 移动次数:2次
- 第三步:
- 将第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+...+(n−1)=n(n−1)21+2+3+...+(n−1)=2n(n−1)1+2+3+...+(n−1)=2n(n−1)
这个公式中的求和部分等于$ 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。
例子
给定数组[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(nlogn) 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. 堆排序
原理
- 先构建大堆顶(升序)或者小堆顶(降序),这里讲大堆顶,小堆顶同理。从最底层开始,对比子节点和父节点,比父节点大的子节点与父节点接环,使得父节点大于子节点,最后根节点为整个数组的最大值。
- 将父节点与最后一个节点交换,重复以上操作。
例子
数组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. 计数排序
原理
- 找到数组num的最大值max和最小值min,创建一个大小为max-min+1的数组C
- 遍历数组num,利用数组C记录num中数字出现的次数 即C[num[index]]=count(num[index]);
- 遍历数组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(3∗n+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. 桶排序
原理
桶排序是计数排序的升级版,将数组已某种方式放入不同的桶中,把每个桶中的数排序,然后把不同桶中的有序数组拼接
例子
数组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(k∗n)
- 空间复杂度:每轮需要一个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;
}