常見的算法時間複雜度計算分析-總結

如何計算時間複雜度:

  1. 非遞歸算法,通常是計算算法中循環的執行次數。那麼就直接設該代碼循環了k次 , 找出規模n與k的關係,得到k的級數趨近後的式子,既爲時間複雜度。
  2. 2. 如果是遞歸算法,且只進行一次遞歸調用,有以一種方法是先求出深度depth,求出每一次執行的時間複雜度T ,總的時間複雜度就是depth * T(和用下面的方法原理是一樣的。。)
  3. 如果遞歸比較複雜,那麼套用遞歸算法的時間複雜度公式:T[n] = aT[n/b] + f(n) ,T [1] = O(1)。f(n)代表每次執行循環代碼的複雜度。然後用迭代法或者公式法等這些方法來求解。

常見的算法時間複雜度分析:

O(1)< O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) <O(n!) < O(n^n)

一:O(1):

程序段的執行時間是一個與問題規模n無關的常數,也就是算法的執行時間不隨着問題規模n的增加而增長,即使算法中有上千條語句,其執行時間也不過是一個較大的常數。此類算法的時間複雜度是O(1)。

1.1簡單循環實現

//與n無關的賦值或者判斷操作
sum=0//與輸入規模n無關的有限循環
for(i=1;i<=C;i++){  

}

二:O(logn) :

注:logn底數默認爲2,因爲對數之間是可以互相轉換,大概意思就是無論底數是什麼的對數函數都可以化簡成一個常數C×以2爲底數的對數函數,然後級數趨近就都變成了log2(n)。

2.1簡單循環實現

for (int i = 1; i <= n; i = i * 2) {
    語句1}
這裏我們可以把求時間複雜度看成求這個循環內語句一執行了多少次的問題。(但時間複雜度是一個級數趨近的變化概念)假設該程序一共循環了k次(注意增加條件爲i = i * 2),所以必然滿足等式 2^(k-1) = n ,便求得k = logn + 1 ,故O(n)= logn;
類似的乘法增長循環求時間複雜度都可以設循環次數k然後倒推出k,進而級數趨近變換得到時間複雜度。

2.2二分查找/折半查找

同樣假設這個循環一共執行了k次,每一次循環都會將規模n除以2;第一次爲n,第二次爲n/2,接下去就是n,n/2,n/4,…n/2^k。最後結束的條件其實等價於 n / 2^k = 1,然後化簡得 k = logn。於是時間複雜度爲logn。
//二分查找
low = 0;
high = a.lenth() = n; 
while(low<=high){
   mid=(low+high)/2;
   if(goal==a[mid]){
       flag=1;
   	break;
   }
   if(guess>a[mid]){
   	low=mid+1;
   }
   if(guess<a[mid]){
       high=mid-1;
   }		
}

2.3平衡二叉樹查找

樹的結點數爲n
理解一:同樣假設執行了遞歸函數k次,每執行一次都會把一個子樹及以下的樹拋棄,相當於把這個問題規模n給除以了2,那麼同樣結束時候會有n / 2^k = 1然後化簡得 k = logn。於是時間複雜度爲logn。
理解二:如果遞歸函數中,只進行一次遞歸調用,遞歸深度爲depth,在每個遞歸函數中,時間複雜度爲T則總體的時間複雜度爲O(T * depth)。這個也很好理解,就是執行次數 * 每次的時間 = 總的時間。
這裏由於是平衡二叉樹的原因,遞歸的深度depth其實就是該樹的深度,也就是 depth = (logn) +1 ,每次的時間複雜度爲T = O(1),故總的時間複雜度depth * T 化簡後也就是O(logn)。
理解三:遞歸算法的時間複雜度公式 T[n] = aT[n/b] + f(n) ,f(n)爲單次執行的時間複雜度。這裏T[n] = T [ n/ 2],迭代的得到第k次遞歸有T[n] = T [ n / (2^k)] ; 最後一次遞歸必有T[1] = T [ n / (2^k)] ,也即 n / (2^k )=1 。故有k = logn,所以時間複雜度爲:O(log n )。
//二叉樹查找
//樹的結點爲n
BiTree Search(BiTree T,ElemType e){
	if(T==NULL){
		//遞歸返回空值 
		return NULL;
	}else if(e == T->data){
		//遞歸返回查找到的指針 
		return T ;
	}else{
		//令一個結點指針等於左邊結點 
		BiTree temp1= Search(T->lchild,e);
		//令一個結點指針等於右邊結點  
		BiTree temp2= Search(T->lchild,e);
		return temp1==NULL?temp2:temp1;

	}
}

三:O(n):

3.1簡單循環實現

假設循環了k次 ,則滿足k = n,故時間複雜度爲O(n)。
for (int i = 1; i < n; i++) {
    語句1}

3.2階乘

理解一:階乘遞歸的函數執行了n+1,每次執行的複雜度爲O(1);總時間複雜度爲O(n),同樣用depth * T 的方法也可以計算出來O(n);
理解二:循環遞歸一共循環了n次,時間複雜度爲O(n)。
//遞歸
Factorial(int  n){
    if(n==0)
        return 1;
    else
        return Factorial(n-1)*n;
}
//循環
sum = 1;
while(i<=n){
    sum*=i;
    ++i;
}

3.3 鏈表合併 (一道很奇怪的問題,有人說選B也有選D的)

已知兩個長度分別爲m 和 n 的升序鏈表,若將它們合併爲一個長度爲 m+n 的降序鏈表,則最壞情況下的時間複雜度是( D )。
  • A O(n)
    
  • B O(m+n)
    
  • C O(min(m ,n))
    
  • D O(max(m,n))
    
參考代碼:
while(i<A.length && j<B.length){    //循環兩兩比較,小的存入結果
    if(A.data[i]<B.data[j])
        C.data[k++]=A.data[i++];
    else
        C.data[k++]=B.data[j++];
}
//再鏈接上沒比較完的順序表、
/*語句*/
我覺得答案確實應該偏向於O(max(m,n));因爲我所理解的時間複雜度更類似一個級數的收斂化簡,循環跳出的條件爲一個鏈表的長度,那麼最壞的情況應該是單純的max(m,n)。儘管最壞情況下有可能執行了m + n次判斷。兩個答案的級數上都是線性級別O(n)。

四、O(nlogn): 排序中容易看到

4.1 簡單循環

forint i=1;i<=n;i++{
    for(int j=1;j<=n;j = j + i){ 
     	/*語句*/   
    }
}
理解:
當i=1時,內循環需要執行n次;
當i=2時,內循環需要執行n/2次;
當i=3時,內循環需要執行n/3次;
當i=n-1時,內循環需要執行n/n-1次;
當i=n時,內循環需要執行n/n次;
所以內循環總執行次數:1+1/2+1/3+……+1/n=lnn (高數下冊上有寫的。。級數那一章) ;
外循環爲n次;
故總的次數爲內外循環次數相乘爲 n * lnn ,所以總的時間複雜度爲n logn !

4.2堆排序

//堆排序
int arr[n+1] = {0,5,4,1,2,6,8,7,3}; // n = 8;
int main(){
    //自下向上對非葉結點進行adjust 
    for(i = n / 2 ; i >= 1 ; i--){
        //範圍n沒變 結點主動變化 
        HeapAdjust(i,n);
    } 
    //自上向下對每一個結點進行adjust 
    for(i = n ; i >= 1 ; i--){
        swap(1,i);
        //範圍因爲斷尾所以主動變化 結點1一直不變 
        HeapAdjust(1,i-1);
    }
}

//對第i個結點進行到範圍爲n的調整堆排序 
int HeapAdjust(int i,int n){
	int temp = arr[i],j;
	//j 爲左子節點,j+1爲右子節點 
	for(j = i*2 ; j <= n ; j=j*2){
		//能進入則j表示沒有超過當前範圍n ,即左節點存在 
		if(j+1 <= n && arr[j]<arr[j+1]){
			//能進入這裏則表示 j+1沒有超過當前未排序範圍,即右節點存在
			//j指向左右結點最大值 
			j++;
		}
		if(arr[j] > temp ){
			//能進入這裏表示子節點夠代替父節點,
			//隱含了交換父子結點的操作  arr[j] = temp;
			//大的子節點變成父節點 
			arr[i] = arr[j]; 
			//那麼交換後還要進行對交換成子節點的原父親結點堆調整 i=j 
			i = j;
			//1使i指向的新的待調整堆頂  2實現了後最一步統一交換 
		}else{
			break;
		}
		arr[i] = temp;
	}
} 

1. 從第一個非葉子結點開始到第一個結點進行堆調整
2. 堆調整:從當前結點的兩個兒子結點中取出一個最大的與之比較:
	大就交換;並把當前結點變成與之交換的兒子結點,再次進行堆調整。 
	小就退出返回第一步,進行下一個處理
3.然後把堆頂和堆尾交換,斷尾,再次對新堆頂進行堆調整,直到只剩一個。
分析:
main函數裏第一個for共循環了 n/2 次,O(n);
第二個for共循環了 n次,分離裏面的Heap Adjust(1,n)函數單獨分析:假設這個Heap Adjust的for循環共循環了k次,初試條件 j = 2 增加條件爲j*=2,則滿足2^k = n;這個for的時間複雜度爲O(logn);則第二個for總的爲O(nlogn).
所以T= O(n) + O(nlogn);故總的時間複雜度爲 O(nlogn)。

補充:利用了二叉樹的性質,堆排序在最好最壞情況下都是 nlogn。空間複雜度爲O(1)。

4.3快速排序

//快速排序
int a[10] = {2,10,12,11,1,5,7,3,4,9};
int n = 10;
void fastSort(int low,int high){
    //如果只有一個數,無須處理
	//這裏不能僅僅是等於; 
	//因爲在邊界處相遇的話,low會比high大1; 
	if (low >= high){
		return ;
	}
	//隨便拿哪個數開始當標準都可以 只要你的規則能依次拿完所有數 
    //這裏每次執行代碼會都會默認把第一個數當成軸
	int left = low , right = high , temp = a[left];
	while ( left != right){
		while ( left != right && a[right] >= temp ){
			right--;
		}
		a[left] = a[right];
		while ( left != right && a[left] <= temp ){
			left++;
		}
		a[right] = a[left];
	}
//	a[left] = temp; left和right都可以。
	a[right] = temp;
	fastSort(low,left-1);
	fastSort(left + 1, high);
}
  /*
    1.傳統每次都固定從第一個開始拿來當軸數,有可能發生最壞情況 ;
    2.一種安全的做法:隨機選取 ,儘量避免出現最壞情況;
    	int i = rand()%(left,right)+left;
    	swap(a[i],a[left]);
    	fastSort(left,right);
    3.中值法(三值中值法);
    */
如何得到這個nlogn的平均時間複雜度?
理解一:
在最優的情況下,每次拿來當軸的數的正確位置都恰好在中間,能平分整個數組;每一次遞歸調用該函數都會把問題分爲一半,這樣一樣就又成了一顆二叉樹,結點數爲規模n,其深度爲logn+1,每一次調用該遞歸函數的代價,也就是進行找到軸數的正確位置所花費的時間爲O(n),故 遞歸深度 * 每一次的代價 = n( logn + 1) ,故其複雜度爲O(nlogn).
理解二:這裏先利用公式套出它的遞歸時間複雜度公式:也就是
T[n] = O(1) ; n=1
T[n] = 2T[n/2] + n ; n>=1
解釋:在最優的情況下,每次拿來當軸的數的正確位置都恰好平分整個數組;每一次執行代碼會將數組分成兩個規模爲n/2的問題,並且還要加上找出正確位置所花費的時間,這裏爲線性時間複雜度O(n),n是實際傳入的規模,所以爲T[n] = 2T[n/2] + n !
用迭代法求解:
T[n] = 2T[n/2] + n //第一次遞歸
T[n] = 2*2 T[ n/ (2^2) ] + n+n //第二次遞歸
T[n] = 222 T[ n/ (222) ] +n+n+n //第三次遞歸
T[n] = 2^k T[ n/ (2^k) ] +n * k //第k次遞歸
當執行最後一次遞歸時候,必然有 T [n/ (2^k] = T[1] ,也即 n / (2^k)=1 ,解得 k = logn, 然後反帶入得T【n】= n + n *logn ;
又因爲當n >= 2時,有nlogn >= n (也就是logn > 1),所以取後面的 nlogn;
綜上所述所以爲O(nlogn)!

補充:

最差的情況時間複雜度爲O( n^2 )。怎麼理解:假設每一次取到的元素就是數組中最小/最大的,然後相當於每一次都排好一個極端元素的順序,沒有進行規模劃分,類比冒泡排序。

平均時間複雜爲O(nlogn)。

遞歸時壓入棧的數據佔用的空間爲O(logn),這是其空間複雜度。

4.4歸併排序

//歸併排序
int mergeSort(int left,int right,int temp[]){
	/*好像寫不等於也可以 */ 
	if(left != right){
		int mid = (left + right) / 2;
		mergeSort(left,mid,temp);
		mergeSort(mid+1,right,temp);
		merge(left,mid,right,temp);  
	}
}
int merge(int left,int mid,int right,int temp[]){
	/*連接重排 left-mid 和 mid+1 - right 的數組*/
	int i = left; // 初始化i, 左邊有序序列的初始索引
    int j = mid + 1; //初始化j, 右邊有序序列的初始索引
    int t = 0; // 指向temp數組的當前索引
    while(i <= mid && j <= right){
    	if(arr[i]<arr[j]){
    		temp[t++] = arr[i++];
		}else{
			temp[t++] = arr[j++];
		}
	}
	while(i <= mid){
		temp[t++] = arr[i++];
	}
	while(j <= right){
		temp[t++] = arr[j++];
	}
	/*將部分排好的數組temp 複製粘貼給arr的相應部分*/
	int tempLeft = left;
	i = 0;
	while(i < t){
		arr[tempLeft++] = temp[i++];
	}
}

理解一: 和最優條件下的快速排序分析方法類似,mergeSort () 函數內部會把該次執行的規模n劃分爲兩個規模爲n/2的問題進行遞歸,也是可以畫出一顆二叉樹,其深度爲logn+1;每一次遞歸都會執行 merge函數,這個函數代價很容易得出來,爲O(n)n爲當次傳入的問題規模,第一次爲n,第二次爲n/2…然後總的時間複雜度爲:depth * f(n) = (logn + 1 ) * n = O(nlogn).
理解二:得到其遞歸時間複雜度公式和最好情況快速排序一樣,分析基本無差,參考上面的快速排序。
T[n] = O(1) ; n=1
T[n] = 2T[n/2] + n ; n>=1

最差和平均條件下都是nlogn的時間複雜度,這裏更應關心其空間複雜度—臨時的數組和遞歸時壓入棧的數據佔用的空間:n + logn;所以空間複雜度爲: O(n)。

五:O(n^2):排序中容易看到

5.1簡單循環

for(i=0;i<n;i++){
    for(j=0;j<n;j++){
    	/**/
    }
}

兩層循環,時間複雜度:O(n^2);

5.2冒泡排序

//最外層可以是n-1層 
for(i = 0 ; i < n-1; i++){
    //內層需要記住 存在j+1那麼j最多隻能取到倒數第二位;且趟循環總是那次循環的最後一個數換到最值,選擇排序則是那一趟的第一個數與最值下標k交換;
	for(j = 0; j < n-(i+1)  ; j++ ){
		if(arr[j] > arr[j+1])
			swap(j,j+1);
	}
}
理解:外循環一共會執行n-1次,而內循環條件爲 j < n - (i + 1) ,按照循環規律依次寫處內循環的循環次數:
當i=1時,內循環需要執行n-1次;
當i=2時,內循環需要執行n-2次;
當i=3時,內循環需要執行n-3次;
當i=n-2時,內循環需要執行n - (n - 2 + 1 ) = 1次;此時循環結束;
所以循環總執行次數:1 + 2 + 3 + … + n-1 = n(n-1) / 2 ;
故時間複雜度爲O(N^2);
最壞最好情況下時間複雜度都是 (N^2);如何理解?
最好的情況就是上面推導的過程,也就是沒有進swap進行交換,所以每一次內循環的代價都是0;所以爲n(n-1) / 2;
最差情況下,也就是剛好倒序或者順序,那麼跟最好情況的區別就是每一次內循環都執行了swap函數,代價增加了三條交換語句,所以總時間複雜度增大爲 3 *n(n-1) / 2 ,但其級數趨近後的式子仍然是O(n^2)!
平均情況當然也爲O(N^2)咯。

5.3插入排序

//直接插入排序
#define n 8
int arr[n] = {8,7,6,5,4,3,2,1};
for( i = 1 ; i < n ; i++){
	temp = arr[i];
	for(j = i - 1 ; j >= 0 ;j--){
		//直接插入
		if(arr[j] > temp){
			arr[j + 1] = arr[j];
		}else{
			break;
		}
	}
	arr[j + 1] = temp;
}
分析:
最壞情況下:
i = 1 ,內循環 1次,
i = 2 ,內循環2次
i = n - 1 ,內循環n-1次;
故總的循環:1 + 2 + 3 + … + (n-1) = n *(n-1)/ 2;
故時間複雜度爲O(n^2)!

5.4選擇排序

//同冒泡 最外層可以是n-1層
for(i = 0 ; i < n - 1 ; i++){
	k = i;
    //讓第一個數去換最值下標k
	for(j = i + 1 ; j < n ; j++){
		if(arr[k] > arr[j]){
			k = j ;
		}
	}
	if(k != i)
		swap(i,k);
}
同冒泡排序一樣,外循環一共會執行n-1次,而內循環條件爲 j < n - (i + 1) ,按照循環規律依次寫處內循環的循環次數:
當i=1時,內循環需要執行n-1次;

當i=n-2時,內循環需要執行n - (n - 2 + 1 ) = 1次;此時循環結束;
所以循環總執行次數:1 + 2 + 3 + … + n-1 = n(n-1) / 2 ; 故時間複雜度爲O(N^2);

六:O(2^n)

6.1斐波那契的遞歸算法

long Fibonacci(int n) {
     if (n == 0)
         return 0;
     else if (n == 1)
         return 1;
     else
         return Fibonacci(n - 1) + Fibonacci(n-2);
 }
有點像細胞分裂,每一次進入Fibonacci函數,就會分裂成兩個小Fibonacci函數,這個兩個小Fibonacci函數又會分裂出4個小小Fibonacci函數,至少要進入n次,故其時間複雜度爲O(2^n)。

待更新

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章