版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2017.08.11 |
前言
將數(shù)據(jù)結(jié)構(gòu)和算法比作計(jì)算機(jī)的基石毫不為過,追求程序的高效是每一個(gè)軟件工程師的夢(mèng)想。下面就是我對(duì)算法方面的基礎(chǔ)知識(shí)理論與實(shí)踐的總結(jié)。感興趣的可以看上面幾篇。
1. 算法簡單學(xué)習(xí)(一)—— 前言
2. 算法簡單學(xué)習(xí)(二)—— 一個(gè)簡單的插入排序
算法設(shè)計(jì)
算法設(shè)計(jì)方法有很多,第二篇說的插入排序使用的是增量(incremental)
方法,在排好子數(shù)組A[1 ... j - 1]
后,將元素A[j]
插入,形成排好序的子數(shù)組A[1 ... j]
。
下面介紹另外一種算法的設(shè)計(jì)策略,分治法(divide - and - conquer)
。
1. 分治法
很多算法在結(jié)構(gòu)上是遞歸的:為解決一個(gè)給定的問題,算法要一次或多次地遞歸調(diào)用其自身來解決相關(guān)的子問題。這些算法通常采用分治策略,將原問題分成n
個(gè)規(guī)模較小而結(jié)構(gòu)與原問題相似的子問題;遞歸地解決這些子問題,然后再合并其結(jié)果,就得到原問題的解。
分治模式在每一層遞歸上都有三個(gè)步驟:
- 分解
Divide
:將原問題分解成一系列子問題。 - 解決
Conquer
:遞歸的解各子問題,若子問題足夠小,則直接求解。 - 合并
Combine
:將子問題的結(jié)果合并成原問題的解。
2. 合并排序
合并排序(merge sort)
算法完全按照上面的模式。
- 分解:將
n
個(gè)元素分別各含有n / 2
個(gè)元素的子序列。 - 解決:用合并排序法對(duì)兩個(gè)子序列遞歸地排序。
- 合并:合并兩個(gè)已排序的子序列以得到排序結(jié)果。
在對(duì)子序列排序時(shí),其長度為1時(shí)遞歸結(jié)束,單個(gè)元素被視為是已排好序的。
合并排序的關(guān)鍵步驟在于合并步驟中的合并兩個(gè)已排好序的序列,叫做合并。引入一輔助過程MERGE(A, p, q, r)
,其中A是一個(gè)數(shù)組,p, q, r
是下標(biāo),且滿足p ≤ q ≤ r
,這里還假設(shè)A[p, q]
和A[q + 1, r]
都已排好序,并將它們合并成一個(gè)已排好序的子數(shù)組代替當(dāng)前的子數(shù)組A[p ... r]
。
MERGE
的過程時(shí)間代價(jià)為O(n)
,其中n = r - p + 1
是待合并的元素個(gè)數(shù)。
算法原理
這里就舉一個(gè)撲克牌的例子,假設(shè)有兩堆牌面朝上放在桌子上,每一堆都是已排序的,最小的牌放在做上面,我們要做的就是將這兩堆牌合并成一個(gè)排好序的輸出堆,面朝下的放在桌子上,基本步驟就是包括在面朝上的兩堆牌中,選取頂上兩張中較小的一張,將其取出后面朝下的放在輸出堆中,重復(fù)這個(gè)步驟,直到其中的一個(gè)輸入堆中為空停止,這時(shí)把輸入堆中余下的牌面朝下的放入輸出堆中即可。我們只是查看并比較頂上的兩張牌,至多進(jìn)行n次的比較,合并排序時(shí)間為O(n)
。
偽代碼
在給出偽代碼之前,需要做一個(gè)小的改動(dòng),按照上面的原理我們需要時(shí)刻監(jiān)測(cè)幾個(gè)輸入堆中是否有一堆是空的,為了避免這種循環(huán)的檢查,我們換一個(gè)思路,在每一堆的底部放上一張哨兵牌(sentinel card)
。它包含了一個(gè)特殊的值,用∞
作為哨兵值,哨兵值漏出來時(shí),不可能是兩張中的最小的值,直到另外一堆也出現(xiàn)了哨兵牌,一旦出現(xiàn)這種兩張哨兵牌同時(shí)出現(xiàn),說明兩堆牌中哨兵牌以外的牌都已經(jīng)完成了排序。執(zhí)行步驟次數(shù)r - p + 1
后,算法就可以停止了。
下面看一下合并排序的偽代碼。
下面我們就詳細(xì)的說明下其過程:
- 第 1 行 計(jì)算數(shù)組
A[p ... q]
的長度n1
。 - 第 2 行計(jì)算數(shù)組
A[q + 1 ... r]
的長度n2
。 - 第 3 行創(chuàng)建數(shù)組
L
和R
,長度分別為n1 + 1
,n2 + 1
。 - 第4 ~ 5行,利用for循環(huán)將數(shù)組
A[p ... q]
復(fù)制到L[1 ... n1]
中去。 - 第 6 ~ 7 行,利用for循環(huán)將數(shù)組
A[p + 1 ... r]
復(fù)制到L[1 ... n2]
中去。 - 第 8 ~ 9 將哨兵元素至于L 和 R數(shù)組的末尾。
- 第 10 ~ 17 行維護(hù)一個(gè)循環(huán)不變式,執(zhí)行
r - p + 1
個(gè)基本步驟。
下面我們看一下上面算法的時(shí)間復(fù)雜度,這里1 ~ 3
行和 8 ~ 11
行都是固定的,也就是說時(shí)間都是常量,不會(huì)因?yàn)閿?shù)組元素的個(gè)數(shù)增加而增加。第 4 ~ 7
行中的for循環(huán)所需要的時(shí)間為O(n1 + n2)= O(n)
,并且第12 ~ 17
行for循環(huán)共有n輪迭代,每一輪迭代所需時(shí)間都是常量。
下面我們就以一個(gè)簡單的例子看一下合并排序的步驟,先看下面兩張圖。
這里我們調(diào)用MERGE(A, 9, 12, 16)
的第10 ~ 17
行的操作中,當(dāng)子數(shù)組A[9 ... 16]
中包含序列{2, 4, 5, 7, 1, 2, 3,6}
時(shí)的情況,在復(fù)制和插入哨兵后,數(shù)組L包含了{2, 4,5,7,∞}
,數(shù)組R包含了{1,2,3,6,∞}
,A中的陰影位置包含了它們的最終值,A中的淺陰影位置包含了它們的最終值。
循環(huán)不變式的驗(yàn)證
下面我們就驗(yàn)證12 ~ 17
行循環(huán)不變式。在第 12 ~ 17
行中for循環(huán)每一輪迭代開始,子數(shù)組A[p ... k - 1]
包含了L[1, n1 + 1]
和R[1, n2 + 1]
中的 k - p
個(gè)最小的元素,并且是排好序的。
初始化:在for循環(huán)開始的時(shí)候,有
k = p
,因而子數(shù)組A[p ... k - 1]
是空的,這時(shí)i = j = 1
,則L[i]
和R[j]
都是各自所在數(shù)組中,尚未被復(fù)制回?cái)?shù)組A中的最小元素。保持:為了說明每一輪迭代都能使循環(huán)不變式成立,首先假設(shè)
L[i] ≤ R[j]
,那么L[i]就是未被復(fù)制回?cái)?shù)組A中的最小元素,由于A[p ... k - 1]
包含了k - p
個(gè)最小的元素,因此,第14行將A[i]復(fù)制到A[k]后,子數(shù)組A[p ... k]將包含k - p + 1
個(gè)最小的元素,增加k和i的值,會(huì)為下一輪迭代重新建立循環(huán)不變式的值,如果L[i] ≥ R[j]
,那么就會(huì)執(zhí)行16 ~ 17
行的代碼,同樣保持循環(huán)不變式成立。終止:在終止時(shí),
k = r + 1
,根據(jù)循環(huán)不變式,子數(shù)組A[p ... k - 1]
(此時(shí)即為A[p ... r]
)包含了L[1 ... n1 + 1]
和R[1 ... n2 + 1]
中k - p = r - p + 1
個(gè)最小元素,已排好序。除了最大的兩個(gè)哨兵元素,其他的元素都已經(jīng)復(fù)制到數(shù)組A中。
詳細(xì)分解過程
下面的過程MERGE - SORT(A, p, r)
對(duì)子數(shù)組A[p ... r]
進(jìn)行排序,如果 p ≥ r
,則子數(shù)組中至多只有一個(gè)元素,否則,分解步驟就計(jì)算出一個(gè)下標(biāo)q,將A[p ... r]
分成A[p ... q]
和A[ q + 1 ... r]
,各自包含n/2個(gè)元素。抽象過程如下所示。
下面看一下數(shù)組A = {5, 2, 4, 7, 1, 3 ,2, 6}
的合并排序過程。
分治法分析
當(dāng)一個(gè)算法中含有對(duì)其自身的遞歸調(diào)用的時(shí)候,其運(yùn)行時(shí)間可以用一個(gè)遞歸方程來表示,分治法遞歸式是基于基本模式中的三個(gè)步驟的。設(shè)T(n)
為一個(gè)規(guī)模為n
的問題的運(yùn)行時(shí)間,如果規(guī)模很小,如n ≤ c
, c為一常量,則得到直接解的時(shí)間為常量,寫作O(1)
,假設(shè)我們把原問題分成a
個(gè)子問題,每一個(gè)大小時(shí)原問題的1/b
,如果分解該問題和合并解的時(shí)間各為D(n)
和C(n)
,則有如下遞歸式。
合并算法時(shí)間消耗分析
有了上面時(shí)間消耗的表達(dá)式,我們其實(shí)可以根據(jù)主定理 (master theorem)
,可以證明T(n)= O(lgn)
,這里lgn就是以2為底的對(duì)數(shù),這個(gè)增長速度要比線性增長的慢,因此,當(dāng)n足夠大的時(shí)候,合并排序最壞情況要比插入排序O(n^2)
好很多。
時(shí)間消耗表達(dá)式可以簡化為:
下面看一下上面時(shí)間消耗表達(dá)式所表示的遞歸樹。
由上可知,總的表達(dá)式就是cnlgn + cn
,也就是O(nlgn)
。
代碼驗(yàn)證
下面我們就看一下代碼。
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc, const char * argv[])
{
int A[8] = {2, 4, 5, 7, 1, 2, 3, 6};
int n1 = 4;
int n2 = 4;
//哨兵
int c = UINT8_MAX;
//L和R兩個(gè)數(shù)組
int L[5] = {0};
int R[5] = {0};
for (int i = 0; i < n1; i ++) {
L[i] = A[i];
}
L[4] = c;
for (int i = n1; i < 8; i ++) {
R[i - 4] = A[i];
}
R[4] = c;
int i = 0;
int j = 0;
int k = 0;
for (k = 0; k < 8; k ++) {
if (L[i] <= R[j]) {
A[k] = L[i];
i ++;
}
else {
A[k] = R[j];
j ++;
}
if (L[i] == c && R[j] == c) {
break;
}
}
for (int k = 0; k < 8; k++) {
printf("%d\n",A[k]);
}
}
下面看輸出結(jié)果
1
2
2
3
4
5
6
7
Program ended with exit code: 0
后記
未完,待續(xù)~~