數據結構之算法的時間與空間複雜度
1.算法效率
算法效率分析分爲兩種:第一種是時間效率,第二種是空間效率。時間效率被稱爲時間複雜度,而空間效率被稱作空間複雜度。時間複雜度主要衡量的是一個算法的運行速度,而空間複雜度主要衡量一個算法所需要的額外空間,在計算機發展的早期,計算機的存儲容量很小。所以對空間複雜度很在乎。但是經過計算機行業的迅速發展,計算機的存儲容量已經達到了極高的水平。所以如今我們已經不需要再特別關注一個算法的空間複雜度。
2.時間複雜度
2.1.什麼是時間複雜度
基本概念:在計算機科學中,算法的時間複雜度是一個函數,它定量描述了該算法的運行時間。一個算法執行所耗費的時間,從理論上說,是不可以算出來的,只有把程序放到計算機上跑起來,纔可以測試出來。但是我們需要每個算法都上機測試,這很麻煩,所以纔有了時間複雜度這個分析方式。一個算法所花費的時間與其中語句的執行次數成正比例,算法中的基本操作的執行次數,爲算法的時間複雜度。
2.2 大O的漸進表示法
2.2.1.例題分析
例:請計算Func基本操作執行了多少次?
void Func(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
Func1 執行的基本操作次數爲 :
F(N) = N^2 + 2*N + 10;
1.若 N = 10 F(N) = 130
2.若 N = 100 F(N) = 10210
3.若 N = 1000 F(N) = 1002010
…
在實際中我們計算時間複雜度時,我們其實並不一定要計算精確的執行次數,而只需要大概執行次數,我們使用大O的漸進表示法。
2.2.2.大O的漸進表示法
大O符號(Big O notation):是用於描述函數漸進行爲的數學符號。
推導大O階方法:
1.用常數1取代運行時間中的所有加法常數。
2.在修改後的運行次數函數中,只保留最高階項。
3.如果最高階項存在且不是1,則去除與這個項目相乘的常數。得到的結果就是大O階。
使用大O的漸進表示法以後,Func的時間複雜度爲:
1.若 N = 10 F(N) = 100
2.若 N = 100 F(N) = 10000
3.若 N = 1000 F(N) = 1000000
…
通過上面我們會發現大O的漸進表示法去掉了那些對結果影響不大的項,簡潔明瞭的表示出了執行次數。
另外有些算法的時間複雜度存在最好、平均和最壞情況:
最壞情況:任意輸入規模的最大運行次數(上界)
平均情況:任意輸入規模的期望運行次數
最好情況:任意輸入規模的最小運行次數(下界)
例如:在一個長度爲N數組中搜索一個數據x
最好情況:1次找到 (數據在數組開頭)
最壞情況:N次找到 (數據在數組末尾)
平均情況:N/2次找到
在實際中一般情況關注的是算法的最壞運行情況,所以數組中搜索數據時間複雜度爲O(N) 。
2.3.時間複雜度計算例題
1. 計算Func1的時間複雜度
void Func1(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
解答:func1基本操作執行了2N+10次,通過推導大O階方法可知,func1的時間複雜度爲 O(N)
2. 計算func2的時間複雜度
void Func2(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
++count;
}
for (int k = 0; k < N ; ++ k)
{
++count;
}
printf("%d\n", count);
}
解答:func2基本操作執行了M+N次,有兩個未知數M和N,故時間複雜度爲 O(N+M)
3. 計算func3的時間複雜度
void Func3(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
解答: func3基本操作執行了10次,通過推導大O階方法,10爲常數,故時間複雜度爲 O(1) 。
4. 計算二分查找的時間複雜度。
int BinaryFind(int *a, int n, int x)
{
assert(a);
int begin = 0;
int end = n - 1;
while(begin < end)
{
int mid = begin + ((end - begin) >> 1);
if(a[mid] == x)
return mid;
else if(a[mid] > x)
end = mid - 1;
else
begin = mid + 1;
}
return -1;
解答:二分查找基本操作執行最好1次,最壞O(logN)次 (2^x = N;x = logN),故時間複雜度爲 O(logN) .(logN在算法分析中表示是底數爲2,對數爲N.)
5.計算遞歸斐波那契數列的時間複雜度
unsigned int Fibonacci(size_t N)
{
if(N < 2)
return N;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
解答:通過計算分析發現斐波那契數列遞歸了2^N 次,時間複雜度爲O(2^N)。
3.空間複雜度
3.1.什麼是空間複雜度
基本概念:空間複雜度是對一個算法在運行過程中臨時佔用存儲空間大小的量度。空間複雜度不是程序佔用了多少字節的空間,因爲這個沒有意義,所以空間複雜度算的是變量的個數。空間複雜度計算規則基本跟時間複雜度類似,也使用大O漸進表示法。
3.2.空間複雜度計算例題
1. 計算冒泡排序的空間複雜度
void BubbleSort(int* a, int n)
{
assert(a);
int end = n - 1;
while(end >= 0)
{
int exchange = 0;
for(int i = 0; i < end; i++)
{
if(a[i] > a[i + 1])
{
int tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
exchange = 1;
}
}
--end;
if(exchange == 0)
break;
}
}
冒泡排序使用了常數個額外空間,所以空間複雜度爲 O(1)
2. 計算階乘遞歸斐波那契數列的空間複雜度
unsigned int Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
斐波那契數列遞歸調用了N次,開闢了N個棧幀,每個棧幀使用了常數個空間。空間複雜度爲O(N)