時間復(fù)雜度
算法的時間復(fù)雜度是一個函數(shù),它定性描述了該算法的運行時間。時間復(fù)雜度常用O(讀作big O)來表示,不包括這個函數(shù)的低階項和首項(高階項)的系數(shù)。
來思考一個問題:
有一個有序數(shù)組A,以及另一個無序數(shù)組B,請打印出B中的所有不在A中的數(shù)。
對于這個問題可以使用以下幾種思路解決:
思路一:對于B中的每一個數(shù)都通過遍歷數(shù)組A來判斷
public class Solution{
public static void printMethod1(int[] arrA,int[] arrB){
for(int i = 0;i<arrB.length;i++){
for(int j = 0;j<arrA.length;j++){
if(arrA[j] == arrB[i])
break;
if(j == arrA.length-1 && arrA[j] != arrB[i])
System.out.println(arrB[i]);
}
}
}
}
在遍歷嵌套的內(nèi)部,進行了數(shù)組的尋址,以及比較等操作,這樣的操作是一個常數(shù)時間的操作,常數(shù)操作的時間復(fù)雜度記作:O(1)。對于printMethod1方法,執(zhí)行算法的時間和數(shù)組A及數(shù)組B的樣本量有關(guān),假設(shè)A中有N個數(shù),B中有M個數(shù),兩層for循環(huán)實際上就是將數(shù)組A和數(shù)組B遍歷一遍。那么該算法的時間復(fù)雜度為(M×N)O(1) ,可以表示為O(M×N)。
思路二:對于B中的每一個數(shù),都在A中通過二分查找判斷
public class Solution{
public static void printMethod2(int[] arrA,int[] arrB){
for(int i = 0;i<arrB.length;i++){
if(!bSearch(arrA,0,arrA.length-1,arrB[i]))
System.out.println(arrB[i]);
}
}
public static boolean bSearch(int []arr,int L,int R,int target){
if(L < R){
int mid = L + ((R-L)>>1) ;
if(target == arr[mid]){
return true;
}else if(target < arr[mid]){
return bSearch(arr,L,mid-1,target);
}else{
return bSearch(arr,mid+1,R,target);
}
}else if(L == R){
return arr[L]==target?true:false;
}else{
return false;
}
}
}
第二種算法,可以分解為兩個過程,第一個過程是對數(shù)組B遍歷的過程,第二個過程是對數(shù)組A二分搜索的過程,其他的比較兩個數(shù),數(shù)組尋址等操作都可以記作O(1)。二分搜索同一棵完全二叉樹一樣,其時間復(fù)雜度和二叉樹的高度有關(guān)為為O(logN),對數(shù)組B遍歷的時間復(fù)雜度為O(M),也就是說共需要M次O(logN)的時間復(fù)雜度,為O(M logN)。
思路三:對數(shù)組B進行排序,再利用外排的方式篩選數(shù)據(jù)。
public class Solution{
public static void printMethod3(int[] arrA,int[] arrB){
// 先對數(shù)組B進行排序
mergeSort(arrB);
// 利用外排的方式進行篩選判斷
int p1 = 0;
int p2 = 0;
int lenA = arrA.length;
int lenB = arrB.length;
while(p1 <= lenA-1 && p2 <= lenB-1){
if(arrB[p2]<arrA[p1] && p1 <= lenA-1 && p2 <= lenB-1){
System.out.println(arrB[p2]);
p2++;
}
if(arrB[p2] == arrA[p1] && p1 <= lenA-1 && p2 <= lenB-1 ){
p2++;
}
if(arrB[p2] > arrA[p1] && p1 <= lenA-1 && p2 <= lenB-1){
p1++;
}
}
while(p2 <= lenB-1){
System.out.println(arrB[p2++]);
}
}
//
public static void mergeSort(int []arr){
if (arr == null || arr.length < 2) {
return;
}
sort(arr,0,arr.length-1);
}
public static void sort(int []arr,int L,int R){
if(L == R)
return;
int mid = L + ((R-L)>>1);
sort(arr,L,mid);
sort(arr,mid+1,R);
merge(arr,L,mid,R);
}
public static void merge(int []arr,int L,int mid,int R){
int p1 = L;
int p2 = mid+1;
int []temp = new int[R-L+1];
int i = 0;
while(p1<=mid && p2<=R){
temp[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=mid){
temp[i++] = arr[p1++];
}
while(p2<=R){
temp[i++] = arr[p2++];
}
for(i = 0;i<temp.length;i++){
arr[L+i] = temp[i];
}
}
}
對于第三個算法,對數(shù)組B先進行排序,這里面使用的排序為歸并排序(merge-sort),歸并排序的時間復(fù)雜度為O(N logN),對數(shù)組B排好序后,利用兩個變量p1,p2分別指向數(shù)組A和數(shù)組B的第一個數(shù)
while(p1 <= lenA-1 && p2 <= lenB-1)
在上面的循環(huán)條件內(nèi),如果p2指向的數(shù)小于p1指向的數(shù),那么打印p2指向的數(shù)字并移動p2;如果p2指向的數(shù)等于p1指向的數(shù),那么移動p2不打印;如果p2指向的數(shù)大于p1指向的數(shù),那么移動p1不打印。這樣利用p1,p2對兩個數(shù)組依次外排,就可以打印出符合條件的數(shù)。這個過程,最壞的情況需要遍歷一遍數(shù)組A和數(shù)組B,所以時間復(fù)雜度顯而易見為O(M+N)。那么該算法的時間復(fù)雜度可以表示為:O(M logM)+O(M+N)。
三種算法時間復(fù)雜度的比較
第一種算法的時間復(fù)雜度為O(M×N),第二種算法的時間復(fù)雜度為O(M logN),如果了解過指數(shù)爆炸,那么就知道第二種算法要遠遠好于第一種算法。當(dāng)N的數(shù)量級達到幾千萬,幾億時logN還沒有超過100。那么對于第二種算法和第三種算法,哪種算法更好呢?這就沒有辦法比較了,第三種算法跟數(shù)組B的數(shù)據(jù)量有很大關(guān)系,當(dāng)數(shù)據(jù)量M很大時,這個算法的評價指標(biāo)趨近為O(N logN)級別的算法,當(dāng)M的值很小時,這個算法的評價指標(biāo)可以看做一個O(N)級別的算法。不過第二種算法和第三種算法的時間復(fù)雜度肯定是優(yōu)于算法一的。
冒泡排序,選擇排序與插入排序的時間復(fù)雜度分析
冒泡排序
public class BubbleSort{
public static void sort(int []arr){
//每次將最大的數(shù)字排到最后,一共需要排len-1次
for(int i = 0;i < arr.length-1;i++){
// 需要交換的次數(shù)
for(int j = 0;j<arr.length-1-i;j++){
if(arr[j] > arr[j+1])
swap(arr,j,j+1);
}
}
}
public static void swap(int []arr,int i,int j){
if(i>arr.length-1 || j>arr.length-1 ||i==j)
return;
arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];
}
}
冒泡排序的時間復(fù)雜度顯而易見為O(N2)
選擇排序
public class SelectionSort{
public static void sort(int []arr){
for(int i = 0;i<arr.length;i++){
int minIndex = i;
for(int j = i+1;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){
if(i>arr.length-1 || j>arr.length-1 || i==j)
return;
arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];
}
}
選擇排序使用一個minIndex來記錄最小值的下標(biāo),每次將最小值排序出來,因為有兩次遍歷數(shù)組的過程,所以這個選擇排序算法的時間復(fù)雜度也是O(N2)。
插入排序
public class InsertionSort{
public static void sort(int []arr){
for(int i = 1;i<arr.length;i++){
for(int j = i;j>=1 && arr[j]<arr[j-1];j--){
swap(arr,j,j-1);
}
}
}
public static void swap(int []arr,int i,int j){
if(i>arr.length-1 || j>arr.length-1 || i==j)
return;
arr[i] = arr[i]^arr[j];
arr[j] = arr[i]^arr[j];
arr[i] = arr[i]^arr[j];
}
}
插入排序又叫撲克牌排序,它的排序過程和我們抓牌并理牌的過程一樣。插入排序和冒泡以及選擇排序不同,冒泡和選擇排序是一個穩(wěn)定的時間復(fù)雜度,但是插入排序不同,試想一個數(shù)組如果是已經(jīng)排好序的數(shù)組,那么插入排序只需要依次插入,遍歷了一個循環(huán)即可,那么時間復(fù)雜度就是O(n),如果一個數(shù)組為逆序數(shù)組,那么插入排序則退化成了O(N2)的算法。而冒泡排序和選擇排序則不會依賴數(shù)組的分布狀況,無論怎樣都會遍歷N2次。所以插入排序在當(dāng)前也是一個非常有意義的排序,如果一個數(shù)組是一個近乎有序的數(shù)組,那么插入排序的速度就會很快。
遞歸的本質(zhì)與master公式的使用
設(shè)計一個算法,結(jié)果返回一個無序數(shù)組的最大值
非遞歸思路:
public class Solution{
public static int findMax(int []arr){
if(arr == null)
return -1;
int maxIndex = 0;
for(int i = 1;i<arr.length;i++){
if(arr[i]>arr[maxIndex])
maxIndex = i;
}
return arr[maxIndex];
}
}
遞歸思路:
public class Solution{
public static int findMax(int []arr,int L,int R){
if(arr == null)
return -1;
if(L==R)
return arr[L];
int mid = L+((R-L)>>1);
int maxInLeft = findMax(arr,L,mid);
int maxInRight = findMax(arr,mid+1,R);
return maxInLeft > maxInRight?maxInLeft:maxInRight;
}
}
在寫遞歸程序的時候,第一步是分治,將一個問題化成更小的一個問題,對于本題,把求出一個數(shù)組中最大值這個問題轉(zhuǎn)化成了將一個數(shù)組分成兩半兒,左邊求出最大值,右邊也求出最大值,左邊的最大值和右邊的最大值比較出一個最大值。這是分治的過程,第二步就是為遞歸程序設(shè)計一個終止點的返回值,當(dāng)遞歸到最簡單的情況時,返回什么。遞歸的本質(zhì)則是系統(tǒng)的壓棧。
程序自上而下執(zhí)行,執(zhí)行到int maxInLeft = findMax(arr,L,mid);
時,系統(tǒng)棧會記錄程序執(zhí)行到第幾行,有哪些變量等信息,并把這些信息壓入到系統(tǒng)棧中,保存好信息以后程序進入到findMax方法,等到L==R時,終于得到了返回值,系統(tǒng)棧開始pop,最后進棧的最先還原,因為棧記錄了所有信息,所以得以還原現(xiàn)場,當(dāng)maxInLeft拿到返回值后,系統(tǒng)棧也沒有殘留的信息了,程序就可以繼續(xù)往下面執(zhí)行。
master公式
一個遞歸過程,如何去分析它的時間復(fù)雜度?對于將一個過程分解成均等子過程的遞歸來說,可以使用master公式來計算時間復(fù)雜度。
master公式:T(N) = a*T(N/b) + O(N^d)
1) log(b,a) > d -> 復(fù)雜度為O(N^log(b,a))
2) log(b,a) = d -> 復(fù)雜度為O(N^d * logN)
3) log(b,a) < d -> 復(fù)雜度為O(N^d)
b代表的含義是遞歸分解成了幾個子過程,a代表的含義是一次遞歸子過程執(zhí)行了幾次,O(N^d)表示除去遞歸外,額外需要的過程的時間復(fù)雜度。
用遞歸求數(shù)組最大值問題中,master通式為:2T(N/2)+O(1)
所以該式子滿足條件二:log(b,a) > d。所以這個求最大值的時間復(fù)雜度為:O(N)。
歸并排序與歸并排序時間復(fù)雜度分析、額外空間復(fù)雜度分析
歸并排序的思路:將一個數(shù)組,分治成左數(shù)組和右數(shù)組,對左,右數(shù)組分別排序,然后利用外排的方式,使用一個輔助數(shù)組將兩個數(shù)組進行merge操作。代碼如下:
public static void mergeSort(int []arr){
if (arr == null || arr.length < 2) {
return;
}
sort(arr,0,arr.length-1);
}
public static void sort(int []arr,int L,int R){
if(L == R)
return;
int mid = L + ((R-L)>>1);
sort(arr,L,mid);
sort(arr,mid+1,R);
merge(arr,L,mid,R);
}
public static void merge(int []arr,int L,int mid,int R){
int p1 = L;
int p2 = mid+1;
int []temp = new int[R-L+1];
int i = 0;
while(p1<=mid && p2<=R){
temp[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
while(p1<=mid){
temp[i++] = arr[p1++];
}
while(p2<=R){
temp[i++] = arr[p2++];
}
for(i = 0;i<temp.length;i++){
arr[L+i] = temp[i];
}
}
歸并排序中利用了非常重要的遞歸思想,對歸并排序的時間復(fù)雜度進行分析為:T(N) = 2T(N/2) + O(N)。遞歸的過程分解了執(zhí)行兩次的子過程,并且兩次sort后,還需要進行merge操作,這里面需要一個額外空間的數(shù)組,并把兩個sort好的數(shù)組進行遍歷,兩個數(shù)組的總長度為N,所以除去遞歸,額外需要的時間復(fù)雜度為O(N),因為log(b,a) = d,所以歸并排序的時間復(fù)雜度為:O(N logN)。什么是額外空間復(fù)雜度呢?空間復(fù)雜度(Space Complexity)是對一個算法在運行過程中臨時占用存儲空間的大小,額外空間復(fù)雜度則是,刨去算法本身程序所占的空間,輸入數(shù)據(jù)所占的空間外,額外的輔助變量所需要的空間與輸入數(shù)據(jù)量N的關(guān)系。歸并排序在merge過程中,需要一個長度為N的輔助數(shù)組來完成,所以歸并排序的額外空間復(fù)雜度為O(N)。下表為幾種常見的排序的時間復(fù)雜度與額外空間復(fù)雜度:
版權(quán)聲明:本文為CSDN博主「-出發(fā)-」的原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/happyjacob/article/details/84620880
對數(shù)器
對數(shù)器的概念和使用
1:有一個你想要測的方法a,
2:實現(xiàn)一個絕對正確但是復(fù)雜度不好的方法b,
3:實現(xiàn)一個隨機樣本產(chǎn)生器
4:實現(xiàn)比對的方法
5:把方法a和方法b比對很多次來驗證方法a是否正確。
6:如果有一個樣本使得比對出錯,打印樣本分析是哪個方法出錯
7:當(dāng)樣本數(shù)量很多時比對測試依然正確,可以確定方法a已經(jīng)正確。
如何驗證歸并排序的結(jié)果是絕對正確的:
設(shè)計對數(shù)器的代碼如下:
// test 實現(xiàn)一個絕對正確的,同要測試的算法實現(xiàn)相同功能的方法
public static void comparator(int[]arr){
Arrays.sort(arr);
}
// test 實現(xiàn)一個隨機樣本生成器
public static int[] generateRandomArray(int maxSize,int maxValue){
// 最多生成容量為 maxSize長度的數(shù)組
int []arr = new int[(int)((maxSize+1)*Math.random())];
for(int i = 0;i<arr.length;i++){
// 數(shù)組每一個數(shù)的范圍為:(-maxValue,maxValue)
arr[i] = (int)((maxValue+1)*Math.random() - (maxValue*Math.random()));
}
return arr;
}
// test 判斷兩數(shù)組是否相等
public static boolean isEqual(int []arr1,int []arr2){
if(arr1 == null && arr2 == null)
return true;
if(arr1 == null && arr2 !=null || arr1 !=null && arr2 == null)
return false;
if(arr1.length != arr2.length)
return false;
for(int i = 0;i<arr1.length;i++){
if(arr1[i] != arr2[i])
return false;
}
return true;
}
// test 打印
public static void printArray(int []arr){
if(arr == null)
return;
for(int i = 0;i<arr.length;i++){
System.out.print(arr[i]+ " ");
}
System.out.println();
}
// test 復(fù)制數(shù)組
public static int[] copyArray(int []arr){
if(arr == null)
return null;
int [] copyArr = new int[arr.length];
for(int i = 0;i<arr.length;i++){
copyArr[i] = arr[i];
}
return copyArr;
}
// 測試mergeSort是否正確0
public static void main(String[]args){
int testTime = 5000;
int maxSize = 100;
int maxValue = 100;
boolean succeed = true;
for(int i = 0;i<testTime;i++){
int [] arr1 = generateRandomArray(maxSize,maxValue);
int [] arr2 = copyArray(arr1);
mergeSort(arr1);
comparator(arr2);
if(!isEqual(arr1,arr2)){
succeed = false;
printArray(arr1);
printArray(arr2);
break;
}
}
System.out.println(succeed?"Nice":"Wrong");
}
對數(shù)器用大樣本來測試算法的正確性,可以對算法進行驗證,如果設(shè)計的算法有誤,還可以打印出導(dǎo)致算法與comparator不一致的樣本,來進行對比觀察。
小和與逆序?qū)栴}
小和問題:
在一個數(shù)組中,每一個數(shù)左邊比當(dāng)前數(shù)小的數(shù)累加起來,叫做這個數(shù)組的小和。求一個數(shù)組
的小和。
例子:
[1,3,4,2,5]
1左邊比1小的數(shù),沒有;
3左邊比3小的數(shù),1;
4左邊比4小的數(shù),1、3;
2左邊比2小的數(shù),1;
5左邊比5小的數(shù),1、3、4、2;
所以小和為1+1+3+1+1+3+4+2=16
解決思路:
可以使用對數(shù)組每一個值遍歷求得,但是時間復(fù)雜度為O(N2)。可以使用歸并排序,代碼如下:
public class SmallSum{
public static int smallSum(int[] arr){
return mergeSort(arr);
}
public static int mergeSort(int[] arr){
return sort(arr,0,arr.length-1);
}
public static int sort(int[] arr,int L,int R){
if(L == R)
return 0;
int mid = L+((R-L)>>1);
return sort(arr,L,mid)
+sort(arr,mid+1,R)
+merge(arr,L,mid,R);
}
public static int merge(int[] arr,int L,int mid,int R){
int p1 = L;
int p2 = mid+1;
int[] temp = new int[R-L+1];
int i = 0;
int result = 0;
while(p1<=mid && p2<=R){
result += arr[p1]<arr[p2]?(R-p2+1)*arr[p1]:0;
temp[i++] = arr[p1]<arr[p2]?arr[p1++]:arr[p2++];
}
// only one can run
while(p1<=mid){
temp[i++] = arr[p1++];
}
// only one can run
while(p2<=R){
temp[i++] = arr[p2++];
}
// temp -> arr
for(i = 0;i<temp.length;i++){
arr[L+i] = temp[i];
}
return result;
}
}
除了小和問題外,逆序?qū)栴}也可以通過歸并排序巧妙解決,
來源于劍指offer:
在數(shù)組中的兩個數(shù)字,如果前面一個數(shù)字大于后面的數(shù)字,則這兩個數(shù)字組成一個逆序?qū)Α?輸入一個數(shù)組,求出這個數(shù)組中的逆序?qū)Φ目倲?shù)P。
并將P對1000000007取模的結(jié)果輸出。 即輸出P%1000000007
逆序?qū)托『蛦栴}的思路是一樣的,代碼如下:
public class Solution{
int result = 0;
public int InversePairs(int[]array){
mergeSort(array);
return result;
}
//
public void mergeSort(int [] arr){
sort(arr,0,arr.length-1);
}
//
public void sort(int []arr,int L,int R){
if(L == R)
return;
int mid = L + ((R-L)>>1);
sort(arr,L,mid);
sort(arr,mid+1,R);
merge(arr,L,mid,R);
}
// merge
public void merge(int[]arr,int L,int mid,int R){
int p1 = L;
int p2 = mid+1;
int [] temp = new int[R-L+1];
int i = 0;
while(p1<=mid && p2<=R){
result=arr[p1]>arr[p2]?(result+(R-p2+1))%1000000007:result;
temp[i++] = arr[p1]>arr[p2]?arr[p1++]:arr[p2++];
}
// only one can execute
while(p1<=mid){
temp[i++] = arr[p1++];
}
// only one can execute
while(p2<=R){
temp[i++] = arr[p2++];
}
// temp -> arr
for(i = 0;i<temp.length;i++){
arr[L+i] = temp[i];
}
}
}