這一章介紹貫穿全書(shū)的框架,即設(shè)計(jì)算法的大致方法過(guò)程。以插入排序和歸并排序?yàn)槔榻B描述算法的方法——偽代碼,證明正確性并分析運(yùn)行時(shí)間——循環(huán)不變量和效率分析,最后引入分治法。
2.1 插入排序
偽代碼:用最清晰、最簡(jiǎn)潔的表示方法說(shuō)明算法,主要是理順?biāo)悸贰?br>
插入排序:在一個(gè)有序數(shù)據(jù)序列中插入新的數(shù)。思路是從前到后比較查找新數(shù)的正確位置,將此位置以后的數(shù)整體后移一位,插入此數(shù)。
插入排序偽代碼:
C++實(shí)現(xiàn):第一個(gè)參數(shù)為待排序數(shù)組,第二個(gè)為該數(shù)組長(zhǎng)度,下標(biāo)1~ n存數(shù)共n個(gè) 。
void InsertSort(int *a, int n)
{
int i, j, t;
for (j = 2; j < 10; j++ )
{
t = a[j];
i = j - 1;
while(i > 0 && a[i] > t)
{
a[i + 1] = a[i];
i --;
}
a[i + 1] = t;
}
}
循環(huán)不變式證明正確性
對(duì)于循環(huán)不變式必須證明三條性質(zhì):
前兩步類似于數(shù)學(xué)歸納法的基本情況和歸納步,像多米諾骨牌一樣。第三條通常是和導(dǎo)致循環(huán)條件終止的條件一起使用循環(huán)不變式。與數(shù)學(xué)歸納法不同,循環(huán)終止時(shí)停止歸納。
舉例說(shuō)明:黑色框代表當(dāng)前插入數(shù)字,即下標(biāo) j 所指。A[1···j - 1]為已排好序,A[j + 1···n]為待插入數(shù),即桌上牌堆。下圖顯示排序過(guò)程
此時(shí)循環(huán)不變式就是A[1···j - 1]子數(shù)組有序
初始化:第一次循環(huán)迭代之前(j = 2),此時(shí)A[1···j - 1]子數(shù)組為A [1],循環(huán)不變式成立。
保持:偽代碼4~7行將A[j - 1]、A[j - 2]等向右移動(dòng)一個(gè)位置直到A[j] 的適當(dāng)位置。此時(shí)子數(shù)組A[1··j]元素只是插入一個(gè)數(shù),相對(duì)位置不變,下一次迭代保持循環(huán)不變式。
終止:終止時(shí)j = n + 1,將循環(huán)不變式中j 用n + 1帶入有A[1···n]有序。
練習(xí)
2.1-1
2.1-2
INSERTION-SORT(A)
for j = 2 to A.length
key = A[j]
//Insert A[j] into the sorted sequence A[1..j-1].
i = j - 1
while i > 0 and A[i] < key // > 改 <
A[i+1] = A[i]
i = i - 1
A[i+1] = key
2.1-3
偽代碼:
SEARCH(A, v):
for i = 1 to A.length
if A[i] == v
return i
return NIL
循環(huán)不變式為找過(guò)的子數(shù)組無(wú)v。
2.1-4
偽代碼:
ADD-BINARY(A, B):
C = new integer[A.length + 1]
carry = 0
for i = 1 to A.length
C[i] = (A[i] + B[i] + carry) % 2 // remainder
carry = (A[i] + B[i] + carry) / 2 // quotient
C[i] = carry
return C
2.2 分析算法
插入排序算法的分析
一般來(lái)說(shuō)算法需要的時(shí)間與輸入的規(guī)模同步增長(zhǎng)。輸入規(guī)模根據(jù)不同問(wèn)題,有時(shí)指輸入中的項(xiàng)數(shù),有時(shí)是用二進(jìn)制表示輸入時(shí)用的總位數(shù),有時(shí)是圖的邊和點(diǎn)數(shù)。算法在特定輸入上的運(yùn)行時(shí)間指執(zhí)行的基本操作數(shù)或步數(shù)。插入排序每步執(zhí)行次數(shù)為:
所有項(xiàng)加起來(lái)得到總時(shí)間。但是真正關(guān)注的是運(yùn)行時(shí)間關(guān)于輸入的增長(zhǎng)量級(jí),所以只考慮公式中最重要的項(xiàng),增長(zhǎng)量級(jí)低的算法更好。
練習(xí)
2.2-1
2.2-2
偽代碼:
SELECTION-SORT(A):
for i = 1 to A.length - 1
min = i
for j = i + 1 to A.length
if A[j] < A[min]
min = j
temp = A[i]
A[i] = A[min]
A[min] = temp
C++實(shí)現(xiàn):
void SelectSort(int *a, int n)
{
int i, j, min, t;
for ( i = 1;i < n; i ++)
{
min = i;
for (j = i + 1; j<= n; j ++)
{
if (a[j] < a[min])
k = j;
}
if (min != i)
{
t = a[min];
a[min] = a[i];
a[i] = t;
}
}
}
循環(huán)不變量是子數(shù)組A[1..i - 1]是原數(shù)組前i-1個(gè)最小數(shù)的有序排列。或者當(dāng)前位置 i 是子數(shù)組A[i..n]的最小數(shù)的最終位置。分析初始化、保持、終止可知正確。因?yàn)閚 - 1個(gè)最小數(shù)排到正確位置后第 n 個(gè)自然是最大數(shù),排最后一個(gè)。最好最壞都是Θ(n^2)
2.2-3
答:平均n/2,最壞n。運(yùn)行時(shí)間都是Θ(n)。因?yàn)榈瓤赡埽總€(gè)數(shù)的概率為1/n,平均查找個(gè)數(shù)為 1/n * (1 + 2 + ... +n) = (n+1)/2,Θ(n)。最壞找到最后一個(gè)或者沒(méi)找到,比較 n 個(gè),Θ(n)。
2.2-4
答:首先檢查輸入數(shù)據(jù)看是否滿足某些可以直接輸出的特定條件,可以輸出事先計(jì)算好的結(jié)果。如:排序數(shù)組本來(lái)有序則直接輸出。
2.3 設(shè)計(jì)算法
算法設(shè)計(jì)技術(shù)有很多,插入排序使用了增量法,還有很多結(jié)構(gòu)遞歸:算法一次或多次遞歸地調(diào)用其自身以解決緊密相關(guān)的若干子問(wèn)題。
分治法
思想:將原問(wèn)題分解為幾個(gè)規(guī)模較小但類似于原問(wèn)題的子問(wèn)題,遞歸地求解這些子問(wèn)題,然后在合并這些子問(wèn)題的解來(lái)建立原問(wèn)題的解。分治模式在遞歸時(shí)有三個(gè)步驟:
- 分解原問(wèn)題為若干子問(wèn)題,這些子問(wèn)題是原問(wèn)題的規(guī)模較小的實(shí)例。
- 解決這些子問(wèn)題,遞歸地求解各子問(wèn)題。若子問(wèn)題規(guī)模足夠小則直接求解。
- 合并這些子問(wèn)題的解成原問(wèn)題的解。
以歸并排序?yàn)槔?/p>
關(guān)鍵是合并操作,調(diào)用MERGE(A, p, q, r),假設(shè)A[p···q]與A[q + 1 ···r]都已有序,合并成一個(gè)有序數(shù)組A[p···r]。思路是:每次將兩子數(shù)組當(dāng)前元素比較,較小的加入總數(shù)組并后移該子數(shù)組指針一位。復(fù)雜度Θ(n)。偽代碼:
C++實(shí)現(xiàn):
void Merge(int *a ,int p, int q, int r)
{
int i, j, k;
int n1 = q - p + 1;
int n2 = r - q;
int L[n1];
int R[n2];
for (i = 0; i < n1; i++)
L[i] = a[p + i];
for (j = 0; j < n2; j++)
R[j] = a[q + j + 1];
for(i = 0, j = 0, k = p; k <= r; k++)
{
if (i == n1)
a[k] = R[j++];
else if (j == n2)
a[k] = L[i++];
else if (L[i] <= R[j])
a[k] = L[i++];
else
a[k] = R[j++];
}
}
void MergeSort(int *a, int p, int r)
{
if (p < r)
{
int q = (p + r) / 2;
MergeSort(a, p, q);
MergeSort(a, q + 1, r);
Merge(a, p, q, r);
}
}
分析分治算法
算法包含遞歸調(diào)用時(shí)可用遞歸式描述運(yùn)行時(shí)間。設(shè)T(n)為一個(gè)規(guī)模為n的問(wèn)題的運(yùn)行時(shí)間。如果問(wèn)題的規(guī)模足夠小,如n≤c(c為常量),則得到它的直接解的時(shí)間為常量,寫(xiě)作?(1)。假設(shè)我們把原問(wèn)題分解成a個(gè)子問(wèn)題,每一個(gè)的大小是原問(wèn)題的1/b。如果分解該問(wèn)題和合并解的時(shí)間各為D(n)和C(n),則得到遞歸式:歸并排序算法的分析
- 分解:這一步僅僅是計(jì)算出子數(shù)組的中間位置,需要常量時(shí)間,因而D(n)= ?(1)
- 解決:遞歸地解兩個(gè)規(guī)模為n/2的子問(wèn)題,時(shí)間為2T(n/2)
-
合并:在一個(gè)含有n個(gè)元素的子數(shù)組上,MERGE過(guò)程的運(yùn)行時(shí)間為?(n),則C(n) = ? (n)
遞歸式為:
n為葉子數(shù),把各層加起來(lái)得到?(nlgn)。
練習(xí)
2.3-1
2.3-2
MERGE(A, p, q, r)
n1 = q - p + 1
n2 = r - q
let L[1..n?] and R[1..n?] be new arrays
for i = 1 to n?
L[i] = A[p + i - 1]
for j = 1 to n?
R[j] = A[q + j]
i = 1
j = 1
for k = p to r
if i > n?
A[k] = R[j]
j = j + 1
else if j > n?
A[k] = L[i]
i = i + 1
else if L[i] ≤ R[j]
A[k] = L[i]
i = i + 1
else
A[k] = R[j]
j = j + 1
區(qū)別主要在for循環(huán)中加了if判斷,如果其中一個(gè)子數(shù)組已經(jīng)遍歷完則加入另一數(shù)組元素。
2.3-3
- k = 1即n = 2時(shí) T(2) = 2成立
- 假設(shè)n = 2^k時(shí)成立,n = 2^(k+1)時(shí):
T(2^ (k + 1))= 2 * T(2^k) + 2^ (k + 1) = 2 * k * 2^k + 2^ (k + 1) = (k + 1)*2^ (k + 1) = nlgn
2.3-4
2.3-5
偽代碼:
BINARY-SEARCH(A, v):
low = 1
high = A.length
while low <= high
mid = (low + high) / 2
if A[mid] == v
return mid
if A[mid] < v
low = mid + 1
else
high = mid - 1
return NIL
C++實(shí)現(xiàn):
int BinarySearch(int *a, int length, int v)
{
int low = 0;
int high = length;
int mid;
while (low < high)
{
mid = (low + high) / 2;
if (a[mid] == v)
return mid;
if (a[mid] < v)
low = mid + 1;
else
high = mid;
}
return -1;
}
T(n) = T(n/2) + c, 故為Θ(lg n)。
2.3-6
答:不行,因?yàn)椴坏檎疫€要移動(dòng)元素。偽代碼:
INSERTION-SORT(A)
for j = 2 to A.length
key = A[j]
i = BinarySearch(A[1..j-1], key) // C1 = ∑lgj {j=2 to n}
for k = j to i+1 //C2 = ∑(j/2) {j=2 to n}
A[k] = A[k-1]
A[i] = k
2.3-7
答:
- 將S排序,遍歷元素 i (< x),二分查找(x - i)。運(yùn)行時(shí)間 = 排序Θ(nlgn) + 遍歷Θ(n)*二分查找(lgn) = Θ(nlgn)。偽代碼:
PAIR-EXISTS(S, x):
A = MERGE-SORT(S)
for i = 1 to S.length
if BINARY-SEARCH(A, x - S[i]) != NIL
return true
return false
- 將S排序,設(shè)兩指針指向頭尾向中間掃描。
- 哈希達(dá)到Θ(n)。同LeetCode Two Sum,Python代碼:
def twoSum(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
buff_dict = {}
for i in range(len(nums)):
if nums[i] in buff_dict:
return [buff_dict[nums[i]], i]
else:
buff_dict[target - nums[i]] = i
思考題
2-1
答:
a. 長(zhǎng)度為k的子表插入排序最壞為Θ(k^2),一共n/k個(gè),所以總運(yùn)行時(shí)間 = n/k * Θ(k^2) = Θ(nk)。
b. 根據(jù)歸并排序的第歸屬分析可知:總代價(jià) = 樹(shù)高 * 每層代價(jià),而每層代價(jià)常數(shù)倍的結(jié)點(diǎn)數(shù)。題干中 n/k 為最底層即葉子個(gè)數(shù),每次合并減少一半,故樹(shù)高為 lg(n/k) ,每層 n 個(gè)元素,所以復(fù)雜度為 n * lg(n/k) = Θ(nlg(n/k))。
c. k = Θ(lgn)。此時(shí) Θ(nk + nlg(n/k)) = Θ(nlgn + nlg(n/lgn)) = Θ(nlgn)。
d. 根據(jù)練習(xí)1.2-2可知 n>43 mergesort 好于 insertion sort,故取 k>43 且 k + lg(n/k) < lgn的值,因?yàn)榉值锰?xì)增加樹(shù)高。
2-2
答:
a. 還要證明數(shù)組A中的元素還是原來(lái)的那些。
b. 循環(huán)不變式為A[j]為A[j...n]中最小的
- 初始化:剛開(kāi)始只有A[n]故滿足最小。
- 保持:每次循環(huán)若A[j] < A[j-1]則交換,故成立。
- 終止:j = i 時(shí)迭代結(jié)束,此時(shí)A[i]是子數(shù)組A[i..n]的最小元素。
c. 位置 i 為原數(shù)組第 i 小的數(shù)。
- 初始化:數(shù)組為空,滿足。
- 保持:每次一輪內(nèi)循環(huán)都滿足 b 中的終止條件,即A[i]是子數(shù)組A[i..n]的最小元素。
- 終止:i = n 時(shí)迭代結(jié)束,此時(shí)A[n]是最后一個(gè)最小數(shù)。
d. Θ(n^2),同一個(gè)數(shù)量級(jí)但由于有交換操作,通常冒泡更慢。
2-3
答:
a. Θ(n)
b. 偽代碼:
y = 0
for i = 0 to n
m = 1
for k = 1 to i
m = m·x
y = y + a?·m
復(fù)雜度Θ(n^2),更慢。
c.
初始時(shí): 沒(méi)有項(xiàng),y = 0。
-
保持:第 i 次迭代結(jié)束時(shí):
終止:此時(shí) i = -1,帶入則為該和式。
d. 根據(jù)前三小問(wèn)得出答案。
2-4
答:
a. ?2,1?, ?3,1?, ?8,6?, ?8,1?,?6,1?.
b. ?n,n-1,n-2,...,3,2,1?最多,共(n-1) + (n-2) + …… + 3 + 2 + 1 = n(n-1)/2。
c. 插入排序中移動(dòng)元素的次數(shù)就是逆序數(shù)的對(duì)數(shù)。分析例子可知每次對(duì)一個(gè)小數(shù)插入時(shí),前面每個(gè)比它大的數(shù)都要后移,個(gè)數(shù)恰好是該元素的逆序?qū)?shù)。
d. 歸并排序merge操作時(shí)每次插入后半邊子數(shù)組時(shí),前半邊還未插入的元素個(gè)數(shù),就是逆序?qū)?shù)。
偽代碼:
MERGE-SORT(A, p, r):
if p < r
inversions = 0
q = (p + r) / 2
// 逆序?qū)?shù)量 = 左分支產(chǎn)生的數(shù)量 + 右分支產(chǎn)生的數(shù)量 + 歸并中產(chǎn)生的數(shù)量
inversions += merge_sort(A, p, q)
inversions += merge_sort(A, q + 1, r)
inversions += merge(A, p, q, r)
return inversions
else
return 0
MERGE(A, p, q, r)
n1 = q - p + 1 //前半部元素個(gè)數(shù)
n2 = r - q //后半部元素個(gè)數(shù)
let L[1..n?] and R[1..n?] be new arrays
for i = 1 to n?
L[i] = A[p + i - 1]
for j = 1 to n?
R[j] = A[q + j]
i = 1
j = 1
for k = p to r
if i > n? //前半部完成
A[k] = R[j]
j = j + 1
else if j > n?
A[k] = L[i]
i = i + 1
else if L[i] ≤ R[j]
A[k] = L[i]
i = i + 1
else //后半部有更小元素
A[k] = R[j]
j = j + 1
inversions += n? - i
return inversions
C++實(shí)現(xiàn):
void Merge(int *a ,int p, int q, int r)
{
int i, j, k, inversions = 0;
int n1 = q - p + 1;
int n2 = r - q;
int L[n1];
int R[n2];
for (i = 0; i < n1; i++)
L[i] = a[p + i];
for (j = 0; j < n2; j++)
R[j] = a[q + j + 1];
for(i = 0, j = 0, k = p; k <= r; k++)
{
if (i == n1)
a[k] = R[j++];
else if (j == n2)
a[k] = L[i++];
else if (L[i] <= R[j])
a[k] = L[i++];
else
{
a[k] = R[j++];
inversions += n1 - i;
}
}
return inversions;
}
void MergeSort(int *a, int p, int r)
{
if (p < r)
{
int inversions = 0;
int q = (p + r) / 2;
inversions += MergeSort(a, p, q);
inversions += MergeSort(a, q + 1, r);
inversions += Merge(a, p, q, r);
return inversions;
}
return 0;
}