天天看點

程式員是否必須會算法

本章的标題既然是“程式員與算法”,就必然要涉及一個基本問題,那就是“程式員是否必須會算法”。這是一個充滿争議的問題,雖然并不像“生存還是毀滅”之類的選擇那樣艱難而沉重,但也絕不是一個輕松的話題。朋友們在我的“算法系列”部落格專欄上發表的評論和回複,并不都是我所期待的贊美和鼓勵,也常常會有一些冷言冷語。比如,“窮舉也算是算法嗎”或者“請你說明一下算法在XX系統中能起到什麼作用”。

有一次,一個網友通過郵件問我:“你寫的都是小兒科的東西,幾十行代碼就能搞定,能不能整一點高深的算法?”我反問他什麼是他所了解的高深的算法,他答複說:“像遺傳算法、蟻群算法之類的。”于是我給了他一個遺傳算法求解0-1背包問題的例子(參見第16章),并告訴他,這也就是幾十行代碼的算法,怎麼了解成是高深的算法?他剛開始不承認這是遺傳算法,直到我給了他Denis Cormier公開在北卡羅來納州立大學伺服器上的遺傳算法的源代碼後,他才相信他一直認為深不可測的遺傳算法的原理原來是這麼簡單。

還有一個網友直言我寫的“用三個水桶等分8升水”之類的問題根本就稱不上算法,他認為像“深藍”那樣的人工智能才算是算法。我告訴他計算機下棋的基本理論就是博弈樹,或者再加一個專家系統。但是他認為博弈樹也是很高深的算法,于是我給了他一個井字棋遊戲(參見第23章),并告訴他,這就是博弈樹搜尋算法,非常智能,你絕對戰勝不了它(因為井字棋遊戲很簡單,這個算法會把所有的狀态都搜尋完)。我相信他一定很震驚,因為這個算法也不超過100行代碼。

對于上面提到的例子,我覺得主要原因在于大家對算法的了解有差異,很多人對算法的了解太片面,很多人覺得隻有名字裡包含“XX算法”之類的東西才是算法。而我認為算法的本質是解決問題,隻要是能解決問題的代碼就是算法。在讨論程式員與算法這個問題之前,我們先探讨一個最基本的問題:什麼是算法。

1.1 什麼是算法

《算法導論》一書将算法(algorithm)描述為定義良好的計算過程,它取一個或一組值作為輸入,并産生一個或一組值作為輸出。Knuth在《計算機程式設計藝術》一書中将算法描述為從一個步驟開始,按照既定的順序執行完所有的步驟,最終結束(得到結果)的一個過程。Weiss在《資料結構與算法分析》一書中将算法描述為一系列的計算步驟,将輸入資料轉換成輸出的結果。

雖然沒有被普遍接受的“算法”的正式定義,但是各種著作中對算法的基本要素或基本特征的定義都是明确的,Knuth總結了算法的四大特征。

  • 确定性。算法的每個步驟都是明确的,對結果的預期也是确定的。
  • 有窮性。算法必須是由有限個步驟組成的過程,步驟的數量可能是幾個,也可能是幾百萬個,但是必須有一個确定的結束條件。
  • 可行性。一般來說,我們期望算法最後得出的是正确的結果,這意味着算法中的每一個步驟都是可行的。隻要有一個步驟不可行,算法就是失敗的,或者不能被稱為某種算法。
  • 輸入和輸出。算法總是要解決特定的問題,問題來源就是算法的輸入,期望的結果就是算法的輸出。沒有輸入的算法是沒有意義的,沒有輸出的算法是沒有用的。

算法需要一定的數學基礎,但是沒有任何文獻資料将算法限定于隻解決數學問題。有些人将貪婪法、分治法、動态規劃法、線性規劃法、搜尋和枚舉(包括窮盡枚舉)等方法了解為算法,其實這些隻是設計算法常用的設計模式(Knuth稱之為設計範式)。同樣,計算機程式隻是算法的一種存在形式,僞代碼、流程圖、各種符号和控制表格也是常見的算法展示形式。而順序執行、并行執行(包括分布式計算)、遞歸方法和疊代方法則是常用的算法實作方法。

綜合以上分析和引述,本人将算法定義為:算法是為解決一個特定的問題而精心設計的一套數學模型以及在這套數學模型上的一系列操作步驟,這些操作步驟将問題描述的輸入資料逐漸處理、轉換,并最後得到一個确定的結果。使用“精心設計”一詞,是因為我将算法的設計過程了解為人類頭腦中知識、經驗激烈碰撞的過程,将算法了解為最終“小宇宙爆發”一般得到的智力結果。

1.2 程式員必須要會算法嗎

很多人可能是好萊塢大片看多了,以為計算機神通廣大,但事實不是這樣的。計算機其實是一種很傻的工具,傻到幾乎沒有智商(至少目前是這樣)。它可以連續幾年做同一件事情而毫無怨言,但是如果你不告訴它怎麼做,它什麼事情也不會做。最有創造性的活動其實是由一種被稱為“程式員”的人做的,計算機做的隻不過是人類不願意做的體力活而已。比如圖像識别技術,需要一個位元組一個位元組地處理資料,提取資料的特征值,然後在海量的資料中比較、比對這些特征值,直到累得兩眼昏花,人類才不會幹這種傻事兒呢。計算機願意做,但前提是你要告訴它怎麼做。算法可以了解為這樣一種技術,它将告訴計算機怎麼做。有人将程式設計了解為搭積木,直接用别人開發好的元件、庫,甚至是類或API就行了,并且美其名曰“不用重複發明輪子”。我認為這其實就是所謂的系統內建,如果一個程式員每天的工作就是搭積木,那将是令人十分羨慕的事情,但是我知道,事實并不是這樣的。這樣搭積木式的程式設計計算機就可以做,沒有必要讓人來做,因為人工的成本高于計算機。我遇到的更多的是在論壇裡發帖求助的人,比如“求代碼,把一個固定格式的文本檔案讀入記憶體”,再比如“誰能幫我把這個結構數組排排序啊,書上的例子都是整數數組排序”。他們是如此地無助,如果不是論壇對回帖有積分獎勵的話,恐怕不會有人理他們。

我要說的是,大多數程式員并不需要知道各種專業領域裡的算法,但是你要會設計能夠解決你面臨問題的算法。一些領域内的經典問題,在前人的努力之下都有了高效的算法實作,本書的很多章節都介紹了這樣的算法,比如穩定比對問題、A*算法等。但是更多情況下,你所面臨的問題并沒有現成的算法實作,需要程式員具有創新的精神。算法設計需要具備很好的數學基礎,但數學并不是唯一需要的知識,計算機技術的一些基礎學科(比如資料結構)也是必需的知識,有人說:程式=算法+資料結構,這個雖然不完全正确,但是提到了計算機程式最重要的兩點,那就是算法和資料結構。算法和資料結構永遠是緊密聯系在一起的,算法可以了解為解決問題的思想,這是程式中最具有創造性的部分,也是一個程式有别于另一個程式的關鍵點,而資料結構就是這種思想的載體。

再次重申一遍,我和大多數人一樣,并不是要求每個程式員都精通各種算法。大多數程式員可能在整個職業生涯中都不會遇到像ACM(Association for Computing Machinery)組織的國際大學生程式設計競賽中的問題,但是說用不到資料結構和算法則是不可想象的。說資料結構和算法沒用的人是因為他們用不到,用不到的原因是他們想不到,而想不到的原因是他們不會。請息怒,我不是要打擊任何人,很多情況下确實是因為不會,是以才用不到,下面就是一個典型的例子。

1.2.1 一個隊列引發的慘案

我所在的團隊負責一款光接入網産品的“EPON業務管理子產品”的開發和維護工作,這是電信級的網絡裝置,是以對各方面性能的要求都非常高。有一天,一個負責內建測試的小夥兒跑過來對我說,今天的每日構造版本出現異常,所有線卡(承載資料業務的闆卡)的上線時間比昨天的版本慢了4分鐘左右。我很驚訝,對于一個電信級網絡裝置來說,每次加電後的線卡上線時間就是業務恢複時間,業務恢複時間這麼慢是不能接受的。于是我檢查了一下前一天的代碼入庫記錄,很快就找到了問題所在。原來目前版本的任務清單中有這樣一項功能,那就是記錄線卡的資料變更日志,需求的描述是線上卡上維護一個日志緩沖區,每當有使用者操作造成資料變更時,就記錄一條變更資訊,線卡上線時的批量資料同步也屬于操作資料變更,也要計入日志。因為是嵌入式裝置,線卡上日志緩沖區的大小受限制,最多隻能存儲1000條記錄,當記錄的日志超過1000條時,新增的日志記錄将覆寫舊的記錄,也就是說,這個日志緩沖區隻保留最近寫入的1000條記錄。一個新來的小夥兒接受了這個任務,并在前一天下班前将代碼簽入庫中(程式員要記住啊,一定不要在臨下班前簽入代碼)。他的實作方案大緻是這樣的(注釋是我加上的):

#define SYNC_LOG_CNT             1000
#define SYNC_LOG_MEMOVER_CNT     50

typedef struct
{
    INT32U logCnt;
    EPON_SYNC_LOG_DATA syncLogs[SYNC_LOG_CNT];
}EPON_SYNC_LOG;

EPON_SYNC_LOG s_EponSyncLog;

void Epon_Sync_Log_Add(EPON_SYNC_LOG_DATA*pLogData)
{
    INT32U i = ;
    INT32U syncLogCnt = ;

    syncLogCnt = s_EponSyncLog.logCnt;
    if(syncLogCnt>=SYNC_LOG_CNT)
    {
        /*緩沖區已滿,向前移動950條記錄,為新紀錄騰出50條記錄的空間*/
        memmove(s_EponSyncLog.syncLogs,
                s_EponSyncLog.syncLogs + SYNC_LOG_MEMOVER_CNT,
                (SYNC_LOG_CNT-SYNC_LOG_MEMOVER_CNT) * sizeof(EPON_SYNC_LOG_DATA));
        /*清空新騰出來的空間*/
        memset(s_EponSyncLog.syncLogs + (SYNC_LOG_CNT - SYNC_LOG_MEMOVER_CNT),
               , SYNC_LOG_MEMOVER_CNT * sizeof(EPON_SYNC_LOG_DATA));
        /*寫入目前一條日志*/
        memmove(s_EponSyncLog.syncLogs + (SYNC_LOG_CNT - SYNC_LOG_MEMOVER_CNT),
                pLogData, sizeof(EPON_SYNC_LOG_DATA));
        s_EponSyncLog.logCnt = SYNC_LOG_CNT - SYNC_LOG_MEMOVER_CNT + ;

        return;
    }
        /*如果緩沖區有空間,則直接寫入目前一條記錄*/
    memmove(s_EponSyncLog.syncLogs + syncLogCnt,
            pLogData, sizeof(EPON_SYNC_LOG_DATA));
    s_EponSyncLog.logCnt++;
}
           

這個方案使用一個長度為1000條記錄的數組存儲日志,用一個計數器記錄目前寫入的有效日志條數,資料結構的設計中規中矩,但是當緩沖區滿,需要覆寫舊記錄時遇到了麻煩,因為每次都要移動數組中的前999條記錄,才能為新記錄騰出空間,這将使

Epon_Sync_Log_Add()

函數的性能急劇惡化。考慮到這一點,小夥兒為他的方案設計了一個門檻值,就是

SYNC_LOG_MEMOVER_CNT

常量定義的50。當緩沖區滿的時候,就一次性向前移動950條記錄,騰出50條記錄的空間,避免了每新增一條記錄就要移動全部資料的情況。可見這個小夥兒還是動了一番腦子的,在

Epon_Sync_Log_Add()

函數調用不是很頻繁的情況下,在功能和性能之間做了個折中,根據自測的情況,他覺得還可以,于是就在下班前匆匆簽入代碼,沒有來得及安排代碼走查和同行評審。但是他沒有考慮到線卡上線時需要批量同步資料的情況,在這種情況下,

Epon_Sync_Log_Add()

函數被調用的頻度仍然超出了這個門檻值所能容忍的程度。通過對任務的性能進行分析,我們發現大量的時間都花費在

Epon_Sync_Log_Add()

函數中移動記錄的操作上,即便是設計了門檻值

SYNC_LOG_MEMOVER_CNT

,性能依然很差。

其實,類似這樣的固定長度緩沖區的讀寫,環形隊列通常是最好的選擇。下面我們來看一下環形隊列的示意圖,如圖1-1所示。

程式員是否必須會算法

圖 1-1 環形隊列示意圖

計算機記憶體中沒有環形結構,是以環形隊列都是用線性表來實作的,當資料指針到達線性表的尾部時,就将它轉到0位置重新開始。實際程式設計的時候,也不需要每次都判斷資料指針是否到達線性表的尾部,通常用取模運算對此做一緻性處理。設模拟環形隊列的線性表的長度是N,隊頭指針為

head

,隊尾指針為

tail

,則每增加一條記錄,就可用以下方法計算新的隊尾指針:

tail = (tail + 1) % N

           

對于本例的功能需求,當

tail + 1

等于

head

的時候,說明隊列已滿,此時隻需将

head

指針向前移動一位,就可以在

tail

位置寫入新的記錄。使用環形隊列,可以避免移動記錄操作,本節開始時提到的性能問題就迎刃而解了。在這裡,套用一句廣告詞:“沒有做不到,隻有想不到。”看看,我沒說錯吧?

1.2.2 我的第一個算法

我的第一份工作是為一個光栅圖像矢量化軟體編寫一個圖像預處理系統,這套光栅圖像矢量化軟體能夠将從紙質工程圖紙掃描得到的位圖圖紙識别成能被各種CAD軟體處理的矢量化圖形檔案。在預處理系統中有一個功能是對已經二值化的光栅位圖(黑白兩色位圖)進行污點消除。光栅位圖上的污點可能是原始圖紙上掃描前就存在的墨點,也可能是掃描器引入的噪點,這些污點會對矢量化識别過程産生影響,會識别出錯誤的圖形和符号,是以需要預先消除這些污點。

當時我不知道有小波算法,也不知道還有各種圖像濾波算法,隻是根據對問題的認識,給出了我的解決方案。首先我觀察圖紙檔案,像直線、圓和弧線這樣有意義的圖形都是最少有5個點互相連在一起構成的,而污點一般都不會超過5個點連在一起(較大的污點都用其他的方法除掉了)。是以我給出了污點的定義:如果一個點周圍與之相連的點的總數小于5,則這幾個相連在一起的點就是一個污點。根據這個定義,我給出了我的算法:從位圖的第一個點開始搜尋,如果這個點是1(1表示黑色,是圖紙上的點;0表示白色,是圖紙背景顔色),就将相連點計數器加1,然後繼續向這個點相連的8個方向分别搜尋,如果某個方向上的相鄰點是0就停止這個方向的搜尋。如果搜尋到的相連點超過4個,說明這個點是某個圖形上的點,就退出這個點的搜尋。如果搜尋完成後得到的相連的點小于或等于4個,就說明這個點是一個污點,需要将其顔色置為0(清除污點)。

算法實作首先定義搜尋過程中存儲相連點資訊的資料結構,這個資料結構定義如下:

typedef struct tagRESULT
{
    POINT pts[MAX_DIRTY_POINT];/*記錄搜尋過的前5個點的位置*/
    int count;
}RESULT;

           

這個資料結構有兩個屬性,

count

是搜尋過程中發現的相連點的個數,

pts

是記錄這些相連點位置的線性表。記錄這些點的位置是為了在搜尋結束後,如果判定這些點是污點,可以利用這些記錄的位置資訊直接清除這些點的顔色。

/*8個方向*/
POINT dir[] = { {-1, 0}, {-1, -1}, {0, -1}, {1, -1}, {1, 0}, {1, 1}, {0, 1}, {-1, 1} };

void SearchDirty(char bmp[MAX_BMP_WIDTH][MAX_BMP_HEIGHT]
                 int x, int y, RESULT *result)
{
    for(int i = 0; i < sizeof(dir)/sizeof(dir[0]); i++)
    {
        int nx = x + dir[i].x;
        int ny = y + dir[i].y;
        if( (nx >= 0 && nx < MAX_BMP_WIDTH)
            && (ny >= 0 && nx < MAX_BMP_HEIGHT)
            && (bmp[nx][ny] == 1) )
        {
            if(result->count < MAX_DIRTY_POINT)
            {
                /*記錄前MAX_DIRTY_POINT個點的位置*/
                result->pts[result->count].x = nx;
                result->pts[result->count].x = ny;
            }
            result->count++;
            if(result->count > MAX_DIRTY_POINT)
                break;

            SearchDirty(bmp, nx, ny, result);
        }
    }
}

           

向8個方向搜尋使用了預置的矢量數組

dir

,這是迷宮或棋盤類遊戲搜尋慣用的模式,本書介紹的算法會多次使用這種模式。

SearchDirty()

函數遞歸地調用自己,實作對8個方向的連通性搜尋,最後的結果存在

result

中,如果

count

的個數大于

4

,說明

[x, y]

位置的點是正常圖形上的點,如果

count

的個數小于或等于4,則說明

[x, y]

位置相鄰的這個點是一個污點。污點相鄰的點的位置都被記錄在

pts

中,将這些位置的位圖資料置0就消除了污點。算法沒有做任何優化,不過好在圖紙上大部分都是白色背景,需要搜尋的點并不多。打開測試圖紙一試,速度并不慢,效果也很好,幾個故意點上去做測試用的污點都沒有了,小的噪點也沒有了,圖紙一下就變白了。不過這段代碼最終并沒有成為那個軟體的一部分,學過機械制圖的同學可能看出來了,這個算法會将一些細小的虛線和點劃線一并幹掉。

這是一個微不足道的問題,但卻是我第一次為解決(當然,未遂)問題而設計了一個算法,并最終用程式将其實作。它讓我領悟到了一個道理,軟體被編寫出來就是為了解決問題的,程式員的任務就是設計解決這些問題的算法。成功固然高興,失敗也沒有什麼代價,可以随時卷土重來。不要小看這些事情,不要以為隻有各種專業領域的程式才會用到算法,每一個微小的設計都是算法創造性的展現,即使失敗,也比放棄強。

1.3 算法的樂趣在哪裡

算法有很多種存在形式,編寫計算機程式隻是其中一種,是程式員慣用的方式,本書要介紹的内容就是如何以計算機程式的方式研究算法。1.2節介紹的兩個例子都是我親身經曆過的事情,程式員在大部分時間裡都是處理一些平凡而瑣碎的程式,但有時候也需要做一些創造性的工作。記住,程式員就是計算機的“上帝”,計算機能解決問題是因為它的“上帝”告訴它怎麼做。那麼,當問題來臨的時候,“上帝”是到各種論壇上發文章求代碼,還是自己解決問題?

如果要自己解決問題,應該如何解決問題?為什麼要自己解決問題?先來回答第一個問題——如何設計算法解決問題?人類解決問題的方式是當遇到一個問題時,首先從大腦中搜尋已有的知識和經驗,尋找它們之間具有關聯的地方,将一個未知問題做适當的轉換,轉化成一個或多個已知問題進行求解,最後綜合起來得到原始問題的解決方案。編寫計算機程式實作算法,讓計算機幫我們解決問題的過程也不例外,也需要一定的知識和經驗。為了讓計算機幫我們解決問題,就要設計計算機能了解的算法程式。而設計算法程式的第一步就是要讓計算機了解問題是什麼。這就需要建立現實問題的數學模型。模組化過程就是一個對現實問題的抽象過程,運用邏輯思維能力,抓住問題的主要因素,忽略次要因素。建立數學模型之後,第二個要考慮的問題就是輸入輸出問題,輸入就是将自然語言或人類能夠了解的其他表達方式描述的問題轉換為數學模型中的資料,輸出就是将數學模型中表達的運算結果轉換成自然語言或人類能夠了解的其他表達方式。最後就是算法的設計,其實就是設計一套對數學模型中的資料的操作和轉換步驟,使其能演化出最終的結果。

數學模型、輸入輸出方法和算法步驟是編寫計算機算法程式的三大關鍵因素。對于非常複雜的問題,建立數學模型是非常難的事情,比如天文實體學家研究的“宇宙大爆炸”模型,再比如熱力學研究的複雜幾何體冷卻模型,等等。不過,這不是本書探讨的範圍,程式員遇到的問題更多的不是這種複雜的理論問題,而是軟體開發過程中常用和常見的問題,這些問題簡單,但并不枯燥乏味。對于簡單的計算機算法而言,建立數學模型實際上就是設計合适的資料結構的問題。這又引出了前面提到的話題,資料結構在算法設計過程中扮演着非常重要的角色。輸入輸出方式和算法步驟設計都是基于相應的資料結構設計的,相應的資料結構要能很友善地将原始問題轉換成資料結構中的各個屬性,也要能很友善地将資料結構中的結果以人們能夠了解的方式輸出,同時,也要為算法轉換過程中各個步驟的演化提供最便利的支援。使用線性表還是關聯結構,使用樹還是圖,都是在設計輸入輸出和算法步驟時就要考慮的問題。

為什麼要自己解決問題?愛因斯坦說過:“興趣是最好的老師。”這就是說,隻要一個人對某事物産生興趣,就會主動去學習、去研究,并在學習和研究的過程中産生愉快的情緒。我把從算法中體會到的樂趣分成三個層次:初級層次是找到特定的算法解決特定的實際問題,這種樂趣是解決問題後的成就感;中級層次是有些算法本身就是充滿樂趣的,搞明白這種算法的原理并寫出算法的程式代碼,能為自己今後的工作帶來便利;進階層次是自己設計算法解決問題,讓其他人可以利用你的算法享受到初級層次的樂趣。有時候問題可能是别人沒有遇到過的,沒有已知的解法,這種情況下隻能自己解決問題。這是本書一直強調算法的樂趣的原因。隻有體會到樂趣,才有動力去學習和研究,而這種學習和研究的結果是為自己帶來正向的激勵,為今後的工作帶來便利。回想一下1.2.1節的例子,環形隊列相關的算法是固定長度緩沖區讀寫的常用模式,如果知道這一點,就不會有這種問題了。

1.4 算法與代碼

本書講到的算法都是以計算機程式作為載體展示的,其基本形式就是程式代碼。作為一個軟體開發人員,你希望看到什麼樣的代碼?是這樣的代碼:

double kg = gScale * 102.1 + 55.3;
NotifyModule1(kk);
double kl1 = kg / l_mask;
NotifyModule2(kl1);
double kl2 = kg * 1.25 / l_mask;
NotifyModule2(kl2);

           

還是這樣的代碼:

double globalKerp = GetGlobalKerp();
NotifyGlobalModule(globalKerp);
double localKrep = globalKerp / localMask;
NotifyLocalModule(localKrep);
double localKrepBoost = globalKerp * 1.25 / localMask;
NotifyLocalModule(localKrepBoost);

           

程式員都有一種直覺,那就是能看懂的代碼就是好代碼。但是“能看懂”是一個非常主觀的感覺,同樣的代碼給不同的人看,能否看懂有着天壤之别。《重構》一書的作者為不好的代碼總結了21條“壞味道”規律,希望能夠對号入座地判斷一下代碼中的“壞代碼”。但是這21條規律仍然太主觀,于是人們又給代碼制定了很多量化名額,比如代碼注釋率(這個名額因為沒有意義,已經被很多組織抛棄了)、平均源代碼檔案長度、平均函數長度、平均代碼依賴度、代碼嵌套深度、測試用例覆寫度,等等。做這些工作的目的在于人們希望看到漂亮的代碼,這不僅僅是主觀審美的需要,更是客觀上對軟體品質的不懈追求。漂亮的代碼有助于改善軟體的品質,這已經是公認的事實,因為程式員在把他們的代碼變得漂亮的過程中,能夠通過一些細小卻又重要的方式改善代碼的品質,這些細小卻又重要的方式包括但不限于更好的設計、可測試性和可維護性等方面的方法。

在保證軟體行為正确性的基礎上,人們都用什麼詞來形容好的代碼呢?好看、漂亮、整潔、優雅、藝術品、像詩一樣?我看過很多軟體的代碼,有開源軟體的代碼,也有商業軟體的代碼,好的代碼給我的感覺就是以上這些形容詞,當然也見過不好的代碼,給我的感覺就是“一堆代碼”而已。我在寫“算法系列”部落格專欄的時候,就特别注意這一點,即便别人已經釋出過類似的算法實作,我也希望我的算法呈現出來的是完全不一樣的代碼。設計算法也和設計軟體一樣,應該是漂亮的代碼,如果幾百行代碼堆在一起,不分主次,關系淩亂,隻是最後堆出了一個正确的結果,這不是我所希望的代碼,即虐人又虐己。大部分人來看你的部落格,應該還是為了看懂吧。在我準備這本書的時候,我把很多算法又重新寫了一遍,不僅算法有趣,研究代碼也是一種樂趣。如果算法本身很有趣,但是最後的代碼實作卻是毫無美感的“一堆代碼”,那真是太掃興了。

1.5 總結

本章借用了多部知名著作中對算法的定義,隻是想讓大家對算法有一個“寬容”一點的了解。通過我親身經曆的兩個例子,說明了程式員與算法之間“剪不斷,理還亂”的關系。除此之外,還簡單探讨了算法樂趣的來源、算法和代碼的關系,以及研究代碼本身的樂趣等内容。

如果你認同我的觀點,就可以繼續閱讀本書了。本書的每一章都是獨立的,沒有前後關系,你可以根據自己的喜好直接閱讀相關的章節。希望本書能使你有所收獲,并體會到算法的樂趣。

1.6 參考資料

[1] Cormen T H, et al. Introduction to Algorithms (Second Edition). The MIT Press, 2001

[2] Knuth D E. The Art of Computer Programming (Third Edition), Vol 1. Addison-Wesley, 1997

[3] Weiss M A. Data Structures and Algorithm Analysis (Second Edition). Addison-Wesley, 2001

[4] Oram A, Wilson G. Beautiful Code. O’Reilly Media, Inc, 2007

[5] Fowler M, et al. Refactoring: Improving the Design of Existing Code. Addison-Wesley, 1999

程式員是否必須會算法

本文摘自《算法的樂趣》

繼續閱讀