演算法妙應用-演算法的複雜度
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)。
相比較而言,演算法的空間複雜度比較簡單,所以我們在討論一個演算法時,更多的是討論演算法的時間複雜度。
3、一些常見的大 O 執行時間
- O(1),如 y = x + 1,只需要一次計算便可得到結果。
- O(logn),也稱為對數時間,如二分查詢演算法。
- O(n),也稱為線性時間,如我們上面例子中的找東西,用的就是簡單查詢演算法。
- O(n*logn),如快速排序演算法。
- O(n2),如選擇排序演算法。
- O(n!),如著名的旅行商問題。
這裡提到的演算法,將在後面的文章中討論,感興趣的小夥伴不妨先搜尋瞭解一下。
上面幾種大 O 執行時間,反應在圖中如下(注意:圖中曲線並不一定從原點開始畫的,只需要知道演算法執行時間的大概走勢就可以了):
演算法的速度,指的並不是時間,而是增速,反應的在圖中就是曲線的斜率,可以看到,隨著輸入的增加,有的演算法所需要的時間越來長,也就是使用這種演算法的程式會越來越慢。
4、小結
演算法的複雜度和需要的時間、空間都有關係,我們更多談論的是演算法的時間複雜度,演算法的時間複雜度不是以秒為單位,演算法執行的速度是從其增速的角度度量的,也即是輸入越多,演算法執行的時間改變的快慢。一個好的演算法應該是時間複雜度和空間複雜度都比較低,通俗的說就是花最少的時間和精力達到最好的效果,但是這兩樣往往是很難同時做到的,這就需要我們犧牲一樣來做到儘可能的更好。
——本文轉自我的微信公眾號《程式設計心路》。