经典排序算法总结
排序算法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
选择排序(selectSort) | O( N 2 N^{2} N2) | O( 1 1 1) | 不稳定 |
冒泡排序(bubbleSort) | O( N 2 N^{2} N2) | O( 1 1 1) | 稳定 |
插入排序 (insertSort) | O( N 2 N^{2} N2) | O( 1 1 1) | 稳定 |
归并排序 (mergeSort) | O( N N N* l o g N logN logN) | O( N N N) | 稳定 |
堆排序 (heapSort) | O( N N N* l o g N ) logN) logN) | O( 1 1 1) | 不稳定 |
快速排序 (quickSort) | O( N ∗ l o g N N*logN N∗logN) | O( l o g N logN logN) | 不稳定 |
计数排序 (countingSort) | O( N N N) | O( N N N) | 稳定 |
基数排序 (radixSort) | O( N N N) | O( N N N) | 稳定 |
排序算法的选择:
- 如果不追求稳定性,使用快排,因为快排的常数系数小(实验得出);
- 如果追求额外空间复杂度小,使用堆排;
- 如果追求稳定性好,使用归并排序。
排序算法稳定性的理解:
稳定性的常见 错误理解 是,稳定性就是算法的时间复杂度不稳定,数据情况糟糕,算法的时间复杂度就变差了 ,这是很典型的错误理解,因为算法的时间复杂度我们就是根据最坏情况来估计的。 正确的理解 是,排序后相同元素在原数组中的相对位置是否改变,如果改变了,就说算法不稳定,否则就稳定。
当然,对于基础数据类型,例如整数来说,稳定性似乎没有用,但是对于对象来说稳定性就很有用,因为它能将原数组里的顺序信息保留下来。
算法详解
一、冒泡排序(bubbleSort)
如何保证稳定:
import java.util.Scanner;
public class bubbleSort {
public static void bubbleSort(int[] arr) {
for(int i=arr.length-1;i>=0;--i) {//0~i范围上进行排序
for(int j=0;j<i;++j) {
if(arr[j]>arr[j+1]) {
swap(arr,j,j+1);
}
}
}
}
public static void swap(int[]arr,int i,int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
public static void print(int[] arr) {
for(int i=0;i<arr.length;++i) {
System.out.print(arr[i]+" ");
}
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n;
n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
bubbleSort(arr);
print(arr);
}
}
二、选择排序(selectSort)
为什么不稳定?
public class selectSort {
public static void selectSort(int[] arr) {
for(int i=0;i<arr.length;++i) {//在i~arr.length-1范围上
int minIndex=i;
for(int j=i;j<arr.length;++j) {
if(arr[j]<arr[minIndex]) {
minIndex=j;
}
}
swap(arr,i,minIndex);
}
}
public static void swap(int[]arr,int i,int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
public static void print(int[] arr) {
for(int i=0;i<arr.length;++i) {
System.out.print(arr[i]+" ");
}
}
public static void main(String[] args) {
int[] arr=new int[] {1,0,5,9,2,7};
selectSort(arr);
print(arr);
}
}
三、插入排序(insertSort)
如何保证稳定:
import java.util.Scanner;
public class insertSort {
public static void insertSort(int[] arr) {
for(int i=1;i<arr.length;++i) {//0~i有序
for(int j=i;j>0;--j) {
if(arr[j]<arr[j-1])swap(arr,j,j-1);
}
}
}
public static void swap(int[]arr,int i,int j) {
int tmp=arr[i];
arr[i]=arr[j];
arr[j]=tmp;
}
public static void print(int[] arr) {
for(int i=0;i<arr.length;++i) {
System.out.print(arr[i]+" ");
}
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n;
n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
insertSort(arr);
print(arr);
}
}
四、归并排序(mergeSort)
如何保证稳定:
归并排序有几个易错点:
import java.util.Scanner;
public class MergeSort {
public static void mergeSort(int[] arr) {
if(arr==null)return ;
process(arr,0,arr.length-1);
}
public static void process(int[] arr,int left,int right) {
if(left==right)return;
int mid=left+((right-left)>>1);
process(arr,left,mid);
process(arr,mid+1,right);
merge(arr,left,mid,right);
}
public static void merge(int[] arr,int left,int mid,int right) {
int curLeft=left;
int curRight=mid+1;
int curHelp=0;
//(1)help数组的大小不是 arr.length,应该是right-left+1
int[] help=new int[right-left+1];
while(curLeft<=mid&&curRight<=right) {
help[curHelp++]=arr[curLeft]<=arr[curRight]?arr[curLeft++]:arr[curRight++];
}
while(curLeft<=mid) {
help[curHelp++]=arr[curLeft++];
}
while(curRight<=right) {
help[curHelp++]=arr[curRight++];
}
//(2)拷贝回原数组,应从L----R
for(int i=0;i<help.length;++i) {
arr[left+i]=help[i];
}
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
mergeSort(arr);
for(int i=0;i<n;++i) {
System.out.printf(arr[i]+" ");
}
}
}
利用master公式估计归并排序的时间复杂度:
T
(
N
)
=
2
∗
T
(
N
2
)
+
O
(
N
)
T\left( N \right) =2*T\left( \frac{N}{2} \right) +O\left( N \right)
T(N)=2∗T(2N)+O(N)
其中,merge()函数复杂度为
O
(
N
)
O(N)
O(N),a=2,b=2,d=1。
而
log
b
a
=
log
2
2
=
d
=
1
\log _ba=\log _22=d=1
logba=log22=d=1
故而归并排序的时间复杂度为
O
(
N
∗
l
o
g
N
)
O(N*logN)
O(N∗logN)
为什么归并排序就比选择排序快呢?
因为选择排序每一次选择一个最小值,有序并没有传递下去,而归并则将每一步的归并之后的有序传递了下去。
五、堆排序(heapSort)
为什么不稳定?
实现堆类 调用堆类的 h e a p S o r t ( ) heapSort() heapSort()方法。
import java.util.Scanner;
public class HeapSort {
public static class BigRootHeap{//大根堆
public int N;
public int len;//标识实现堆结构的数组的实际元素个数
public int[] heap;
BigRootHeap(){
N=0;
len=0;
}
BigRootHeap(int N){
this.N=N;
len=0;
heap=new int[N];
}
public void heapSort() {//每次pop()的元素放到末尾
for(int i=0;i<N;++i) {
heap[N-1-i]=pop();
}
}
public void add(int x) {
heap[len++]=x;
//向上调整至大根堆
int cur=len-1;
while(cur!=0) {
int father=(cur-1)/2;
if(heap[father]>=heap[cur])break;
else {
swap(heap,father,cur);
cur=father;
}
}
}
public void heapify(int index) {//从index位置向下判断是否是大根堆,不是就向下调整
int left=index*2+1;
while(left<len) {//有左孩子
int biggestIndex=left+1<len&&heap[left+1]>heap[left]?left+1:left;//先取左右孩子最大的一个
biggestIndex=heap[biggestIndex]>heap[index]?biggestIndex:index;
if(biggestIndex==index)break;
else {
swap(heap,biggestIndex,index);
index=biggestIndex;
left=index*2+1;
}
}
}
public int pop() {//弹出并返回堆顶元素(也即整个堆的最大值)
if(len<1)return Integer.MIN_VALUE;
int top=heap[0];
len--;
heap[0]=heap[len];
heapify(0);
return top;
}
public void swap(int[] arr,int a,int b) {
if(a==b)return;
int tmp=arr[a];
arr[a]=arr[b];
arr[b]=tmp;
}
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
BigRootHeap myHeap=new BigRootHeap(n);
for(int i=0;i<n;++i) {
myHeap.add(scan.nextInt());;
}
myHeap.heapSort();
for(int i=0;i<n;++i) {
System.out.printf("%d ",myHeap.heap[i]);
}
}
}
堆的 a d d ( i n t ) add(int) add(int)方法的时间复杂度是 O ( l o g N ) O(logN) O(logN), h e a p i f y ( i n t ) heapify(int) heapify(int)方法的时间复杂度是 O ( l o g N ) O(logN) O(logN), p o p ( ) pop() pop()方法的时间复杂度是 O ( l o g N ) O(logN) O(logN)。因此 h e a p S o r t ( ) heapSort() heapSort()方法的时间复杂度是 O ( N ∗ l o g N ) O(N*logN) O(N∗logN)。
六、快速排序(quickSort)
为什么不稳定?
- 快排 1.0
借助荷兰国旗一的思想,每次都选取最后一个位置上的数作为划分值,将这些数划分成小于等于划分值和大于划分值两大部分,再将划分值与大于区的第一个数做交换,那么,划分值排序后的位置就确定了,然后分别对划分值左边和右边的数进行划分,使之有序,我们估计时间复杂度都是根据算法的最坏时间复杂度来判断的,最坏的情况就是,每一次大于区的最左侧几乎等于数组长度,也即大于区和小于等于区的规模差距很大。因此时间复杂度是 O ( N 2 ) O(N^2) O(N2) 。
import java.util.Scanner;
public class QuickSort {
public static void quickSort_1(int[] arr,int L,int R) {
if(L>=R)return;
int bigBorder=doutchFlag_1(arr,arr[R],L,R-1);
swap(arr,bigBorder,R);
quickSort_1(arr,L,bigBorder-1);
quickSort_1(arr,bigBorder+1,R);
}
//返回大于区的左边界
public static int doutchFlag_1(int[] arr,int k,int L,int R) {//荷兰国旗问题1
int areaBorder=L-1;//小于等于区边界
int cur=L;//当前位置
while(cur<=R) {
if(arr[cur]<=k) {
swap(arr,cur++,++areaBorder);
}
else {
cur++;
}
}
return areaBorder+1;
}
public static void print(int[] arr) {
for(int i=0;i<arr.length;++i) {
System.out.print(arr[i]+" ");
}
}
public static void swap(int[] arr,int a,int b) {
if(a==b)return;
int tmp=arr[a];
arr[a]=arr[b];
arr[b]=tmp;
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
quickSort_1(arr,0,n-1);
print(arr);
}
}
- 快排 2.0
在1.0的基础上,将区域划分成小于区,等于区和大于区三部分,当有多个等于划分值的数时,一次划分可以使等于划分值的多个数有序,而1.0只能一次使一个数有序。同样地,在最坏情况下,时间复杂度是 O ( N 2 ) O(N^2) O(N2) 。
import java.util.Scanner;
public class QuickSort {
public static void quickSort_2(int[] arr,int L,int R) {
if(L>=R)return;
int[] Border=new int[2];
Border=doutchFlag_2(arr,arr[R],L,R-1);
swap(arr,Border[1]+1,R);
quickSort_2(arr,L,Border[0]-1);
quickSort_2(arr,Border[1]+2,R);
}
//返回小于区和大于区边界
public static int[] doutchFlag_2(int[] arr,int k,int L,int R) {//荷兰国旗问题2
int smallBorder=L-1;//小于区边界
int bigBorder=R+1;//大于区边界
int cur=L;//当前位置
while(cur<bigBorder) {
if(arr[cur]==k)cur++;
else if(arr[cur]<k) {
swap(arr,cur++,++smallBorder);
}
else {
swap(arr,cur,--bigBorder);
}
}
int[] equalBorder=new int[]{smallBorder+1,bigBorder-1};
return equalBorder;
}
public static void print(int[] arr) {
for(int i=0;i<arr.length;++i) {
System.out.print(arr[i]+" ");
}
}
public static void swap(int[] arr,int a,int b) {
if(a==b)return;
int tmp=arr[a];
arr[a]=arr[b];
arr[b]=tmp;
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
quickSort_2(arr,0,n-1);
print(arr);
}
}
- 快排 3.0
1.0 1.0 1.0 和 2.0 2.0 2.0 之所以慢,是因为每次划分值都固定选取最后一个数,这样很容易出现最坏情况, 3.0 3.0 3.0 对此进行了优化,每次的划分值的下标是通过随机数来选取,这样好情况与坏情况出现的 概率 就是一样的,经过数学的严格证明,最后这样的算法时间复杂度就是 O ( N ∗ l o g N ) O(N*logN) O(N∗logN) 。
import java.util.Random;
import java.util.Scanner;
public class QuickSort {
public static void quickSort_3(int[] arr,int L,int R) {
if(L>=R)return;
//随机划分值
Random random=new Random();
int randomIndex=L+random.nextInt(R-L+1);
swap(arr,randomIndex,R);
int[] Border=new int[2];
Border=doutchFlag_2(arr,arr[R],L,R-1);
swap(arr,Border[1]+1,R);
quickSort_2(arr,L,Border[0]-1);
quickSort_2(arr,Border[1]+2,R);
}
//返回小于区和大于区边界
public static int[] doutchFlag_2(int[] arr,int k,int L,int R) {//荷兰国旗问题2
int smallBorder=L-1;//小于区边界
int bigBorder=R+1;//大于区边界
int cur=L;//当前位置
while(cur<bigBorder) {
if(arr[cur]==k)cur++;
else if(arr[cur]<k) {
swap(arr,cur++,++smallBorder);
}
else {
swap(arr,cur,--bigBorder);
}
}
int[] equalBorder=new int[]{smallBorder+1,bigBorder-1};
return equalBorder;
}
public static void print(int[] arr) {
for(int i=0;i<arr.length;++i) {
System.out.print(arr[i]+" ");
}
}
public static void swap(int[] arr,int a,int b) {
if(a==b)return;
int tmp=arr[a];
arr[a]=arr[b];
arr[b]=tmp;
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
quickSort_3(arr,0,n-1);
print(arr);
}
}
七、计数排序(countingSort)
import java.util.Scanner;
public class CountingSort {
public static void countingSort(int[] arr) {
int[] count=new int[1001];
for(int i=0;i<arr.length;++i) {
count[arr[i]]++;
}
for(int i=0;i<count.length;++i) {
for(int j=0;j<count[i];++j) {
System.out.print(i+" ");
}
}
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
countingSort(arr);
}
}
八、基数排序(radixSort)
import java.util.Scanner;
public class RadixSort {
public static int maxDigit(int x) {
int ans=0;
while(x>0) {
x/=10;
ans++;
}
return ans;
}
public static int getMax(int[] arr) {
int ans=arr[0];
for(int i=0;i<arr.length;++i) {
ans=ans>arr[i]?ans:arr[i];
}
return ans;
}
public static int getDigit(int x,int index) {
if(maxDigit(x)<index) {
return 0;
}
int ans=0;
while(index>0) {
ans=x%10;
x/=10;
index--;
}
return ans;
}
public static void radixSort(int[] arr) {
//找出最大数的位数
int maxNum=getMax(arr);
int digit=maxDigit(maxNum);
for(int times=1;times<=digit;++times) {//处理digit次
int[] count=new int[10];
for(int i=0;i<arr.length;++i) {
int index=getDigit(arr[i],times);
count[index]++;
}
for(int i=1;i<count.length;++i) {
count[i]+=count[i-1];
}
int[] bucket=new int[arr.length];
for(int i=arr.length-1;i>=0;--i) {
int index=getDigit(arr[i],times);
bucket[count[index]-1]=arr[i];
count[index]--;
}
for(int i=0;i<arr.length;++i) {
arr[i]=bucket[i];
}
}
}
public static void main(String[] args) {
Scanner scan=new Scanner(System.in);
int n=scan.nextInt();
int[] arr=new int[n];
for(int i=0;i<n;++i) {
arr[i]=scan.nextInt();
}
radixSort(arr);
for(int i=0;i<arr.length;++i) {
System.out.print(arr[i]+" ");
}
}
}
常见的坑
- 归并排序的额外空间复杂度可以变成O(1)(但就不稳定了,何必呢?为啥不用堆排呢?),但是非常难,不需要掌握,有兴趣可以搜“归并排序内部缓存法”。【
这样的贴子根本不用去看】 - “原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O(
N
2
N^2
N2).【
这样的贴子根本不用去看】 - 快速排序可以做到稳定,但是因此带来额外空间复杂度变成O(N)(何必呢?用归并排序不香吗?),非常难,不需要掌握,有兴趣可以搜“01 stable sort”。【
这样的贴子根本不用去看】 - 所有的改进都不重要,因为目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。
- 有一道题目,要求奇数放到数组左边,偶数放到数组右边,还要求原始的相对次序不变,要求额外空间复杂度为O(1),时间复杂度O(N),碰到这个问题,可以直接怼面试官。
因为做不到。试想,快速排序在partition部分,<p , >p 与判断 一个数是奇数和偶数操作一样,那设计快速排序时,人家为啥不采用奇偶这种方式,而采用<p 的数放左边,>p的数放右边?====》因为做不到。
工程上对排序的改进
(1)充分利用 O ( N ∗ l o g N ) O(N*logN) O(N∗logN) 和 O ( N 2 ) O(N^2) O(N2) 排序各自的优势。O( N 2 N^2 N2) :在N不太大的时候,常数时间比较小;O(N*logN):在N很大时,调度时间比较短。
(2)考虑稳定性。比如说C++提供的sort()函数,对数据进行分流:基础类型数据,不需要稳 定,就用快排实现;非基础类型数据,需要考虑稳定性,就用归并实现。