一招搞定算法!程序員的必備技能:時間複雜度與空間複雜度的計算

前言

算法(Algorithm)是指用來操作數據、解決程序問題的一組方法。算法是大廠、外企面試的必備項,也是每個高級程序員的必備技能。針對同一問題,可以有很多種算法來解決,但不同的算法在效率和佔用存儲空間上的區別可能會很大。

那麼,通過什麼指標來衡量算法的優劣呢?其中,上面提到的效率可以用算法的時間複雜度來描述,而所佔用的存儲空間可以用算法的空間複雜度來描述。

時間複雜度:用於評估執行程序所消耗的時間,可以估算出程序對處理器的使用程度。

空間複雜度:用於評估執行程序所佔用的內存空間,可以估算出程序對計算機內存的使用程度。

在實踐中或面試中,我們不僅要能夠寫出具體的算法來,還要了解算法的時間複雜度和空間複雜度,這樣才能夠評估出算法的優劣。當時間複雜度和空間複雜度無法同時滿足時,還需要從中選取一個平衡點。

一個算法通常存在最好、平均、最壞三種情況,我們一般關注的是最壞情況。最壞情況是算法運行時間的上界,對於某些算法來說,最壞情況出現的比較頻繁,也意味着平均情況和最壞情況一樣差。

通常,時間複雜度要比空間複雜度更容易出問題,更多研究的是時間複雜度,面試中如果沒有特殊說明,講的也是時間複雜度。

時間複雜度

要獲得算法的時間複雜度,最直觀的想法是把算法程序運行一遍,自然可以獲得。但實踐中往往受限於測試環境、數據規模等因素,直接測試算法要麼難以實現,要麼誤差較大,而且理論上也沒必要對每個算法都進行一遍測試,只需要找到一種評估指標,獲得算法執行所消耗時間的基本趨勢即可。

時間頻度

通常,一個算法所花費的時間與代碼語句執行的次數成正比,算法執行語句越多,消耗的時間也就越多。我們把一個算法中的語句執行次數稱爲時間頻度,記作 T(n)。

漸進時間複雜度

在時間頻度 T(n) 中,n 代表着問題的規模,當 n 不斷變化時,T(n) 也會不斷地隨之變化。那麼,如果我們想知道 T(n) 隨着 n 變化時會呈現出什麼樣的規律,那麼就需要引入時間複雜度的概念。

一般情況下,算法基本操作的重複執行次數爲問題規模 n 的某個函數,也就是用時間頻度 T(n) 表示。如果存在某個函數 f(n),使得當 n 趨於無窮大時,T(n)/f(n) 的極限值是不爲零的常數,那麼 f(n) 是 T(n) 的同數量級函數,記作 T(n)=O(f(n)),稱 O(f(n)) 爲算法的漸進時間複雜度,簡稱爲時間複雜度。

漸進時間複雜度用大寫 O 表示,所以也稱作大 O 表示法。算法的時間複雜度函數爲:T(n)=O(f(n));

T(n)=O(f(n)) 表示存在一個常數 C,使得在當 n 趨於正無窮時總有 T(n) ≤ C * f(n)。簡單來說,就是 T(n) 在 n 趨於正無窮時最大也就跟 f(n) 差不多大。也就是說當 n 趨於正無窮時 T(n) 的上界是 C * f(n)。其雖然對 f(n) 沒有規定,但是一般都是取儘可能簡單的函數。

常見的時間複雜度有:O(1) 常數型;O(log n) 對數型,O(n) 線性型,O(nlog n) 線性對數型,O(n2) 平方型,O(n3) 立方型,O(nk)k 次方型,O(2n) 指數型。

上圖爲不同類型的函數的增長趨勢圖,隨着問題規模 n 的不斷增大,上述時間複雜度不斷增大,算法的執行效率越低。

常見的算法時間複雜度由小到大依次爲:Ο(1)<Ο(log n)<Ο(n)<Ο(nlog n)<Ο(n2)<Ο(n3)<…<Ο(2^n)<Ο(n!)。

值得留意的是,算法複雜度只是描述算法的增長趨勢,並不能說一個算法一定比另外一個算法高效。這要添加上問題規模 n 的範圍,在一定問題規範範圍之前某一算法比另外一算法高效,而過了一個閾值之後,情況可能就相反了,通過上圖我們可以明顯看到這一點。這也就是爲什麼我們在實踐的過程中得出的結論可能上面算法的排序相反的原因。

如何推導時間複雜度

上面我們瞭解了時間複雜度的基本概念及表達式,那麼實踐中我們怎麼樣才能通過代碼獲得對應的表達式呢?這就涉及到求解算法複雜度。

求解算法複雜度一般分以下幾個步驟:

找出算法中的基本語句:算法中執行次數最多的語句就是基本語句,通常是最內層循環的循環體。

計算基本語句的執行次數的數量級:只需計算基本語句執行次數的數量級,即只要保證函數中的最高次冪正確即可,可以忽略所有低次冪和最高次冪的係數。這樣能夠簡化算法分析,使注意力集中在最重要的一點上:增長率。

用大Ο表示算法的時間性能:將基本語句執行次數的數量級放入大Ο記號中。

其中用大 O 表示法通常有三種規則:

用常數 1 取代運行時間中的所有加法常數;

只保留時間函數中的最高階項;

如果最高階項存在,則省去最高階項前面的係數;

下面通過具體的實例來說明以上的推斷步驟和規則。

時間複雜度實例

常數階 O(1)

無論代碼執行了多少行,只要是沒有循環等複雜結構,那這個代碼的時間複雜度就都是 O(1),如:

inti=1;intj=2;intk=1+2;

上述代碼執行時,單個語句的頻度均爲 1,不會隨着問題規模 n 的變化而變化。因此,算法時間複雜度爲常數階,記作 T(n)=O(1)。這裏我們需要注意的是,即便上述代碼有成千上萬行,只要執行算法的時間不會隨着問題規模 n 的增長而增長,那麼執行時間只不過是一個比較大的常數而已。此類算法的時間複雜度均爲 O(1)。

對數階 O(log n)

先來看對應的示例代碼:

inti =1;// ①while(i <= n) {  i = i *2;// ②}

在上述代碼中,語句①的頻度爲 1,可以忽略不計。

語句②我們可以看到它是以 2 的倍數來逼近 n,每次都乘以 2。如果用公式表示就是 1_2_22…2 <=n,也就是說 2 的 x 次方小於等於 n 時會執行循環體,記作 2^x <= n,於是得出 x<=logn。也就是說上述循環在執行 logn 次之後,便結束了,因此上述代碼的時間複雜度爲 O(log n)。

其實上面代碼的時間複雜度公式如果精確的來講應該是:T(n) = 1 + O(log n),但我們上面已經講到對應的原則,“只保留時間函數中的最高階項”,因此記作 O(log n)。

線性階 O(n)

示例代碼:

intj =0;// ①for(inti =0; i < n; i++) {// ②j = i;// ③j++;// ④}

上述代碼中,語句①的頻度爲 1,②的頻度爲 n,③的頻度爲 n-1,④的頻度爲 n-1,因此整個算法可以用公式 T(n)=1+n+(n-1)+(n-1) 來表示。進而可以推到 T(n)=1+n+(n-1)+(n-1)=3n-1,即 O(n)=3n-1,去掉低次冪和係數即 O(n)=n,因此 T(n)=O(n)。

在上述代碼中 for 循環中的代碼會執行 n 遍,因此它消耗的時間是隨着 n 的變化而成線性變化的,因此這類算法都可以用 O(n) 來表示時間複雜度。

線性對數階 O(nlogN)

示例代碼:

for(intm =1; m < n; m++) {inti =1;// ①while(i <= n) {      i = i *2;// ②}}

線性對數階要對照對數階 O(log n) 來進行理解。上述代碼中 for 循環內部的代碼便是上面講到對數階,只不過在對數階的外面套了一個 n 次的循環,當然,它的時間複雜度就是 n*O(log n) 了,於是記作 O(nlog n)。

平方階 O(n²)

示例代碼:

intk =0;for(inti =0; i < n; i++) {for(intj =0; j < n; j++) {      k++;  }}

平方階可對照線性階來進行理解,我們知道線性階是一層 for 循環,記作 O(n),此時等於又嵌套了一層 for 循環,那麼便是 n * O(n),也就是 O(n * n),即 O(n^2)。

如果將外層循環中的 n 改爲 m,即:

intk =0;for(inti =0; i < m; i++) {for(intj =0; j < n; j++) {      k++;  }}

那麼,對應的時間複雜度便爲:O(m * n)。

同理,立方階 O(n³)、K 次方階 O(n^k),只不過是嵌套了 3 層循環、k 層循環而已。

排序算法對比

上面介紹了各種示例算法的時間複雜度推理過程,對照上面的時間複雜度以及算法效率的大小,來看一下我們常見的針對排序的幾種算法的時間複雜度對比。

空間複雜度

最後,我們再瞭解一下空間複雜度。空間複雜度主要指執行算法所需內存的大小,用於對程序運行過程中所需要的臨時存儲空間的度量,這裏的空間複雜度同樣是預估的。

程序執行除了需要存儲空間、指令、常數、變量和輸入數據外,還包括對數據進行操作的工作單元和存儲計算所需信息的輔助空間。存儲空間通常包括:指令空間(即代碼空間)、數據空間(常量、簡單變量)等所佔的固定部分和動態分配、遞歸棧所需的可變空間。其中可變空間與算法有關。

一個算法所需的存儲空間用 f(n) 表示。S(n)=O(f(n)) 其中 n 爲問題的規模,S(n) 表示空間複雜度。

下面看兩個常見的空間複雜度示例:空間複雜度 O(1) 和 O(n)。

空間複雜度 O(1)

空間複雜度爲 O(1) 的情況的示例代碼與時間複雜度爲 O(1) 的實例代碼一致:

inti=1;intj=2;intk=1+2;

上述代碼中臨時空間並不會隨着 n 的變化而變化,因此空間複雜度爲 O(1)。總結一下就是:如果算法執行所需要的臨時空間不隨着某個變量 n 的大小而變化,此算法空間複雜度爲一個常量,可表示爲 O(1),即 S(n) = O(1)。

空間複雜度 O(n)

示例代碼:

intj =0;int[] m =newint[n];for(inti =1; i <= n; ++i) {  j = i;  j++;}

上述代碼中,只有創建 int 數組分配空間時與 n 的大小有關,而 for 循環內沒有再分配新的空間,因此,對應的空間複雜度爲 S(n) = O(n)。

總結一下

本篇文章給大家講了可以通過時間複雜度和空間複雜度來衡量算法的優劣,同時用具體的實例來講解如何計算不同方法的時間複雜度和空間複雜度。當我們瞭解了這些基本的概念、函數、計算方法、計算規則及算法性能之後,再進行算法的學習便可以輕鬆預估出算法的性能等指標。

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