前言
在算法性能上我們常常面臨的挑戰是我們的程序能否求解實際中的大型輸入:
--爲什麼程序運行的慢?
--爲什麼程序耗盡了內存?
沒有理解算法的性能特徵會導致客戶端的性能很差,爲了避免這種情況的出線,需要具備算法分析的一些知識。
此篇主要涉及一些基礎數學知識和科學方法,以及如何在實踐應用中使用這些方法理解算法的性能。我們的重點放在獲得性能的預測上。
主要分爲5部分:
- 觀察特點 (observations)
- 數學模型 (mathematical models)
- 增長階數分類 (order-of-growth classifications)
- 算法理論 (theory of algorithms)
- 內存使用 (memory)
我們將從多種不同的角色思考這些問題:
- 程序員:解決一個問題,讓算法能夠工作,並部署它
- 用戶:完成某項工作,但不關心程序做了什麼
- 理論家:想要理解發生的事情
- 團隊:可能需要完成以上角色的所有工作
關於算法分析需要集中考慮的關鍵是運行時間。運行時間也可以理解爲完成一項計算我們需要進行多少次操作。
這裏主要關心:
- 預測算法的性能
- 比較完成同一任務不同算法的性能
- 在最壞情況下算法性能的底線
- 理解算法如何運行的一些理論基礎
算法分析的科學方法概述:
- 從自然界中觀察某些特徵(程序在計算機上的運行時間)
- 提出假設模型(與觀察到的現象相一致的模型)
- 預測(利用上邊的假設做出合理的預測,一般用來預測更大問題規模,或者另一臺計算機上的運行時間)
- 驗證(作更多的觀察來驗證我們的預測)
- 證實(重複地驗證直到證實我們的模型和觀察的特徵吻合,證實我們的模型假設是正確的)
使用科學方法有一些基本原則:
- 別人做同樣的實驗也會得到相同的結果
- 假設必須具備某個特殊性質:可證僞性
(可證僞性:指從一個理論推導出來的結論(解釋、預見)在邏輯上或原則上要有與一個或一組觀察陳述與之發生衝突或牴觸的可能。
可證僞,不等於已經被證僞;可證僞,不等於是錯的。)
觀察
第一步是要觀察算法的性能特點,這裏就是要觀察程序的運行時間。
給程序計時的方法:
- 看錶(你沒看錯,簡單粗暴)
- 利用API:很多第三方或者Java標準庫中有一個秒錶類,可以計算用掉的時間
(如apache commons lang,springframework的工具包都有,這裏使用stdlib庫中的Stopwatch API 進行時間監控)
我們將使用 3-SUM 問題作爲觀察的例子。
3-SUM問題描述
三數之和。如果有N個不同的整數,以3個整數劃爲一組,有多少組整數只和爲0.
如下圖,8ints.txt 有8個整數,有四組整數和爲0
目標是編寫一個程序,能對任意輸入計算出3-SUM整數和爲0有多少組。
這個程序實現的算法也很簡單,首先是第一種,“暴力算法”
"暴力算法"
EN:brute-force algorithm
這裏使用第三方API的方法測量程序運行的時間。
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Stopwatch;
public class ThreeSum {
public static int count(int[] a) {
int N = a.length;
int count = 0;
//三重的for循環,檢查每三個整數組合
for (int i = 0; i < N; i++)
for (int j = i + 1; j < N; j++)
for (int k = j + 1; k < N; k++)
//爲了方便觀察算法的性能問題,這裏忽略了整型溢出問題的處理
if (a[i] + a[j] + a[k] == 0)
count++;
return count;
}
/**
* 讀入所有整數,輸出count的值
* 利用StopWatch執行時間監控
* @param args
*/
public static void main(String[] args) {
int[] a = StdIn.readAllInts();
Stopwatch stopwatch = new Stopwatch();
StdOut.println(ThreeSum.count(a));
double time = stopwatch.elapsedTime();
}
}
實證分析
測試數據可以用越來越大的輸入來運行。每次將輸入的大小翻倍,程序會運行得更久。通過類似的測試,有時能相當方便和快速地評估程序什麼時候結束。
數據分析
通過實證得出的數據,可以建立圖像,使觀察跟直觀:
- 標準座標 :Y軸:運行時間;X軸:輸入大小
- 雙對數座標:Y軸:取運行時間的對數;X軸:取問題輸入大小的對數
(lg以2爲底)
使用雙對數座標通常情況下是得到一條直線,這條直線的斜率就是問題的關鍵。
這個例子(3-SUM 暴力算法)的斜率是3
--通過對數座標的方法得出公式:lg(T(N)) = blgN + c (可看做 y = b*x + c,其中 y = lg(T(N)),x = lgN)
--通過圖中兩點可求出b,c值,如果等式兩邊同時取2的冪,就得到 T(N) = 2c*N^b, 其中 2c 爲一個常數,可記作 a
由此,從這個模型的觀察中我們就得到了程序的運行時間,通過一些數學計算(在這裏是迴歸運算),我們就知道得出了運行時間:
T(N) = a*N^b (b爲雙對數座標中直線的斜率,同時 b 也是這個算法的增長量級,第三點會講到)
預測和驗證
假設
通過上述的數據分析,我們得出假設:
運行時間看起來大約是 1.006 × 10^–10 × N^2.999 (秒)
預測
可以運用這個假設繼續做預測,只要帶入不同的N值,就能計算出需要的大致時間。
・51.0 seconds for N = 8,000.
・408.1 seconds for N = 16,000.
驗證
通過對比 程序實際運行時間(下圖) 和 通過我們的假設模型預測的時間上一步) 可以看出結果非常相近
這個模型幫助我們在不需要花時間運行試驗的前提下做一些預測。實際上這個情形中存在冪定律(a*N^b).實際上絕大多數的計算機算法的運行時間滿足冪定律。
下邊介紹一種求解符合冪定律運行時間中的增長量級值(b)的方法
Doubling hypothesis 方法
- 計算b值
這裏可以通過 Doubling hypothesis 的方法可以快速地估算出冪定律關係中的 b 值:
運行程序,將每次輸入的大小翻倍(doubling size of the input),然後計算出N和2N運行時間的比率。主要看下圖的後幾行運算時間比率,前幾行的輸入值小,以現在的計算機運算能力處理起來,立方級別的增量級運算速度快也相差無幾。
ratio ≈ T(2N)/T(N).
至於爲什麼 0.8/0.1≈7.7 或其他看起來 "運算錯誤" 類似的情況,是因爲圖上的運行時間的記錄是簡化精確到了小數點後一位,實際運算比率值是使用了實際運行時間(精確到小數點後幾位)去計算的,所以會出現0.8/0.1≈7.7。
通過不斷地進行雙倍輸入實驗,可以看到比率會收斂到一個常數(這裏爲8),而實際上比率的對數會收斂到N的指數,也就是 b 的值,這裏粗暴算法的 b 值就等於3
通過Doubling hypothesis方法我們又能提出假設:
此算法的運行時間大約是 a*N^b, 其中 b = lg ratio
注意:Doubling hypothesis 不適用於識別對數因子
- 計算a值
得出 b 的值後,在某個大的輸入值上運行程序,就能求出 a 值。
由此得出假設:運行時間 ≈ 0.998 × 10^–10 × N^3 (秒)
我們通過作圖得出的模型( ≈ 1.006 × 10^–10 × N^2.999 )和我們通過Doubling hypothesis方法得出的模型是很接近的。
計算機中有很多的因素也會影響運行時間,但是關鍵因素一般和計算機的型號無關。
影響因素
關鍵的因素即爲你使用的算法和數據. 決定冪定律中的 b 值
還有很多與系統相關的因素:
- 硬件配置:CPU,內存,緩存...
- 軟件環境:編譯器,解析器,垃圾回收器...
- 計算機的系統:操作系統,網絡,其它應用...
以上所有因素,包括關鍵因素,都決定了冪定律中的 a 值
現代計算機系統中硬件和軟件是非常複雜的,有時很難獲得非常精確的測量,但是另一方面我們不需要像其他科學中需要犧牲動物或者向一顆行星發射探測器這些複雜地方法,我們只需要進行大量的實驗,就能理解和得到我們想要知道的影響因子(的值)。
數學模型
待更新。。。