具體化你的解決的問題。下面是A和B的對話。html
A:我該如何對磁盤文件進行排序? B:須要排序的內容是什麼?文件中有多少條記錄?每一個記錄的格式是什麼? A:該文件包含至多10,000,000個記錄,每條記錄都是一個7位整數。 B:若是文件那麼小,爲何要使用磁盤排序呢?爲何不在主存中對它排序? A:該功能是某大型系統中的一部分,大概只能提供1MB主存給它。 B:你能將記錄方面的內容說得更詳細一些嗎? A:每一個記錄是一個7位正整數,沒有其它的關聯數據,每一個整數至多隻能出現一次。 ... ...
通過一系統的問題,咱們能夠將一個定義模糊不清的問題變得具體而清晰:node
輸入: 所輸入的是一個文件,至多包含n個正整數,每一個正整數都要小於n,這裏n=10^7。 若是輸入時某一個整數出現了兩次,就會產生一個致命的錯誤。 這些整數與其它任何數據都不關聯。 輸出: 以增序形式輸出通過排序的整數列表。 約束: 大概有1MB的可用主存,但可用磁盤空間充足。運行時間至多容許幾分鐘, 10秒鐘是最適宜的運行時間。
若是主存容量不是嚴苛地限制在1MB,好比說能夠是1MB多,或是1~2MB之間, 那麼咱們就能夠一次性將全部數據都加載到主存中,用Bitmap來作。 10,000,000個數就須要10,000,000位,也就是10,000,000b = 1.25MB。程序員
程序可分爲三個部分:第一,初始化全部的位爲0;第二,讀取文件中每一個整數, 若是該整數對應的位已經爲1,說明前面已經出現過這個整數,拋出異常,退出程序 (輸入要求每一個整數都只能出現一次)。不然,將相應的位置1;第三, 檢查每一個位,若是某個位是1,就寫出相應的整數,從而建立已排序的輸出文件。面試
若是主存容量嚴苛地限制在1MB,而使用Bitmap須要1.25MB, 所以沒法一次載入完成排序。那麼,咱們能夠將該文件分割成兩個文件, 再分別用Bitmap處理。分割策略能夠簡單地把前一半的數據放到一個文件, 後一半的數據放到另外一個文件,分別排序後再作歸併。 也能夠把文件中小於某個數(好比5,000,000)的整數放到一個文件,叫less.txt, 把其他的整數放到另外一個文件,叫greater.txt。分別排序後, 把greater.txt的排序結果追加到less.txt的排序結果便可。算法
第2章圍繞3個問題展開。數據庫
這是CTCI中的一道題目,詳細解答請戳如下連接:編程
請猛戳我api
這個問題很常見了,作3次翻轉便可,無需額外空間:數組
reverse(0, i-1); // cbadefgh reverse(i, n-1); // cbahgfed reverse(0, n-1); // defghabc
這個問題能夠分3步來解決。第一步將每一個單詞按字典序排序, 作爲原單詞的簽名,這樣一來,變位詞就會具備相同的簽名。 第二步對全部的單詞按照其簽名進行排序,這樣一來,變位詞就會彙集到一塊兒。 第三步將變位詞分組,造成變位詞集。示意圖以下:安全
恰當的數據視圖實際上決定了程序的結構。 咱們經常能夠經過從新組織內部數據來使程序變得小而美。
發明家悖論:更通常性的問題也許更容易解決。(有時候吧)
程序員在節省空間方面機關用盡時,將本身從代碼中解脫出來, 退回起點並集中心力研究數據,經常能有奇效。數據的表示形式是程序設計的根本。
下面是退回起點進行思考時的幾條原則:
使用數組從新編寫重複代碼。冗長的類似代碼經常可使用最簡單的數據結構—— 數組來更好地表述。
封裝複雜結構。當須要很是複雜的數據結構時,使用抽象術語進行定義, 並將操做表示爲類。
儘量使用高級工具。超文本,名字-值對,電子表格,數據庫, 編程語言等都是特定問題領域中的強大的工具。
從數據得出程序的結構。在動手編寫代碼以前,優秀的程序員會完全理解輸入, 輸出和中間數據結構,並圍繞這些結構建立程序。
提到的書籍:Polya的《How to Solve it》,中文書《怎樣解題》; Kernighan和Plauger的《Elements of Programming Style》;Fred Brooks的《人月神話》 Steve McConnell的《代碼大全》;《Rapid Development》; 《Software Project Survival Guide》
本章以二分搜索爲例子,講述瞭如何對程序進行驗證及正確性分析。
深刻閱讀:David Gries的《Science of Programming》 是程序驗證領域裏極佳的一本入門書籍。
到目前爲止,你已經作了一切該作的事:經過深刻挖掘定義了正確的問題, 經過仔細選擇算法和數據結構平衡了真正的需求,經過程序驗證技術寫出了優雅的代碼, 而且對其正確性至關有把握。萬事俱備,只欠編程。
使用斷言assert
自動化測試程序
進階閱讀:《Practice of Programming》第5章(調試),第6章(測試) 《Code Complete》第25章(單元測試),第26章(調試)
下圖展現了一個程序的性能提高過程, 該程序的做用是對三維空間中n個物體的運動進行仿真。從圖中能夠看出, 一個程序能夠從多方面進行性能提高,而其中算法和數據結構的選擇又顯得尤其重要。
從設計層面提高程序性能:
深刻閱讀:Butler Lampson的「Hints for Computer System Design」, 該論文特別適合於集成硬件和軟件的計算機系統設計。
這一章講述了估算技術,我認爲是至關有用的一章。
文中先拋出一個問題:密西西比河一天流出多少水?若是讓你來回答, 你會怎麼答,注意不能去Google哦。
做者是這麼回答這個問題:假設河的出口大約有1英里寬和20英尺深(1/250英里), 而河水的流速是每小時5英里,也就是天天120英里。則能夠計算出一天的流量:
1英里 * 1/250英里 * 120英里/天 約等於 1/2 英里^3/天
上述算式很是簡單,但是在看到這些文字以前,若是有人真的問你, 密西西比河一天流出多少水?你真的能答上來嗎?仍是愣了一下後,擺擺手,說: 這我哪知道!
對於上面的問題,咱們至少能夠注意到如下兩點:
你須要把問題轉換成一個可計算的具體模型。這一點每每不須要太擔憂, 由於咱們作的是估算,因此能夠忽視不少可有可無的因素,能夠去簡化你的模型, 記住咱們要的只是一個粗略計算的結果。好比對於上面的問題, 計算密西西比河一天流出多少水其實就是計算其一天的流量,利用中學所學知識, 流量 = 截面積 x 流速,那咱們就只需計算密西西比河的出水口的截面積和流速便可。 咱們能夠將出水口簡化成一個矩形,所以就只須要知道出水口的寬和深便可。
你須要知道常識性的東西。上面咱們已經把問題轉換成了一個可計算的具體模型: 流量 = 出水口寬 x 出水口深 x 流速。接下來呢?你須要代入具體的數值去求得答案。 而這就須要你具有一些常識性的知識了。好比做者就估計了密西西比河的出口有1英里寬, 20英尺深(若是你估計只有幾十米寬,那就相差得太離譜了)。 這些常識性的知識比第1點更值得關注,由於你沒法給出一個靠譜的估算值每每是由於這點。
當咱們懂得如何把一個問題具體化定義出來併爲其選用適當的模型, 而且咱們也積累了必要的常識性的知識後,回答那些初看起來無從下手的問題也就不難了。 這就是估算的力量。
如下是估算時的一些有用提示:
兩個答案比一個答案好。即鼓勵你從多個角度去對一個問題進行估算, 若是從不一樣角度獲得的答案差異都不大,說明這個估算值是比較靠譜的。
快速檢驗。即量綱檢驗。即等式兩邊最終的量綱要一致。 這一點在等式簡單的時候至關顯而易見。好比位移的單位是米,時間單位是秒, 速度單位是米/秒,那顯然咱們應該要用位移去除以時間來獲得速度, 這樣才能保證它們單位的一致。你可能會說,我了個去,這種小學生都懂的事, 你好意思拿出來說。其實否則,當你面對的是一個具備多個變量的複雜物理公式, 或者你提出某種物理假設,正在考慮將其公式化,該方法能夠切切實實地幫你作出檢驗。
經驗法則。「72法則」:1.假設以年利率r%投資一筆錢y年,若是r*y = 72, 那麼你的投資差很少會翻倍。2.若是一個盤子裏的菌羣以每小時3%的速率增加, 那麼其數量天天(24小時)都會翻倍。在偏差不超過千分之五的狀況下, \pi秒就是一個納世紀。也就是說:
3.14秒 = 10-9 * 100年 = 10-7 年
也就是說,1年大概是3.14x107 秒。因此若是有人告訴你,一個程序運行107 秒, 你應該能很快反應出,他說的實際上是4個月。
若是問題的規模太大,咱們還能夠經過求解它的小規模同質問題來作估算。好比, 咱們想測試某個程序運行10億次須要多長時間,若是你真去跑10億次, 說不定運行幾個小時都沒結束,那不是很悲劇?咱們能夠運行這個程序1萬次或是10萬次, 得出結果真後倍增它便可。固然,這個結果未必是準確的, 由於你無法保證運行時間是隨着運行次數線性增長的。謹慎起見,咱們能夠運行不一樣的次數, 來觀察它的變化趨勢。好比運行10次,100次,1000次,10000次等, 觀察它的運行時間是不是線性增長的,或是一條二次曲線。
有時候,咱們須要爲估算的結果乘上一個安全係數。好比, 咱們預估完成某項功能須要時間t,那根據以往經驗,也許咱們須要爲這個值乘上2或4, 這樣也許纔是一個靠譜的預估值。
Little定律:系統中物體的平均數量等於物體離開系統的平均速率和每一個物體在系統中停留 的平均時間的乘積。(若是物體離開和進入系統的整體出入流是平衡的, 那麼離開速率也就是進入速率)
舉個例子,好比你正在排除等待進入一個火爆的夜總會, 你能夠經過估計人們進入的速率來了解本身還要等待多長時間。根據Little定律, 你能夠推論:這個地方能夠容納約60人,每一個人在裏面逗留時間大約是3小時, 所以咱們進入夜總會的速率大概是每小時20人。如今隊伍中咱們前面還有20人, 也就意味着咱們還要等待大約一個小時。
深刻閱讀:Darrell Huff的《How To Lie With Statistics》;關鍵詞: 費米近似(Fermi estimate, Fermi problem)
這一章就一個小問題研究了4種不一樣的算法,重點強調這些算法的設計技術。 研究的這個小問題是一個很是常見的面試題:子數組之和的最大值。 若是以前沒有聽過,建議Google之。
深刻閱讀:Aho,Hopcroft和Ullman的《Data Structures and Algorithms》 Cormen,Leiserson,Rivest和Stein的《Introduction to Algorithms》
前面各章討論了提升程序效率的高層次方法:問題定義,系統結構, 算法設計及數據結構選擇。本章討論的則是低層次的方法:代碼調優。
代碼調優的最重要原理就是儘可能少用它。不成熟的優化是大量編程災害的根源。 它會危及程序的正確性,功能性以及可維護性。當效率很重要時, 第一步就是對系統進行性能監視,以肯定其運行時間的分佈情況。 效率問題能夠由多種方法來解決,只有在確信沒有更好的解決方案時才考慮進行代碼調優。
事實上,若是不是十分十分必要,不要去作代碼調優, 由於它會犧牲掉軟件的其餘許多性質。
so,just skip this chapter。
本章講述了節省空間的一些重要方法。
減小程序所需數據的存儲空間,通常有如下方法:
如下是節省代碼空間的幾種通用技術:
稀疏數據結構
假設咱們有一個200 x 200的矩陣(共40000個元素),裏面只有2000個元素有值, 其它的都爲0,示意圖以下:
顯然這是一個稀疏矩陣,直接用一個200 x 200 的二維數組來存儲這些數據會形成大量的空間浪費,共須要200x200x4B=160KB。 因此,咱們應該想辦法用另外一種形式來存儲這些數據。
方法一
使用數組表示全部的列,同時使用鏈表來表示給定列中的活躍元素。 以下圖所示:
該結構中,有200個指針(colhead)和2000條記錄(每條記錄是兩個整數和一個指針), 佔用空間是200x4B + 2000x12B = 24800B = 24.8KB, 比直接用二維數組存儲(160KB)要小不少。
方法二
咱們能夠開三個數組來保存這些數,以下圖所示:
firstincol是一個長度爲201的數組,對於第i列,在數組row中, 下標爲firstincol[i]到firstincol[i+1]-1對應的行元素非0, 其值存儲在相應的pointnum數組中。
好比對於上圖,在第0列中,元素值非0的行有3行,分別是row[0],row[1],row[2], 元素值是pointnum[0],pointnum[1],pointnum[2];在第1列中,元素值非0的行有2行, 分別是row[3],row[4],元素值是pointnum[3],pointnum[4]。依次類推。
該結構所須要的存儲空間爲2x2000x4B + 201x4B = 16804B = 16.8KB。 因爲row數組中的元素所有都小於200,因此每一個元素能夠用一個unsigned char來保存, firstincol數組中元素最大也就2000,因此能夠用一個short(或unsigned short)來保存, pointnum中的元素是一個4B的int, 最終所需空間變爲:2000x4B + 2000x1B + 201x2B = 10402B = 10.4KB。
深刻閱讀:Fred Brooks的《人月神話》
本章先簡單介紹了插入排序,而後着重講述快速排序。
插入排序
// 版本1 void InsertSort(int a[], int n) { for(int i=1; i<n; ++i) for(int j=i; j>0 && a[j-1]>a[j]; --j) swap(a[j-1], a[j]); } // 版本2 void InsertSort1(int a[], int n) { for(int i=1; i<n; ++i) { int t = a[i]; int j = i; for(; j>0 && a[j-1]>t; --j) a[j] = a[j-1]; a[j] = t; } }
快速排序
咱們在這裏規定:小於等於pivot的元素移到左邊,大於pivot的元素移到右邊。
實現1:單向移動版本
這個版本的關鍵是設置一快一慢兩個指針,慢指針左側都是小於等於pivot(包含慢指針所在位置), 慢指針到快指針之間的值是大於pivot,快指針右側的值是還未比較過的。示意圖以下:
小於等於pivot | 大於pivot | ? slow fast
快指針一次一步向前走,遇到大於pivot什麼也不作繼續向前走。遇到小於等於pivot的元素, 則慢指針slow向前走一步,而後交換快慢指針指向的元素。一次劃分結束後, 再遞歸對左右兩側的元素進行快排。代碼以下:
// 數組快排 void QSort(int a[], int head, int end) { if(a==NULL || head==end) return; int slow = head, fast = head + 1; int pivot = a[head]; while(fast != end) { if(a[fast] <= pivot) swap(a[++slow], a[fast]); ++fast; } swap(a[head], a[slow]); QSort(a, head, slow); QSort(a, slow+1, end); }
排序數組a只須要調用QSort(a, 0, n)便可。該思路一樣能夠很容易地在鏈表上實現:
// 單鏈錶快排 void qsort(Node *head, Node *end){ if(head==NULL || head==end) return; Node *slow = head, *fast = head->next; int pivot = head->data; while(fast != end){ if(fast->data <= pivot){ slow = slow->next; swap(slow->data, fast->data); } fast = fast->next; } swap(head->data, slow->data); qsort(head, slow); qsort(slow->next, end); }
排序頭指針爲head的單鏈表只需調用qsort(head, NULL)便可。
實現2:雙向移動版本
版本1能可以快速完成對隨機整數數組的排序,但若是數組有序, 或是數組中元素相同,快排的時間複雜度會退化成O(n2 ),性能變得很是差。
一種緩解方案是使用雙向移動版本的快排,它每次劃分也是使用兩個指針, 不過一個是從左向右移動,一個是從右向左移動,示意圖以下:
小於等於pivot | ? | 大於pivot i j
指針j不斷向左移動,直到遇到小於等於pivot,就交換指針i和j所指元素 (指針i一開始指向pivot);指針i不斷向右移動,直到遇到大於pivot的, 就交換指針i和j所指元素。pivot在這個過程當中,不斷地換來換去, 最終會停在分界線上,分界線左邊都是小於等於它的元素,右邊都是大於它的元素。 這樣就避免了最後還要交換一次pivot的操做,代碼也變得美觀許多。
int partition(int a[], int low, int high){ int pivot = a[low], i=low, j=high; while(i < j){ while(i<j && a[j]>pivot) --j; if(i < j) swap(a[i], a[j]); while(i<j && a[i]<=pivot) ++i; if(i < j) swap(a[i], a[j]); } return i; } void quicksort(int a[], int first, int last){ if(first<last){ int k = partition(a, first, last); quicksort(a, first, k-1); quicksort(a, k+1, last); } }
固然,若是對於partition函數,你若是以爲大循環內的兩個swap仍是作了些無用功的話, 也能夠把pivot的賦值放到最後一步,而不是在這個過程當中swap來swap去的。代碼以下:
int partition(int a[], int low, int high){ int pivot = a[low], i=low, j=high; while(i<j){ while(i<j && a[j]>pivot) --j; if(i<j) a[i++] = a[j]; while(i<j && a[i]<=pivot) ++i; if(i<j) a[j--] = a[i]; } a[i] = pivot; return i; }
若是數組基本有序,那隨機選擇pivot(而不像上面那樣選擇第一個作爲pivot) 會獲得更好的性能。在partition函數裏,咱們只須要在數組中隨機選一個元素, 而後將它和數組中第一個元素交換,後面的劃分代碼無需改變, 就能夠達到隨機選擇pivot的效果。
進一步優化
對於小數組,用插入排序之類的簡單方法來排序反而會更快,所以在快排中, 當數組長度小於某個值時,咱們就什麼也不作。對應到代碼中, 就是修改quicksort中的if條件:
if(first < last) 改成 if(last-first > cutoff)
其中cutoff是一個小整數。程序結束時,數組並非有序的, 而是被組合成一塊一塊隨機排列的值,而且知足這樣的條件: 某一塊中的元素小於它右邊任何塊中的元素。咱們必須經過另外一種排序算法對塊內進行排序。 因爲數組是幾乎有序的,所以插入排序比較適用。
這種方法結合了快排和插入排序,讓它們去作各自擅長的事情,每每比單純用快排要快。
深刻閱讀:Don Knuth的《The Art of Computer Programming, Volume 3: Sorting and Searching》;Robert Sedgewick的《Algorithms》; 《Algorithms in C》,《Algorithms in C++》,《Algorithms in Java》。
本章講述了一個小的隨機抽樣問題,並用不一樣的方法來解決它。
問題:對於整數m和n,其中m<n,輸出0~n-1範圍內m個隨機整數的有序列表
, 不容許重複。
好比m=3, n=5,那麼一種可能輸出是0,2,3(要求有序)。實現1來自Knuth的TAOCP, 時間複雜度O(n):
void GenKnuth(int m, int n) { for(int i=0; i<n; ++i) { if((bigrand()%(n-i)) < m) { cout<<i<<endl; --m; } } }
其中,bigrand()的做用是返回一個很大的隨機整數。
實現2:在一個初始爲空的集合裏面插入隨機整數,直到個數足夠。代碼以下:
void GenSets(int m, int n) { set<int> s; while(s.size() < m) s.insert(bigrand() % n); set<int>::iterator i; for(i=s.begin(); i!=s.end(); ++i) cout<<*i<<endl; }
實現3:把包含整數0~n-1的數組順序打亂,而後把前m個元素排序輸出。 該方法的性能一般不如Knuth的算法。代碼以下:
void GenShuf(int m, int n) { int x[n]; for(int i=0; i<n; ++i) x[i] = i; for(int i=0; i<m; ++i) { int j = randint(i, n-1); swap(x[i], x[j]); } sort(x, x+m); for(int i=0; i<m; ++i) cout<<x[i]<<endl; }
深刻閱讀:Don Knuth的《The Art of Computer Programming, Volume 2: Seminumerical Algorithms》
本章詳細研究這樣一個搜索問題:在沒有其餘相關數據的狀況下,如何存儲一組整數? 爲些介紹了5種數據結構:有序數組,有序鏈表,二叉搜索樹,箱,位向量。
其中,二叉搜索樹應該熟練掌握,如下是一種實現:
struct Node { int data; Node *lchild, *rchild, *parent; Node(): lchild(NULL), rchild(NULL), parent(NULL) { } }; class BST { private: static const int kMax = 1000; Node *root_, *parent_, nodes_[kMax]; int size_; private: Node* minimum(Node* node); Node* maximum(Node* node); Node* successor(Node* node); Node* predecessor(Node* node); void Insert(Node* &node, int x); void InorderTraver(Node* node); Node* Find(Node* node, int x); public: BST(): root_(NULL), parent_(NULL), size_(0) { memset(nodes_, '\0', sizeof(nodes_)); } void Insert(int x); void InorderTraver(); Node* Find(int x); void Remove(Node* z); }; Node* BST::minimum(Node* node) { if(node == NULL) return NULL; while(node->lchild) node = node->lchild; return node; } Node* BST::maximum(Node* node) { if(node == NULL) return NULL; while(node->rchild) node = node->rchild; return node; } Node* BST