挑戰408——數據結構(3)——Big-O表示法

對比上篇的文章,通常對於數值型的操作,通常可以用數值的本身來反映問題的規模。對於大多數的關於數組或者vector的算法,通常參與運算的數的多少本身就代表了問題的規模。在評估算法效率過程中,我們通常用字母N來表示問題規模的大小,當N逐漸增大時,N與算法性能之間的關係稱爲算法複雜度The relationship between N and the performance of an algorithm as N becomes large is called the computational complexity of that algorithm
通常,算法最重要的性能指標就是執行時間,儘管算法分析的時候,也要考慮到運行時所需的內存空間,但是因爲隨着科技的發展,現在的空間已經不是首要的考慮因素。所以我們考慮問題首先考慮時間複雜性。
通常我們使用一種稱爲大O符號的特殊速記符號來表示算法的計算複雜度。該符號是德國數學家保羅·巴赫曼(Paul Bachmann)在1892年前在計算機開發之前提出的。符號本身很簡單,由字母O組成,後跟括號括起來的公式。當用於指定計算複雜度時,公式通常是涉及問題大小N的簡單函數。比如下面的表示方法:
在這裏插入圖片描述
記爲O(N^2),(英文讀作:big-oh of N squared)

Big-O的簡化

使用大O符號來估計算法的計算複雜度時,其目的是提供一個對問題的定性觀察,以便觀察當N中的變化(如N變大)是怎麼影響算法性能的。因爲大O表示法並不是一個量化措施,所以要簡化括號內的公式,以便以最簡單的形式捕獲算法的定性行爲。使用big-O表示法時可以使用的最常見的簡化如下:

  1. 去除當N變大時,任何對算法性能的影響不再顯著的部分(Eliminate any term whose contribution to the total ceases to be significant as N becomes large)。當一個公式涉及多個部分時,其中一個部分通常比其他部分增長得更快,並且隨着N變大而最終導致影響整個表達式。對於N值較大的值,此部分單獨控制算法的運行時間,你可以完全忽略公式中的其他部分。(類似求極限,N->無窮,可以忽略低次冪部分)。
  2. 去除常數因素。(Eliminate any constant factors.) 當計算複雜度時,主要關注的是運行時間如何隨着問題大小N的變化而變化。常數因子對整體模式沒有影響。

例如:
在這裏插入圖片描述
可以簡化爲:
在這裏插入圖片描述
此表達式抓住了選擇排序的性能的本質。隨着問題的大小增加,運行時間的增長趨向於問題規模的平方。 因此,如果你將vector的大小加倍,則運行時間將增加四倍。如果你將輸入值的數量乘以10,則運行時間將會增加100倍。

計算算法複雜度

根據之前的算法,算法的複雜度肯定是越小越好的。那麼如何從代碼中降低算法的複雜度呢?
我們先看一段代碼,這段代碼是用來求vector中的數據的平均值的:

double average(vector<double> & vec) {
    int n = vec.size();
    double total = 0;
    for (int i = 0; i < n; i++) {
    total += vec[i];
}
    return total / n;
}

現在我們來分析一下這個算法的複雜度。
當調用此函數時,代碼的某些部分只會執行一次,例如初始化total爲0,並在返回語句中進行除法運算。這些計算需要一定的時間,但是他們不依賴於vector大小的意義上(即無論vector多大,他們運行的時間和次數都是確定的),運行時間是恆定的。對於這種執行時間不依賴於問題大小的代碼被稱爲以恆定時間運行(constant time),以大O表示法表示爲O(1)。
有些人可能會對O(1)的心存疑惑,因爲O記法是要依賴於N的。但是實際上,這種不依賴於N的符號,是O(1)符號的整體。因爲增加問題的大小時,這部分代碼的運行時間根本不增加(因爲它是確定的,就像數學函數中的Y = 1的一條線,它斜率爲0,表示隨着問題規模的增大,運行時間不變。)。
現在再來分析一下for循環裏面的語句:


for (int i = 0; i < n; i++) {
    total += vec[i];
}

對於for循環來說,的每個循環都是執行 一次。這些部分包括在for循環語句中的表達式i ++還有下面的語句,它們構成循環體:

total += vec[i];

雖然這裏這部分操作的每執行一次執行都花費了固定的時間t(假設這部分操作爲 i 自增一次,total加一次),那麼具體要執行多少次就由for循環中的n決定。也就是說這些語句執行n次的意味着它們的總執行時間T與vector的大小N成正比。也就是T = O(tN),根據O記法,這裏應該省略常數。即該函數的算法複雜度爲O(N),這種複雜度通常稱爲線性時間(linear time)
當然我們還可以通過查看代碼的循環結構來估算算法複雜度。在大多數情況下,單個表達式和語句,除非它們涉及必須單獨考慮的函數調用,否則會在固定的時間裏運行。在計算複雜性方面重要的是這些語句的執行頻率。對於許多程序,可以通過找到最常執行的代碼片段來確定計算複雜度,並確定其作爲N的函數被執行了多少次。
例如在average函數的情況下,循環體是執行n次 因爲代碼的任何部分都不會比這更頻繁地執行,所以我們可以預測計算複雜度將爲O(N)。
這個時候我們可以用同樣的方式對選擇排序函數進行分析。代碼中最常執行的部分是語句中的比較兩個數的大小:

if (vec[i] < vec[rh]) rh = i;

該語句嵌套在兩個for循環中,並且都受N的值的限制。內循環運行的次數跟外循環一樣頻繁,也就意味着內循環中的語句的運行時間爲O(N^2).

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章