在開發(fā)的時候,我們?nèi)绾卧u估一個算法的好壞,如何描述一個算法運(yùn)行效率的高低呢?通俗一點的表達(dá)方法就是程序執(zhí)行快或慢,但是這只是一種較為寬泛的描述,我們?nèi)绾沃庇^科學(xué)的用的描述它呢?
有同學(xué)可能會說,用其運(yùn)行時間不就可以很好很直觀的描述它了。不過,不同的語言,不同的編譯器,不同的CPU來說,對程序的處理的時間是不同的,我們無法單單用運(yùn)行時間來描述某個算法執(zhí)行效率。另外,當(dāng)需要處理的數(shù)據(jù)增長時,算法的基本操作要重復(fù)執(zhí)行的次數(shù)也會增長,對于不同的算法的增長的速度也不一樣。
數(shù)學(xué)果然是個不錯的工具,為了描述算法的運(yùn)行時間的增長情況,我們可以用不同數(shù)學(xué)公式來分析。在計算機(jī)科學(xué)上,我們是有專門的術(shù)語來表征算法的效率,就是今天要和大家一起學(xué)習(xí)的大O表示法。大O并不是表示算法運(yùn)行需要多長時間,它表示的是算法運(yùn)行時間的增速,即算法的運(yùn)行時間以不同的速度增加,也叫漸進(jìn)時間復(fù)雜度。
我們可以用下面的表達(dá)式來表示:
通常主要有以下幾種表達(dá)式來描述時間復(fù)雜度:
- O(1):常量時間
- O(n):線性時間
- O(log n):對數(shù)時間
- O(n^2):二次方時間
- O(2^n):指數(shù)時間
- O(n!):階乘時間
每種時間復(fù)雜度有所不同,下面我們一起來詳細(xì)了解這幾種時間復(fù)雜度。
大O復(fù)雜度
O(1)
O(1)表示常量時間復(fù)雜度,當(dāng)給定大小為n的輸入,無論n為何值,最后算法執(zhí)行的時間是個常量。舉個例子:
int func(int n)
{
n++;
return n*2;
}
上面的程序中,無論輸入n的值如何變化,程序執(zhí)行時間始終是個常量。我們簡化處理一下,假如函數(shù)中每行語句的執(zhí)行時間是1,則執(zhí)行時間的數(shù)學(xué)表達(dá)式:
無論n為多大,最后的執(zhí)行時間都是2這個固定值。雖然是運(yùn)行時間為2,但是這里我們也用O(1)來表示,這里的1代表是一個常數(shù)。
O(n)
O(n)表示線性時間復(fù)雜度,算法的執(zhí)行時間隨著輸入n的大小成線性變化。
int func(int n)
{
int sum = 0;
for(int i=0; i<n; i++)
{
sum = sum + i;
}
return sum;
}
上面的這個程序中,函數(shù)的執(zhí)行時間隨著n的變化成線性的關(guān)系。
對于這種可以用線性表達(dá)式表示的情況,我們用O(n)來表示。
為什么可以省略掉表達(dá)式中的其他系數(shù)呢?主要是當(dāng)n趨近于無窮大時,系數(shù)相對于無窮大的n來說可以忽略不計。
O(n^2 )
O(n^2)表示二次方時間復(fù)雜度,一個算法的時間將會隨著輸入數(shù)據(jù)n的增長而呈現(xiàn)出二次關(guān)系增加。
int func(int n)
{
int sum = 0;
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
{
sum = sum + i + j;
}
}
return sum;
}
上面的程序中,是個兩層循環(huán)的程序,函數(shù)的執(zhí)行時間和n是二次方的關(guān)系:
對于這種類型的程序,我們可以用O(n^2)表示。不過,循環(huán)嵌套除了這種兩層循環(huán)之外,還會有三層、四層...n層循環(huán),對應(yīng)的其復(fù)雜度就是O(n^3) 、O(n^4)...O(n^n)。
O(2^n)
O(2^n)表示指數(shù)復(fù)雜度,隨著n的增加,算法的執(zhí)行時間成倍增加,它是一種爆炸式增長的情況。
int func(int n)
{
if(n==0) return 1;
return func(n) + func(n-1)
}
上面的代碼中,有兩次遞歸調(diào)用,函數(shù)的執(zhí)行時間就會和輸入n成指數(shù)的關(guān)系。
因此,這里我們可以用O(2^n)表示。
O(log n)
O(log n)表示對數(shù)時間復(fù)雜度,算法執(zhí)行時間和n是一種對數(shù)關(guān)系。這種類型的算法會在執(zhí)行的過程中,隨著程序的執(zhí)行其完成某個功能的操作步驟越來越少。其中,我們所熟知的二分查找法就是一個很好的例子。比如,下面這個代碼在一個有序列表中查找某個值的位置,我們通過二分法進(jìn)行查找。
int func(int a[], int size, int num)
{
int left = 0;
int right = size-1;
while(left <= right)
{
int mid = (left + right)/2;
if(a[mid] > num)
{
right = mid - 1;
}
else if (a[mid] < num)
{
left = mid + 1;
}
else
{
return num;
}
}
return -1;
}
在最糟糕的情況下,我們通過二分法拆分x次后,最后一個元素就是我們要找的元素。我們可以得到下面的等式:
函數(shù)運(yùn)行時間可以表示為:
因此,這里我們可以用O(log n)表示。
O(n!)
對于階乘關(guān)系的復(fù)雜度,最典型的例子就是旅行商問題。
假設(shè)有一個旅行商人要拜訪n+1個城市,他必須選擇所要走的路徑,路徑的限制是每個城市只能拜訪一次,而且最后要回到原來出發(fā)的城市。路徑的選擇目標(biāo)是要求得的路徑長度為所有路徑之中的最小值。
這個問題最簡單的方法是通過窮舉法列出所有的排列組合。如果有n+1個城市,根據(jù)我們數(shù)學(xué)中學(xué)過的排列組合計算方法,可以算出所有組合數(shù)為n!,所以這種窮舉法對應(yīng)的時間復(fù)雜度也就是O(n!)了。