演算法複雜度的分析——時間複雜度和空間複雜度
演算法的複雜度
如何分析一個演算法的複雜度?
演算法的時間複雜度和空間複雜度統稱為演算法的複雜度
時間複雜度:
下面程式碼的迴圈語句總共會執行多少次?
語句總執行次數:f(n) = n^2 + 2*n + 10void 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++; } }
時間複雜度實際就是一個函式,該函式計算的是執行基本操作的次數
注意:此時使用執行次數來衡量演算法的好壞,是因為在不同的環境下面,同一段程式碼的執行效率也會有所不同,比如:在普通膝上型電腦執行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);
此時:迴圈執行10+2*n次,常數次和常數係數不計算在時間複雜度之內,則時間複雜度為:O(n);void Test1(int n) { int iCount = 0; for (int iIdx = 0; iIdx < 10; ++iIdx) { iCount++; } for (int iIdx = 0; iIdx < 2*n; ++iIdx) { iCount++; } }
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;
}
}
限於編者水平,有很多的不正確的地方,歡迎各位前來指正!