1. 程式人生 > >演算法複雜度的分析——時間複雜度和空間複雜度

演算法複雜度的分析——時間複雜度和空間複雜度

演算法的複雜度

如何分析一個演算法的複雜度?

演算法的時間複雜度和空間複雜度統稱為演算法的複雜度

時間複雜度:

下面程式碼的迴圈語句總共會執行多少次?

void Test(int n) 
{ 
	int iConut = 0; 
	for(int i = 0; i < n; ++i) 
	{ 
		for(int j = 0; j < n; ++j) 
		{ 
			iCount++; 
		} 
	} 
	for(int k = 0; k < 2*n; ++k)
	{ 
		iCount++; 
	} 
	int count = 10;
	while(count--) 
	{ 
		iCount++; 
	} 
}
語句總執行次數:f(n) =   n^2 + 2*n + 10
時間複雜度實際就是一個函式,該函式計算的是執行基本操作的次數

注意:此時使用執行次數來衡量演算法的好壞,是因為在不同的環境下面,同一段程式碼的執行效率也會有所不同,比如:在普通膝上型電腦執行10s的程式,在超級計算機上可能0.1s都不到就可以 執行出結果。

演算法存在最好、平均和最壞情況:
最壞情況:任意輸入規模的最大執行次數(上界)
平均情況:任意輸入規模的期望執行次數
最好情況:任意輸入規模的最小執行次數,通常最好情況不會出現
(下界)
例如:在一個長度為N的線性表中搜索一個數據x
最好情況:1次比較
最壞情況:N次比較
平均情況:N/2次比較
在實際中通常關注的是演算法的最壞執行情況,即:任意輸入規模N,演算法
的最長執行時間。理由如下:
①一個演算法的最壞情況的執行時間是在任意輸入下的執行時間上界
②對於某些演算法,最壞的情況出現的較為頻繁
③大體上看,平均情況與最壞情況一樣差
因此:一般情況下使用O漸進表示法來計算演算法的時間複雜度

此處有一段關於時間複雜度的解釋,我覺得很貼切,大家可以參考一下:

(轉載)一個演算法語句總的執行次數是關於問題規模N的某個函式,記為f(N),N稱為問題的規模。語句總的執行次數記為T(N),當N不斷變化時,T(N)也在變化,演算法執行次數的增長速率和f(N)的增長速率相同。則有T(N) =O(f(N)),稱O(f(n))為時間複雜度的O漸進表示法。

常見的時間複雜度:

void Test0(int n) 
{ 
	int iCount = 0; 
	for (int iIdx = 0; iIdx < 10; ++iIdx) 
	{ 
		iCount++; 
	} 
} 
此時迴圈執行10次,為常數次,則時間複雜度為:O(1);
void Test1(int n) 
{ 
	int iCount = 0; 
	for (int iIdx = 0; iIdx < 10; ++iIdx) 
	{ 
		iCount++; 
	} 
	for (int iIdx = 0; iIdx < 2*n; ++iIdx) 
	{ 
		iCount++; 
	}
}
此時:迴圈執行10+2*n次,常數次和常數係數不計算在時間複雜度之內,則時間複雜度為:O(n);
void Test2(int n) 
{ 
	int iCount = 0; 
	for (int iIdx = 0; iIdx < 10; ++iIdx) 
	{ 
		iCount++; 
	} 
	for (int iIdx = 0; iIdx < 2*n; ++iIdx) 
	{ 
		iCount++; 
	} 
	for (int i = 0; i < n; ++i) 
	{ 
		for (int j = 0; j < n; ++j) 
		{ 
			iCount++; 
		} 
	} 
} 
程式迴圈執行次數為:10+2*n+n^2

出去常數和常數係數,選擇增長最快的一部分,作為時間複雜度:O(n^2)

void Test3(int m, int n) 
{ 
 int iCount = 0; 
 for (int i = 0; i < m ; ++i) 
 { 
 iCount++; 
 } 
 for (int k = 0; k < n ; ++k) 
 { 
 iCount++; 
 } 
} 
此時有m 和n兩個不確定的迴圈上界,則此時的時間複雜度為:O(m+n);
void Test4(int m, int n)// f(n,m) = 2*m*n == O(m*n)
{ 
 int iCount = 0; 
 for (int i = 0; i < 2*m ; ++i) 
 { 
 for (int j = 0; j < n ; ++j) 
 { 
 iCount++; 
 } 
 } 
} 
此時m和n是兩個巢狀的位置迴圈次數,則時間複雜度為:O(m*n);


一般演算法O(n)計算方法:
①用常數1取代執行時間中的所有加法常數
②在修改後的執行次數函式中,只保留最高階項
③如果最高階項係數存在且不是1,則去除與這個項相乘的常數


分治演算法的時間複雜度:

我們用簡單的二分查詢為例子,(下面為二分查詢的程式碼):

#include <stdio.h>
#include <assert.h>
int BinarySearch(int *a ,size_t size,int x){
	size_t mid = 0;
	size_t left = 0;
	size_t right = size -1;
	assert(a);
	while(left<=right){
		mid = left+((right - left )>>1);
		if(a[mid] < x){
			left = mid+1;
		}
		else if(a[mid] > x){
			right = mid -1;
		}
		else return mid;
	}
	return -1;
}
int main(){
	int a[10]= {1,2,3,4,5,6,7,8,9,10};
	printf("%d",BinarySearch(a,10,10));
	return 0;
}
此時:二分查詢函式的引數為:要查詢的陣列,陣列的大小,要查詢的數;

我們可以把整個有序陣列比作一個二叉樹,根節點的左子樹都小於根,右子樹都大於根,二叉樹有N個結點,則二叉樹的高度就是:h≈log以2為底的N次方。

顯然有N個結點的M叉樹的高度就是log以M為底的N次方。

此時最壞的情況就是把所有的結點都了以便,即二分查詢的時間複雜度就是:O(log以2為底的N次方);

我們下面給出二分查詢的遞迴演算法,以及所有的測試用例:

int BinarySearch(int* a,size_t left,size_t right, int x)//二分查詢的遞迴演算法
{
size_t mid;
assert(a);
mid = left+((right - left)>>1);
if(left > right )return -1;
if(a[mid] > x){
BinarySearch(a,left,mid-1,x);
}
else if(a[mid]<x){
BinarySearch(a,mid+1,right,x);
}
else if (a[mid] == x)
return mid;
}
int main(){
size_t mid,left,right;
int a[10]= {1,2,3,4,5,6,7,8,9,10};
left = 0;
right = 9;
printf("%d",BinarySearch(a,left,right,10));
printf("%d",BinarySearch(a,left,right,9));
printf("%d",BinarySearch(a,left,right,8));
printf("%d",BinarySearch(a,left,right,7));
printf("%d",BinarySearch(a,left,right,6));
printf("%d",BinarySearch(a,left,right,5));
printf("%d",BinarySearch(a,left,right,4));
printf("%d",BinarySearch(a,left,right,3));
printf("%d",BinarySearch(a,left,right,2));
printf("%d",BinarySearch(a,left,right,1));
printf("%d",BinarySearch(a,left,right,33));
return 0;
}
其中遞迴演算法時間複雜度:遞迴總次數*每次遞迴次數

空間複雜度
空間複雜度:函式中建立物件的個數關於問題規模函式表示式

int Sum(int N) 
{ 
 int count = 0; 
 for(int i = 1; i <= N; ++i) 
 count += i; 
 return count; 
} 
此時程式只建立了常數個變數,則空間複雜度就是:O(1);

下面這段程式碼功能是將兩個陣列按照一定得順序進行合併:

int* Merge(int* array1, int size1, int* array2, int size
2)
{ 
 int index1 = 0, index2 = 0, index = 0; 
 int* temp= (int*)malloc(sizeof(int)*(size1+size2)); 
 if(NULL == temp) 
 return NULL; 
 while(index1 < size1 && index2 < size2) 
 { 
 if(array1[index1] <= array2[index2]) 
 temp[index++] = array1[index1]; 
 else
 temp[index++] = array2[index2]; 
 } 
 while(index1<size1) 
 temp[index++] = array1[index1++]; 
 while(index2 < size2) 
 temp[index++] = array2[index2++]; 
 return temp; 
} 
因為要合併,所以要建立兩個陣列總共大小的空間,去存放兩個陣列的裡面的變數,所以空間複雜度為:O(size1+size2);

下面我們分析一下斐波那契數列的時間和空間複雜度:

long long Fib(int n) 
{ 
 if(n < 3) 
 return 1; 
 return Fib(n-1)+Fib(n-2); 
} 
之前有說過:其中遞迴演算法時間複雜度:遞迴總次數*每次遞迴次數。

此時,當作一顆二叉樹,第一次n=3,即根節點為n=3,左子樹呼叫傳參:n=2,右子樹呼叫傳參n=1;

則其二叉樹高度為:h≈log以2為底3次方,結點個數為2^h+1,也就是也就是呼叫了2^n+1遍,其中1為常數,所以得:時間複雜度為O(2^N);
在呼叫的過程中,沒有建立臨時變數,則空間複雜度為:O(N);

斐波那契尾遞迴實現:

時間複雜度:O(n);

long Fib(long first, long second, long N) 
{ 
 if(N < 3) 
 return 1; 
 if(3 == N) 
 return first+second; 
 return Fib(second, first+second, N-1); 
} 

對於尾遞迴的空間複雜度,是和編譯器有一定關係的,在VS環境執行程式碼,編譯器是會對尾遞迴進行優化

假設我們在main函式中呼叫Fib函式:Fib(1,1,10);則編譯器會在棧中為Fib(1,1,10)建立一個棧區,

但是在呼叫Fib(1,2,9)的時候,此時函式已經不會再對之前的數值在做任何的操作了,所以在函式一層一層遞迴之後,返回的時候,不再需要空間對之前的值進行更改,所以編譯器不會在給Fib(1,2,9)這個函式開闢新的棧,直接就在Fib(1,1,10)的棧區進行操作。

總結:在編譯器對尾遞迴進行優化的時候,空間複雜度為:O(1);如果不做優化的話,那麼空間複雜度就是:O(n)。

在給出非遞迴實現:

long Fibonacci(int n) {
      if (n <= 2)
          return 1;
      else {
          long num1 = 0;
          long num2 = 1;
         for (int i = 2;i < n - 1;i++) {
             num2 = num1 + num2;
             num1 = num2 - num1;
         }
         return num2;
     }
 }

限於編者水平,有很多的不正確的地方,歡迎各位前來指正!