演算法妙應用-演算法的複雜度

Wizey發表於2018-11-28

演算法詞雲.png

0、什麼是演算法的複雜度?

對於任何一個程式來說,都可以從三個方面進行分析,分別是 輸入處理輸出,也即 IPO(Input、Process、Output),這種分析方法對硬體和軟體程式都是適用的。

資料的來源(Input):可以是硬體感測器收集的,也可以是從網上爬取的...。資料的輸出(Output):可以顯示在網頁上,安卓APP上,電子螢幕上...。而最重要的是程式處理,可以對資料進行簡單的處理,也可以對資料進行挖掘...。

就拿最簡單的 Hello World 程式來說,也是由這三方面組成的,輸出函式(處理)幫你處理輸入的 “Hello World” 字串(輸入),然後再幫你將這些字元輸出顯示到控制檯上(輸出)。大到現在熱門的技術,物聯網、大資料,人工智慧等,做的也無非都是上面三個方面內的事情,關於這些,讀者可以思考一下。

評價一個程式的複雜程度,關鍵也是看程式中處理資料的這部分,對資料處理就要用到演算法了。什麼?你說你沒有用過演算法,其實從你程式設計開始的第一句 “Hello World” 就註定程式設計和演算法分不開了,一個輸出函式背後也同樣有演算法呀,只是你在使用演算法的時候並沒有意識到你在用演算法罷了(嘿嘿,就是這麼神奇~)。

演算法是一個程式的靈魂,就好比沒有蛋的泡麵是沒有靈魂的一樣。一個好的演算法可以有很多的應用,比如在美劇《矽谷》中,主角發明了一種壓縮演算法,將它用在音樂、雲端儲存、視訊等方面都大獲成功。雖然故事是虛構的,但是在一方面也說明了演算法的重要性。

分析一個演算法的複雜度,也是在分析一個演算法的好壞優劣,簡單高效的演算法才是我們應該追求的,而複雜低效的演算法則是我們需要改進的。演算法的複雜度包括 時間複雜度空間複雜度,下面將用盡量少的概念來幫你搞懂這兩個度。

1、什麼是演算法的時間複雜度?

討論演算法的時間複雜度,也是在討論程式使用該演算法執行的時間。經常聽到身邊的同學說我的電腦好慢呀,開啟個軟體都要一分多鐘,急死人了。這從演算法的角度看,只能說電腦硬體比較差,不一定說程式寫的很垃圾(當然也有可能有程式的鍋),同樣的程式可能在別人配置高的電腦上開啟就很快。所以你看單純從程式執行需要的時間長短上,並不能反映演算法的優劣,因為這和執行的裝置也有很大關係(計算機計算主要用到的是 CPU 和 GPU)。

演算法的時間複雜度並不能以具體的時間數值為單位(如1秒鐘,1分鐘等),那演算法複雜度中的時間單位是什麼呢?這個時間單位其實更像是程式中執行的次數或者步驟數。

舉個栗子,當你忘記東西放哪裡了,可能會把所有的抽屜都找一遍,假如你有 n 個抽屜,那麼找完 n 個抽屜就可以找到你的東西了,每個抽屜都找了一遍,就找了 n 遍。演算法的時間複雜度(執行時間)用大 O 表示(不需要關心大 O 表示法怎麼來的,就是個名字),把你找東西的這個過程寫成程式,演算法的時間複雜度就是 O(n),是不是感覺演算法其實就在我們中間。

在上面這個例子中,最好的情況是,當你找完第一個抽屜,你就找到你的東西了,這當然是最好的了,用大 O 表示法表示就是 O(1),但是這樣的情況存在偶然性,並不能代表演算法的複雜度;最壞的情況是,直到你找完最後一個抽屜,累的要死,你才找到你的東西,用大 O 表示法表示就是 O(n)。位於最壞和最好之間的情況是,當你找到中間一個抽屜時,你找到的你的東西了,用大 O 表示法表示就是 O(n/2)。

那麼這三種情況,哪一種應該代表演算法的時間複雜度呢?最好的情況畢竟是小概率事件,不具有普適性,肯定是不能代表演算法真實的時間複雜度。平均的情況,確實在一定程度上可以反映出演算法的時間複雜度,但是學過數學的我們知道,平均值容易受到極端值的影響(在評委打分時也經常是去掉最高分和最低分),所以平均情況也不是很合適。而最壞的情況卻可以給我們一種保證,我們心裡也可以有一個預期,這個演算法在最差的情況下表現如何(就像我們做事也常常考慮最壞的情況一樣),所以我們用最壞情況下的時間複雜度來衡量演算法的時間複雜度

對於大 O 括號內的引數(或者稱為運算元)的係數,往往被我們主動忽略,如一個計算出所需次數為 n/2 的演算法,用大 O 表示法表示是 O(n),而對於計算次數是個常數(如 1,5, 9)的演算法,用大 O 表示法表示都是 O(1),這點是需要我們注意一下的。為什麼這樣做呢?因為對於計算次數是 n2/2 + n/2 + 5 這樣的演算法,起決定作用的是 n,而不是 n 前面的係數,當 n 為無窮大時,n 前面係數的影響就微不足道了,最終這個演算法的時間複雜度用大 O 表示法為 O(n2 + n)。

2、什麼是演算法的空間複雜度?

程式執行時肯定是要消耗空間資源的,暫存器、記憶體和磁碟等。輸入和輸出這兩部分佔用空間是必需的,所以程式處理的空間指的是程式執行演算法時所需的那部分空間。先來看個例子,交換兩個數的值,相信大家都做過吧,一般的方法是找一箇中間變數儲存其中一個數的值,再讓一個數等於另一個數的值,另外一個數等於中間變數的值,就像下面的虛擬碼這樣。

// 交換 a 和 b
temp = a
a = b
b = temp
複製程式碼

棧這種資料結構,我都應該很熟悉,特點是先進後出(FILO),交換的這兩個數肯定都是要放在棧中的,但由於引入了中間變數實現,所以在程式棧中還要有中間變數的空間,一個變數佔用一塊棧空間(想象一下),我們用一個格子來表示,就像下面這樣,中間變數也要佔用一個格子(其實這個格子在其他棧中叫做 ,如 Java虛擬機器的本地方法棧和虛擬機器棧,幀又是一種資料結構)。

因為這個值交換演算法用到了中間變數,而中間變數又要佔用一個格子,所以這個演算法的空間複雜度用大 O 表示法表示就是 O(1)。

演算法複雜度.png

相比較而言,演算法的空間複雜度比較簡單,所以我們在討論一個演算法時,更多的是討論演算法的時間複雜度。

3、一些常見的大 O 執行時間

  • O(1),如 y = x + 1,只需要一次計算便可得到結果。
  • O(logn),也稱為對數時間,如二分查詢演算法。
  • O(n),也稱為線性時間,如我們上面例子中的找東西,用的就是簡單查詢演算法。
  • O(n*logn),如快速排序演算法。
  • O(n2),如選擇排序演算法。
  • O(n!),如著名的旅行商問題。

這裡提到的演算法,將在後面的文章中討論,感興趣的小夥伴不妨先搜尋瞭解一下。

上面幾種大 O 執行時間,反應在圖中如下(注意:圖中曲線並不一定從原點開始畫的,只需要知道演算法執行時間的大概走勢就可以了):

幾種常見的演算法時間.jpg

演算法的速度,指的並不是時間,而是增速,反應的在圖中就是曲線的斜率,可以看到,隨著輸入的增加,有的演算法所需要的時間越來長,也就是使用這種演算法的程式會越來越慢。

4、小結

演算法的複雜度和需要的時間、空間都有關係,我們更多談論的是演算法的時間複雜度,演算法的時間複雜度不是以秒為單位演算法執行的速度是從其增速的角度度量的,也即是輸入越多,演算法執行的時間改變的快慢。一個好的演算法應該是時間複雜度和空間複雜度都比較低,通俗的說就是花最少的時間和精力達到最好的效果,但是這兩樣往往是很難同時做到的,這就需要我們犧牲一樣來做到儘可能的更好。

——本文轉自我的微信公眾號《程式設計心路》。

相關文章