對於評價一個算法的好壞,我們通常以時間複雜度與空間複雜度來衡量。
時間複雜度:算法中執行基本操作語句的總次數。
空間複雜度:算法中創建對象的個數。
一.時間複雜度
這裏有一點需要注意:對於時間複雜度而言,我們並沒有算法的運行總時間來衡量算法的好壞,這裏最重要的原因是對於一個算法的運行時間,它受外在的其他因素的影響較大,而且能夠影響時間的因素又有很多。
而對於一個算法的時間複雜度,我們可以大致分爲三種情況:
1.最壞情況:任意輸入規模的最大運行次數(上界);
2.平均情況:任意輸入規模的期望運行次數;
3.最好情況:任意輸入規模的最小運行次數,通常最好情況不會出現。(下界)
而我們在實際中我們通常情況考量的是算法的最壞運行情況。也就是說對於任意輸入規模N,算法的最長運行時間,理由如下:
1. 一個算法的最壞情況的運行時間是在任意輸入下的運行時間上界。
2. 對於某些算法,最壞的情況出現的較爲頻繁。
3. 大體上看,平均情況與最壞情況一樣差。
算法分析要保持大局觀:
1. 忽略掉那些的常數。
2. 關注運行時間的增長趨勢,關注函數式中增長最快的表達式。
由此,我們通常通過O的漸進表達式來表示我們的時間複雜度,而我們原本用f(n)也就是算法中所有基本語句的總次數來衡量,而對於O漸進表達式我們是通過f(n)來求的,也就是O(f(n))的求法如下:
令n趨近無窮大,忽略對這個式子的影響最小項,以常數1來代替所有的常數項,只保留最高項,並且如果最高項係數爲常數,則令最高項係數爲1
二.空間複雜度
對於我們的空間複雜度,其實與時間複雜度類似,它也通常用O漸進表達式來表示,只不過時間複雜度的f(n)是我們算法中的所有基本語句總次數,而這裏f(n)則是算法中創建對象的個數,而通過f(n)來求O(f(n))的方法都是一樣的。
下面給出兩個算法,來求它們的時間複雜度與空間複雜度:
1.二分查找(非遞歸版本)
int BinarySearch(int* arr,int size,int data)
{
int left=0;
int right=size;
int mid=0;
while(left<right)
{
mid=(left&right)+((left^right)>>1);
if(arr[mid]==data)
return mid;
else if(arr[mid]>data)
{
right=mid;
}
else
{
left=mid+1;
}
}
return -1;
}
這裏空間複雜度很好求,爲O(1),而對於時間複雜度,我們可以來分析一下:
我們的二分查找也叫折半查找,如上圖所示,每次查找都會對當前的查找範圍縮小一半,最壞的情況就是直到將我們的查找範圍縮小至只有一個數據的時候才找到,或者根本找不到,無論何種情況,到了這一步,我們的算法都將會退出,由此,我們就可以找到查找範圍size和查找次數M的關係爲size/(2^M)=1。而這裏我們就可以求出M=log2(size),這裏2爲底數。所以我們的時間複雜度就爲O(log2(n))。
2.二分查找(遞歸算法)
int BinarySearch(int* arr,int left,int right,int data)
{
int mid=(left&right)+((left^right)>>1);
if(left<=right)
{
if(arr[mid]==data)
return mid;
else if(arr[mid]>data)
return BinarySearch(arr,left,mid-1,data);
else
return BinarySearch(arr,mid+1,right,data);
}
return -1;
}
對於遞歸算法,我們要明確它的時間複雜度爲遞歸調用的總次數*每次進行遞歸調用中執行基本語句的總次數;而對於它的空間複雜度,它等於遞歸深度*每層遞歸創建的對象個數。
而這裏對於二分查找的遞歸算法,它的時間複雜度與非遞歸的一樣,還是爲O(log2(n)),原本非遞歸的版本是將所有的折半放在一個函數調用當中,而這裏只不過是將每一次的折半都換成一次遞歸調用自己,遞歸的次數和深度均爲log2(n),總體計算下來時間複雜度還是沒變的。
但是對於空間複雜度,這裏由於遞歸深度爲log2(n),所以空間複雜度變爲O(log2(n))了。
3.斐波那契數列(遞歸算法)
int Fib(int n)
{
if(n<3)
return 1;
return Fib(n-2)+Fib(n-1);
}
對於斐波那契數列的遞歸算法,我們一眼是很難看出它的時間複雜度和空間複雜度的,這種情況最好畫圖來分析:
(以N=5爲例)
當我們需要求Fib(5)的時候,則必須要先知道Fib(3)和Fib(4),要知道Fib(4)則要知道Fib(3)和Fib(2),總之我們這個算法的核心就是通過Fib(n-1)與Fib(n-2)來求出Fib(n),由此往下直至已知的Fib(1)和Fib(2),由此我們就得到了上圖的樹結構,其中的每一個節點都是一次遞歸調用,而我們知道當n趨近無窮大的時候,最後一層的那兩個節點是可以忽略的,所以我們可以根據層數與節點數之間的關係m=2^(n)以及層數與所給的參數N之間的關係N=n+3(忽略最後一層),求出總節點數(即遞歸調用總次數)M=2^(N-3) -1,而遞歸深度也就是我們的層數n=N-2。
所以我們可以得到我們的時間複雜度爲O(2^N),空間複雜度爲O(N)。
4.斐波那契數列(非遞歸算法)
int Fib(int n)
{
if(n<3)
return 1;
int ret=0;
int first=1;
int second=1;
for(int i=2;i<n;i++)
{
ret=first+second;
first=second;
second=ret;
}
return ret;
}
對於斐波那契數列的非遞歸算法,它的時間複雜度與空間複雜度就很簡單了,分別爲O(N)和O(1)
所以,你明白爲什麼對於斐波那契數列我們這麼牴觸遞歸了嗎?
常見的時間複雜度與空間複雜度的O漸進表達式之間的複雜度大小關係:
O(1)<O(logN)<O(N)<O(NlogN)<O(N^2)<O(N^3)<O(2^N)<O(N!)<O(N^N)