算法簡單學(xué)習(xí)(三)—— 分治法與合并排序

版本記錄

版本號(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ù)組LR,長度分別為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è)簡單的例子看一下合并排序的步驟,先看下面兩張圖。

合并排序1
合并排序2

這里我們調(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í)間消耗分析

有了上面時(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á)式

下面看一下上面時(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ù)~~

秋天
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,402評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評(píng)論 0 287
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,596評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,784評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評(píng)論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,027評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,398評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,743評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容