概述
对于各种排序问题可以说时面试中经常问到的,所有笔者在这里做一个总结,从复杂度理解到各种排序的时间空间复杂度以及稳定性到他们的代码实现做一个汇总,方便复习。
1 复杂度
1.时间复杂度
一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
上面这一段解释是很规范的,但是对于非专业性的我们来说并不是那么好理解,说白了时间复杂度就是时间复杂度的计算并不是计算程序具体运行的时间,而是算法执行语句的次数。通常我们计算时间复杂度都是计算最坏情况 。
最坏时间复杂度和平均时间复杂度
最坏情况下的时间复杂度称最坏时间复杂度。一般不特别说明,讨论的时间复杂度均是最坏情况下的时间复杂度。
这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的上界,这就保证了算法的运行时间不会比任何更长。
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,算法的期望运行时间。设每种情况的出现的概率为pi,平均时间复杂度则为sum(pi*f(n))
计算时间复杂度的方法:
常数的话用O(1)表示
保留最高阶项
去除最高阶项的系数
例子:
T(n)=n^2+5n+6与T(n)=3n^2+2n+1,都为O(n^2)。 怎么计算的呢? 去除低阶项,保留高阶项且高阶项的系数为1,所有他们的时间复杂度都为O(n^2)
2.空间复杂度
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分。
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
一个算法所需的存储空间用f(n)表示。S(n)=O(f(n)) 其中n为问题的规模,S(n)表示空间复杂度。
2Master公式
master公式(也称主方法)是用来利用分治策略来解决问题经常使用的时间复杂度的分析方法,(补充:分治策略的递归解法还有两个常用的方法叫做代入法和递归树法,以后有机会和亲们再唠),众所周知,分治策略中使用递归来求解问题分为三步走,分别为分解、解决和合并,所以主方法的表现形式:
T [n] = aT[n/b] + f (n)(直接记为T [n] = aT[n/b] + T (N^d))
其中 a >= 1 and b > 1 是常量,其表示的意义是n表示问题的规模,a表示递归的次数也就是生成的子问题数,b表示每次递归是原来的1/b之一个规模,f(n)表示分解和合并所要花费的时间之和。
解法:
1.当d < logb a时,时间复杂度为O(n^(logb a))
2.当d = logb a时,时间复杂度为O((n^d)*logn)
3.当d > logb a时,时间复杂度为O(n^d)
总结:时间复杂度指的是语句执行次数,空间复杂度指的是算法所占的存储空间
3排序算法总结
排序方法 | 最差时间分析 | 平均时间复杂度 | 稳定度 | 空间复杂度 |
---|---|---|---|---|
冒泡排序 | O(n^2) | O(n^2) | 稳定 | O(1) |
插入排序 | O(n^2) | O(n^2) | 稳定 | O(1) |
选择排序 | O(n^2) | O(n^2) | 稳定 | O(1) |
归并排序 | O(n*log2^n) | O(n*log2^n) | 稳定 | O(n) |
快速排序 | O(n^2) | O(n*log2^n) | 不稳定 | (最好)O(log2^n)~O(n)(最坏) |
二叉树排序 | O(n^2) | O(n*log2^n) | 不稳定 | O(n) |
堆排序 | O(n*log2^n) | O(n*log2^n) | 不稳定 | O(1) |
希尔排序 | O(n^1.3) | O(n^2) | 不稳定 | O(1) |
3.1冒泡排序
冒泡排序是比较基本的排序了,思想也比较简单,就是相邻两个数比较,大的放在右边(左边),效率低每次只能排好一个,需要比较n次,所以时间复杂度为O(n^2),空间复杂度为O(1);
- 代码实现
package sort;
import java.util.Arrays;
public class BubbleSort {
public static void bubbleSort(int [] arr){
if (arr==null || arr.length<2) return;
for (int i = 0;i < arr.length;i++){ //循环n次
for (int j = 0;j< arr.length-i-1;j++){ //从i=0开始相邻两个数比较 arr[0]和arr[1] arr[1]和arr[2] arr[2]和arr[3] ......
if (arr[j] > arr[j+1]){
swap(arr,j,j+1);
}
}
}
}
//两数交换
public static void swap(int [] arr,int x,int y){
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
public static void main(String[] args) {
int [] arr={4,1,5,2,3,8,6,9};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
3.2选择排序
选择排序,从左到右每次选择一个数依次和其他数比较,选择一个最大的,比较n次。每选出一个数要比较n次,一共进行n次。所以时间复杂度为O(n^2)空间复杂度O(1)。
- 代码
package sort;
import java.util.Arrays;
public class SellectSort {
public static void sellectSort(int [] arr){
if (arr == null || arr.length < 2) return;
for (int i = 0;i < arr.length-1;i++){ //控制比较次数
for (int j = i+1;j<arr.length;j++){ //第i个数分别和其他数比较,选择最小的放在i
if (arr[i]>arr[j]){
swap(arr,i=i,j);
}
}
}
}
public static void swap(int [] arr,int x,int y){
int temp = arr[x];
arr[x] = arr[y];
arr[y] = temp;
}
public static void main(String[] args) {
int [] arr={4,1,5,2,3,8,6,9};
sellectSort(arr);
System.out.println(Arrays.toString(arr));
}
}
3.3插入排序
插入排序就行我们打牌一样,按顺序插进去,举个例子:
4,1,5,2,3,8,6,9
第一次 1和4比较 1,4
第二次 1,4,5三个数比较5和4比较不动,4和1比较不动 1,4,5
第三次1,4,5,2 :2和5比较2,5交换1,4,2,5,2和4比较1,2,4,5 1和2比较不动,1,2,4,5
………
可以看出插入排序是和数据状况有关的,如果数组已经排好序时间复杂度O(n),数组无序则复杂度为O(n^2)因为复杂度是和最坏情况计算,所以插入排序时间复杂度为O(n^2),空间O(1)。
package sort;
import java.util.Arrays;
/**
* Created by grace on 2018/9/2.
*/
public class InsertSort {
public static void insertSort(int [] arr){
if (arr == null || arr.length < 2) return;
for (int i = 1;i < arr.length;i++){ //前i个数比较
for (int j = i-1;j >= 0 && arr[j] > arr[j+1];j--){ //0-i个数排序
swap(arr,j,j+1);
}
}
}
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
public static void main(String[] args) {
int [] arr={4,1,5,2,3,8,6,9};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
}
3.4归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
算法步骤:
1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置
3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
4. 重复步骤3直到某一指针达到序列尾
5. 将另一序列剩下的所有元素直接复制到合并序列尾
- 代码
package sort;
import java.util.Arrays;
public class MergeSort {
public static void mergeSort(int [] arr){
if (arr == null || arr.length < 2) return;
mergeSort(arr,0,arr.length-1);
}
public static void mergeSort(int [] arr,int L,int R){
if (L < R){
int m = L + ((R-L) >> 1); //(R+L)/2
System.out.println(m);
mergeSort(arr,L,m); //分治的思想先对L-m进行排序
mergeSort(arr,m+1,R); //在对m+1-R进行排序
merge(arr,L,m,R); //归并的过程对整体进行排序
}
}
//归并的过程
public static void merge(int [] arr,int L,int m,int R){
int i = 0;
int p1 = L; //p1指针指向L
int p2 = m+1; //p2指针指向m+1
int [] help = new int[R-L+1]; //辅助数组
while (p1 <= m && p2 <= R){
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; //p1指针和p2指针指向的小的元素放入到help数组中
}
while (p1 <= m){ //p2已经走到头了
help[i++] = arr[p1++];
}
while (p2 <= R){ //p1已经走到头了
help[i++] = arr[p2++];
}
for (int j = 0;j <help.length;j++){ //排好序的数放回arr中
arr[L+j] = help[j];
}
}
//两数交换
public static void swap(int [] arr,int x,int y){
arr[x] = arr[x] ^ arr[y];
arr[y] = arr[x] ^ arr[y];
arr[y] = arr[x] ^ arr[y];
}
public static void main(String[] args) {
int [] arr={4,1,5,2,3,8,6,9};
mergeSort(arr);
System.out.println(Arrays.toString(arr));
}
}