经典排序算法总结

经典排序算法总结

排序算法时间复杂度空间复杂度稳定性
选择排序(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 NlogN)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)稳定

排序算法的选择:

  1. 如果不追求稳定性,使用快排,因为快排的常数系数小(实验得出);
  2. 如果追求额外空间复杂度小,使用堆排
  3. 如果追求稳定性好,使用归并排序

排序算法稳定性的理解:

稳定性的常见 错误理解 是,稳定性就是算法的时间复杂度不稳定,数据情况糟糕,算法的时间复杂度就变差了 ,这是很典型的错误理解,因为算法的时间复杂度我们就是根据最坏情况来估计的。 正确的理解 是,排序后相同元素在原数组中的相对位置是否改变,如果改变了,就说算法不稳定,否则就稳定。



在这里插入图片描述

当然,对于基础数据类型,例如整数来说,稳定性似乎没有用,但是对于对象来说稳定性就很有用,因为它能将原数组里的顺序信息保留下来。


算法详解

一、冒泡排序(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)=2T(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(NlogN)


为什么归并排序就比选择排序快呢?

因为选择排序每一次选择一个最小值,有序并没有传递下去,而归并则将每一步的归并之后的有序传递了下去。


五、排序(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) addint方法的时间复杂度是 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(NlogN)


六、快速排序(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(NlogN)




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]+" ");
		}
		
	}

}

在这里插入图片描述

常见的

  1. 归并排序的额外空间复杂度可以变成O(1)(但就不稳定了,何必呢?为啥不用堆排呢?),但是非常难不需要掌握,有兴趣可以搜“归并排序内部缓存法”。【这样的贴子根本不用去看
  2. 原地归并排序”的帖子都是垃圾,会让归并排序的时间复杂度变成O( N 2 N^2 N2).【这样的贴子根本不用去看
  3. 快速排序可以做到稳定,但是因此带来额外空间复杂度变成O(N)(何必呢?用归并排序不香吗?),非常难,不需要掌握,有兴趣可以搜“01 stable sort”。【这样的贴子根本不用去看
  4. 所有的改进都不重要,因为目前没有找到时间复杂度O(N*logN),额外空间复杂度O(1),又稳定的排序。
  5. 有一道题目,要求奇数放到数组左边,偶数放到数组右边,还要求原始的相对次序不变,要求额外空间复杂度为O(1),时间复杂度O(N),碰到这个问题,可以直接怼面试官。
    因为做不到。试想,快速排序在partition部分,<p , >p 与判断 一个数是奇数和偶数操作一样,那设计快速排序时,人家为啥不采用奇偶这种方式,而采用<p 的数放左边,>p的数放右边?====》因为做不到。

工程上对排序的改进

(1)充分利用 O ( N ∗ l o g N ) O(N*logN) O(NlogN) O ( N 2 ) O(N^2) O(N2) 排序各自的优势。O( N 2 N^2 N2) :在N不太大的时候,常数时间比较小;O(N*logN):在N很大时,调度时间比较短。

(2)考虑稳定性。比如说C++提供的sort()函数,对数据进行分流:基础类型数据,不需要稳 定,就用快排实现;非基础类型数据,需要考虑稳定性,就用归并实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值