演算法
演算法是解決特定問題求解步驟的描述,在計算機中表現為指令的有限序列,並且每條指令表示一個或多個操作
演算法定義中,提到了指令,指令能被人或機器等計算裝置執行。它可以是計算機 指令,也可以是我們平時的語言文字。
為了解決某個或某類問題,需要把指令表示成一定的操作序列,操作序列包括一 組操作,每一個操作都完成特定的功能,這就是演算法了。
演算法的特徵
演算法具有五個基本特性:輸入、輸出、有窮性、確定性和可行性。
輸入輸出
輸入和輸出特性比較容易理解, 演算法具有零個或多個輸入。儘管對於絕大多數算 法來說,輸入引數都是必要的,但對於個別情況,如列印 "hello world! " 這樣的代 碼,不需要任何輸入引數3 因此演算法的輸入可以是零個。 演算法至少有一個或多個輸 出, 演算法是一定需要輸出的,不需要輸出,你用這個演算法幹嗎?輸出的形式可以是打 印輸出,也可以是返回一個或多個值等.
有窮性
有窮性:指演算法在執行有限的步驟之後,自動結束而不會出現無限迴圈,並且每 一個步驟在可接受的時間內完成。現實中經常會寫出死迴圈的程式碼,這就是不滿足有窮性。當然這裡有窮的概念並不是純數學意義的,而是在實際應用當中合理的、可以接受的"有邊界飛你說你寫一個演算法,計算機需要算上個二十年,一定會結束,它在數學意義上是有窮了,可是媳婦都熬成婆了,演算法的意義也不就大了。
確定性
確定性:演算法的每一步驟都具有確定的含義, 不會出現二義性。 演算法在一定條件下,只有一條執行路徑,相同的輸入只能有唯一的輸出結果。演算法的每個步驟被精確定義而無歧義。
可行性
可行性:演算法的每一步都必須是可行的, 也就是說,每一步都能夠通過執行有限次數完成。 可行性意味著演算法可以轉換為程式上機執行,並得到正確的結果。儘管在目前計算機界也存在那種沒有實現的極為複雜的演算法, 不是說理論上不能實現, 而是因為過於複雜,我們當前的編碼方法、工具和大腦限制了這個工作,不過這都是理論研究領域的問題,不屬於我們現在要考慮的範圍。
演算法設計的要求
1.正確性
正確性:演算法的正確性是指演算法至少應該具有輸入、輸出和加工處理無歧義性、 能正確反映問題的需求、能夠得到問題的正確答案。
但是算陸的"正確"通常在用法上有很大的差別,大體分為以下四個層次。
- 演算法程式沒有語法錯誤。
- 演算法程式對於合法的輸入資料能夠產生滿足要求的輸出結果。
- 演算法程式對於非法的輸入資料能夠得出滿足規格說明的結果。
- 演算法程式對於精心選擇的,甚至刁難的測試資料都有滿足要求的輸出結果。
對於這四層含義,層次 1 要求最低,但是僅僅沒有語法錯誤實在談環上是好算 法。 這就如同僅僅解決溫飽, 不能算是生活幸福一樣。 而層次 4 是最困難的,我們幾 乎不可能逐一驗證所有的輸入都得到正確的結果。
因此演算法的正確性在大部分情況下都不可能用程式來證明,而是用數學方法證明 的。證明一個複雜演算法在所4寄層次上都是正確的,代價非常昂貴。所以一般情況下, 我們把層次 3 作為一個演算法是否正確的標準。 好演算法還有一個特徵:容易理解
2.可讀性
可讀性: 演算法設計的另一目的是為了便於閱讀、 理解和交流。
3.健壯性
健壯性:當輸入資料不合法時,演算法也能做出相關處理, 而不是產生異常或莫名 其妙的結果。
4.時間效率高和儲存量低
好的演算法還應該具備時間效率高和儲存雖低的特點。
時間效率指的是演算法的執行時間 ,對於同一個問題,如果有多個演算法能夠解決,執行時間短的演算法效率高,執行時間長的效率低。儲存量需求指的是演算法在執行過程中需要的最大儲存空間,主要指演算法程式執行時所佔用的記憶體或外部硬碟儲存空間。設計演算法應該儘量滿足時間效率高和儲存量低的需求。
在生活中,人們都希望花最少 的錢,用最短的時間 ,辦最大的事,演算法也是一樣的思想,最好用最少的儲存空間,花最少的時間,辦成同樣的事就是好的演算法。求 100 個人的高考成績平均分 ,與求全省的所有考生的成績平均分在佔用時間和記憶體儲存上是有非常大的差異的 ,我們自然是追求可以高效率和低儲存量的演算法來解決問題。
綜上,好的演算法,應該具有正確性、 可讀性、健壯性、 高效率和低儲存量的特徵。
演算法效率的度量方法
事後統計方法
事後統計方法:這種方法主要是通過設計好的測試程式和資料,利用計算機計時 器對不同演算法編制的程式的執行時間進行比較,從而確定演算法效率的高低。
但這種方法顯然是有很大缺陷的:
- 必須依據演算法事先編制好程式,這通常需要花費大量的時間和精力。 如果 編制出來發現宮根本是很糟糕的演算法,不是竹籃打水一場空嗎?
- 時間的比較依賴計算機硬體和軟體等環境因素,有時會掩蓋演算法本身的優 劣。 要知道,現在的一臺四核處理器的計算機, N~當年 286、 386、 486 等 老爺爺輩的機器相比,在處理演算法的運算速度上,是不能相提並論的 i 而 所用的作業系統、編譯器、 執行框架等軟體的不同,也可以影響官們的結 果;就算是同一臺機器, CPU 使用率和記憶體佔用情況不一樣,也會造成細 微的差異。
- 演算法的測試資料設計困難,並且程式的執行時間往往還與測試資料的規模 有很大關係,效率高的演算法在才、的測試資料面前往往得不到體現。 比如 10 個數字的排序,不管用什麼算沽, 差異幾乎是零。 而如果有一百萬個隨機 數字排序,那不同演算法的差異就非常大了。那麼我們為了比較算沽,到底 用多少資料來測試,這是很難判斷的問題。
基於事後統計方法有這樣那樣的缺陷,我們考慮不予採納。
事前分析估算方法
我們的計算機前輩們,為了對演算法的評判更科學,研究出了一種叫做事前分析估算的方法。
事前分析估算方法:在計算機程式編制前,依據統計方法對演算法進行估算。
經過分析,我們發現,一個用高階程式語言編寫的程式在計算機上執行時所消耗的時間取決於下列因素:
- 演算法採用的策略、方法。
- 編譯產生的程式碼質量。
- 問題的輸入規模。
- 機器執行指令的速度。 第 1 條當然是演算法好壞的根本,第 2 條要由軟體來支援, 第 4 條要看硬體效能。 也就是說, 拋開這些與計算機硬體、軟體有關的因素, 一個程式的執行時間,依賴於演算法的好壞和問題的輸入規模 。 所謂 問題輸入規模是指輸入量的多少 。
我們在分析一個演算法的執行時間時,重要的是把基本操作的敬量與輸入規模關聯 起來, f!P基本操作的數量必須表示成輸入規模的函式。如下圖所示

我們可以這樣認為,隨著 n 值的越來越大,它們在時間效率上的差異也就越來越大。好比你們當中有些人每天都在學習,我指有用的學習,而不是隻為考試的死讀書, 每天都在進步,而另一些人,打打遊戲,睡睡大覺。 人校時大家都一樣, 但畢業時結果可能就大不一樣,前者名企爭搶著耍,後者求職無門。
函式的漸近增長
函式的漸近增長:給定兩個函式 f ( n )和 g ( n ), 如果存在一個整數 N, 使得對於所有的 n > N, f ( n )總是比 g ( n )大,那麼, 我們說 f ( n ) 的增長漸近快於 g ( n )。
與最高次項相乘的常數並不重要。
最高次項的指數大 的,函式隨著 n 的增長,結果也會變得增長特別快。
判斷一個演算法的效率時,函式中的常數和其他次要項常常可以忽略,而更應該關注主項(最高階項) 的階數。
判斷一個演算法好不好,我們只通過少量的資料是不能做出準確判斷的。根據剛才 的幾個樣例 , 我們發現,如果我們可以對比這幾個演算法的關鍵執行次數函式的漸近增 長性,基本就可以分析出:**某個演算法,隨著 n 的增大,它會越來越優於另一演算法,或 者越來越差於另一演算法。**這其實就是事前估算方法的理論依據, 通過演算法時間複雜度來估算演算法時間效率。
演算法時間複雜度
演算法時間複雜度定義
在進行演算法分析時, 語旬總的執行次數 T ( n )是關於問題規模 n 的函式,進而分析 T ( n )隨 n 的變化情況並確定T ( n ) 的數量級。演算法的時間複雜度,也就是演算法的時間量度,記作:T ( n ) = O ( f ( n ) )。它表示隨問題規模n的增大,演算法執行時間的增長率和f ( n ) 的增長率相同。稱作演算法的漸遠時間複雜度,簡稱胃時間複雜度。f ( n ) 是問題規模n的某個函式。
這樣用大寫 O()來體現演算法時間複雜度的記法,我們稱之為大 0 記法。 一般情況下,隨著 n 的增大, T(n)增長最慢的演算法為最優演算法。
顯然,由此演算法時間複雜度的定義可知,我們的三個求和演算法的時間複雜度分別 為 O(n) , O(1), O(n²)。我們分別給官們取了非官方的名稱, 0(1)叫常數階、 O(n)叫線 性階、 O(n²)叫平方階,當然,還有其他的一些階,我們之後會介紹。
推導大 O 階方法
如何分析一個演算法的時間複雜度呢?即如何推導大 0 階呢?
推導大 O 階:
- 用常鼓 1 取代執行時閨中的所有加法常顫。
- 在修改後的執行次搬畫鍾中,只保留最高階項。
- 如果最高階項存在且不是 1 ,則去除與這個項相乘的常敢。
得到的結果就是大 O 階。
常數階
首先順序結構的時間複雜度。下面這個演算法,也就是剛才的第二種演算法(高斯演算法) .為什麼時間複雜度不是 O(3) .而是O(1)。
ìnt sum = O, n =100;/*執行一次 */ sum = (1+n)*n/2;/*執行一次 */ prìntf ( "%d" , sum) ; /*執行一次 */ 複製程式碼
這個演算法的執行次數函式是 f (n) =3。 根據我們推導大 0 階的方法,第一步就是 把常數項 3 改為 1。在保留最高階項時發現,它根本沒有最高階項,所以這個演算法的時間複雜度為 0(1)。
另外,我們試想一下,如果這個演算法當中的語句 sum= ( 1+0) 句12 有 10 旬, 即:
int sum = 0, 0 = 100; / *執行 1 次*/ sum = (1+n)*0/2; /*執行第 1 次*/ sum = (1+n)*0/2; /*執行第 2 次*/ sum = (1+n)*0/2; /*執行第 3 次*/ sum = (1+n)*0/2; /*執行第 4 次*/ sum = (1+n)*0/2; /*執行第 5 次*/ sum = (1+n)*0/2; /*執行第 6 次*/ sum = (1+n)*0/2; /*執行第 7 次*/ sum = (1+n)*0/2; /*執行第 8 次*/ sum = (1+n)*0/2; /*執行第 9 次*/ sum = (1+n)*0/2; /*執行第 10 次*/ printf ("%d",sum); /*執行 1 次*/ 複製程式碼
事實上無論 n 為多少,上面的兩段程式碼就是 3 次和 12 次執行的差異。這種與問 題的大小無關 (n 的多少) ,執行時間恆定的演算法,我們稱之為具有 0(1)的時間複雜 度,又叫常數階。
注意: 不管這個常數是多少,我們都記作 O(1),而不能是 0(3)、 0(12)等其他任何數字,這是初學者常常犯的錯誤。 對於分支結構而言,無論是真,還是假,執行的次數都是恆定的,不會隨著 n 的 變大而發生變化,所以單純的分支結構(不包含在迴圈結構中) ,其時間複雜度也是0(1)。
線性階
線性階的迴圈結構會複雜很多。要確定某個演算法的階次,我們常常需要確定某個 特定語句或某個語句集執行的次數。因此,我們要分析演算法的複雜度,關鍵就是要分 析迴圈結構的執行情況。
下面這段程式碼,它的迴圈的時間複雜度為 O(n) , 因為迴圈體中的程式碼須要執行 n次。
int i; for (i = 0; i < n; i++) { /* 時間複雜度為O(1)的程式步驟序列 */ } 複製程式碼
對數階
int count = 1; while (count < n) { count = count * 2; /* 時間複雜皮為 O(1) 的程式步驟序列 */ } 複製程式碼
由於每次 count 乘以 2 之後,就距離 n 更近了一分。 也就是說,有多少個2相乘後大於 n ,則會退出迴圈。 由 2x=n 得到 x=log2n。 所以這個迴圈的時間複雜度為 o (logn)。
平方階
下面例子是一個迴圈巢狀,它的內迴圈剛才我們已經分析過,時間複雜度為 O(n)。
i nt i ,j ; for (i = 0; i < n; i++) { for ( j = 0 ; j < n ; j++) { /*時間複雜度為O(1)的程式步驟序列*/ } } 複製程式碼
而對於外層的迴圈,不過是內部這個時間複雜度為 O(n)的語旬,再迴圈 n 次。 所以這段程式碼的時間複雜度為 O(n²).
如果外迴圈的迴圈次數改為了m ,時間複雜度就變為 O(mXn)。
i nt i ,j ; for (i = 0; i < m; i++) { for ( j = 0 ; j < n ; j++) { /*時間複雜度為O(1)的程式步驟序列*/ } } 複製程式碼
所以我們可以總結得出,迴圈的時間複雜度等於迴圈體的複雜度乘以該迴圈執行的次數。
那麼下面這個迴圈巢狀,它的時間複雜度是多少呢?
i nt i ,j ; for (i = 0; i < m; i++) { for ( j = 0 ; j < n ; j++) { /*時間複雜度為O(1)的程式步驟序列*/ } } 複製程式碼
由於當 i= 0 時,內迴圈執行了 n 次,當 i = 1 時,執行了 n-1 次,……當 i=n一1 時,執行了 1 次。所以總的執行次數為:

用我們推導大 0 階的方法,第一條,沒有加法常數不予考慮j 第二條,只保留最 高階項,因此保留時/2; 第三條,去除這個項相乘的常數,也就是去除 1/2 ,最終這 段程式碼的時間複雜度為 O(n2)。
常見的時間複雜度
常見的時問複雜度如表所示

常用的時間複雜度所耗費的時間從小到大依次是:


最壞情況與平均情況
我們查詢一個有n 個隨機數字陣列中的某個數字, 最好的情況是第一個數字就是,那麼演算法的時間複雜度為 0(1) ,但也有可能這個數字就在最後一個位置上待著,那麼演算法的時間複雜度就是 O(n),這是最壞的一種情況了。
最壞情況執行時間是一種保證,那就是執行時間將不會再壞了。 在應用中,這是 一種最重要的需求, 通常, 除非特別指定, 我們提到的執行時間都是最壞情況的執行 時間。
而平均執行時間也就是從概率的角度看 , 這個數字在每一個位置的可能性是相同的,所以平均的查詢時間為n/2 次後發現這個目標元素。 **平均執行時闖是所有情況中最有意義的,因為它是期望的執行時間。**也就是說, 我們執行一段程式程式碼時,是希望看到平均執行時間的。可現實中 ,平均執行時間很難通過分析得到,一般都是通過執行一定數量的實驗資料後估算出來的。 對演算法的分析,一種方法是計算所有情況的平均值,這種時間複雜度的計算方法稱為平均時間複雜度。 另一種方法是計算最壞情況下的時間複雜度,這種方法稱為最壞時間複雜度。 一般在沒有特殊說明的情況下,都是指最壞時間複雜度。
演算法空間複雜度
演算法的空間複雜度通過計算演算法所需的儲存空間實現,演算法空間複雜度的計算公式記作: S(n)= O(f(n)),其中, n 為問題的規模, f(n)為語句關於 n 所佔儲存空間的函式。
一般情況下, 一個程式在機器上執行時,除了需要儲存程式本身的指令、常數、 變數和輸入資料外,還需要儲存對資料操作的儲存單元,若輸入資料所佔空間只取決 於問題本身,和演算法無關,這樣只需要分析該演算法在實現時所需的輔助單元即可。若 演算法執行時所簾的輔助空間相對於輸入資料量而言是個常數,則稱此演算法為原地工 作,空間複雜度為 0(1)。
通常, 我們都使用"時間複雜度"來指執行時間的需求,使用"空間複雜度"指 空間需求。當不用限定詞地使用"複雜度'時,通常都是指時間複雜度。顯然我們這 本書重點要講的還是演算法的時間複雜度的問題。
總結回顧
- 演算法的定義:演算法是解決特定問題求解步驟的描述,在計算機中為指令的有限序 列,並且每條指令表示一個或多個操作。
- 演算法的特性: 有窮性、確定性、可行性、輸入、輸出。
- 演算法的設計的要求: 正確性、可讀性、健壯性、 高效率和低儲存量需求。
- 演算法特性與演算法設計容易混,需要對比記憶。
- 演算法的度量方法: 事後統計方法(不科學、不準確)、 事前分析估算方法。
在講解如何用事前分析估算方法之前,我們先給出了函式潮近增長的定義。
函式的漸近增長:給定兩個函式 f(n)和 g(n),如果存在一個整數 N, 使得對於所有的 n > N,f(n)總是比 g(n)大,那麼,我們說 f(n)的增長漸近快於g(n)。於是我們可以得出一個結論,判斷一個演算法好不好,我們只通過少量的資料是不能做出準確判斷的 ,如果我們可以對比演算法的關鍵執行次數函式的漸近增長性,基本就可以分析出 : 某個演算法,隨著 n 的變大,它會越來越優於另一演算法,或者越來越羞於另一演算法。
然後給出了演算法時間複雜度的定義和推導大 0 階的步驟。 推導大 0 階:
- 用常數 1 取代執行時間中的所有加法常數。
- 在修改後的執行次數函式中,只保留最高階項。
- 如果最高階項存在且不是 1 ,則去除與這個項相乘的常數。 得到的結果就是大 0 階。
通過這個步驟,我們可以在得到演算法的執行次數表示式後,很快得到宮的時間復 雜度,即大 0 階。同時我也提醒了大家,其實推導大 0 階很容易,但如何得到執行次數的表示式卻是需要數學功底的。
接著我們給出了常見的時間複雜度所耗時闊的大小排列:

最後,我們給出了關於演算法最壞情況和平均情況的概念,以及空間複雜度的概念。
感謝你花時間讀到結尾!:D
後端一枚,默默搬磚擼程式碼,如果覺得不錯歡迎關注我的公眾號
