表驅動法
前注:希望我的讀書筆記能帶你快速翻過20頁的書,歡迎討論http://www.cnblogs.com/jerry19880126
這里談談一些學習方法吧,看了二十多年的書的,發現不同的書,有不同的看法:小說類的讀起來最輕松,只要跟着作者走就行了,會寫書的作者應該能呈現一些劇情的細節,讀者腦海中會形成相應的影像;散文類的讀起來最值得細細品味,比如讀者里面的散文,不是很長,但讀起來會有一種小資情調;技術類的讀起來最吃力了,但這也是自己謀生的必經之路,所以再覺得難,也要啃下去,但技術這個東西,只要肯下功夫,學通之后,就會有一種難以名狀的成就感,這種快樂得到的越多,你的成長就越大。
千萬不要像讀小說或讀散文一樣看技術書,技術書是需要思考的,更重要的是需要實踐,讀多了也就發現雖然內容各不相同,但寫作的規律總是大差不差的。給定一種技術,你覺得作者會怎樣介紹?我覺得分成三步:第一步說“是什么”,第二步說“為什么”,第三步說“怎么做”。就以本章“表驅動法”為例,《代碼大全》作者先是回答了“什么是表驅動”
表驅動法是一種編程模式,從表里面查詢信息而不使用邏輯語句(如if或switch)
然后舉了一個不使用表驅動的反例,說明不這樣做會使代碼可讀性大大下降,這就回答了第二步。至於第三步,作者花了大量篇幅去介紹,你所看到的大部分內容實際上是第三步。
讀書筆記也打算用三步去介紹這個表驅動法。第一步是定義,前面已經抄了書上的一句話,已經說的很明白了,就是用查表來代替if語句或switch語句。第二步是原因,可以舉個例子,如果有這樣一個函數int getTotalDayInMonth(int month),它輸入一個月份,然后返回這個月份的總天數(不考慮潤年,二月以28天計),比如輸入5,返回的是31,因為5月里共有31天。
一種寫法是這樣的:
1 // 獲得某一月中的總天數,monthIndex從 1 開始
2 int getTotalDayInMonth(int month)
3 {
4 int totalDay = 0;
5 if(month == 2)
6 {
7 totalDay = 28;
8 }
9 else if(month == 4 || month == 6 || month == 9 || month == 11)
10 {
11 totalDay = 30;
12 }
13 else
14 {
15 totalDay = 31;
16 }
17 return totalDay;
18 }
在這種寫法里使用了邏輯if判斷,里面有很多憑空出現的數字,比如2,4,11等,這些稱為magic number的數字出現在程序里是很不好的,因為不好修改與擴充,比如說上面的代碼適合與地球上的計算,現在要你去改一個火星上的情況(火星公轉時長不同於地球,所以每個月划分的天數也會不同),你可能就要去改這些magic number了,更復雜的,你可能要因為更多的天數可能性(假定火星7月只有17天,而9月有21天),而添加更多的if分支。但如果像下面這樣使用一個表,就使程序簡單多了:
1 const int totalDayTable[12] =
2 {
3 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
4 };
5
6 // 獲得某一月中的總天數,monthIndex從 1 開始
7 int getTotalDayInMonthFromTable(int month)
8 {
9 return totalDayTable[month - 1];
10 }
怎么樣,把用表與不用表的代碼對比一下,是不是要簡化許多呢?更有意義的是,如果某個天數變了,可以直接修改或擴充totalDayTable就行了,至於函數則分毫不用修改。
好吧,寫到這里,應該已經解決了第二步了,就是使用表驅動可以提高源程序的可讀性,使之更簡潔而且更容易修改與擴充。
下面走到第三步,也是本章中花篇幅最大的一步了——如何去用表驅動來解決問題。
書上說查表方法可以細分為三種:
(1) 直接訪問
(2) 索引訪問
(3) 階梯訪問
直接訪問是最簡單的,查表本質其實就是去索引“鍵”來獲得“值”,有點像獲得數組值一樣,給定下標index,然后matrix[index]就獲得數組在相應下標處的數值,再如前面的return totalDayTable[month - 1]就是直接用month-1來作為鍵的,而值可以直接通過查表來獲得。
現在有個問題,萬一“鍵”是不能直接用的呢?比如我想設計一個幼兒學習動物的軟件,若小孩想查找牛的信息,這時屏幕上會打印出牛的特性,而若小孩想查找狗的信息,則屏幕上會打印出狗的信息。顯然,這里的鍵是動物名,而值是相應的描述。下標必須是整數,但動物名是string,怎么辦呢?數據結構中的hash表當然可以了,它就是計算string的hash值,通過hash值來索引表格的,但在這里我們不打算用hash值,而是由程序員自行設計string到int的映射,怎么做呢?很簡單啊,自己做個菜單唄,讓用戶只能選擇相應的數字,這樣“鍵”就成int了哈。
代碼如下:
1 class Animal
2 {
3 public:
4 virtual void print() = 0;
5 };
6
7 class Dog: public Animal
8 {
9 public:
10 void print()
11 {
12 cout << "This is Dog..." << endl;
13 }
14 };
15
16 class Cat: public Animal
17 {
18 public:
19 void print()
20 {
21 cout << "This is Cat..." << endl;
22 }
23 };
24
25 class Cow: public Animal
26 {
27 public:
28 void print()
29 {
30 cout << "This is Cow..." << endl;
31 }
32 };
33
34 Animal* animalTable[] = {
35 new Dog, new Cat, new Cow
36 };
37
38 int main()
39 {
40 cout << "想知道哪種動物的描述?" << endl;
41 cout << "1. 狗" << endl << "2. 貓" << endl << "3. 奶牛" << endl << endl;
42 int choiceIndex;
43 cout << "我選擇:";
44 cin >> choiceIndex;
45 assert(choiceIndex >= 1 && choiceIndex <= 3);
46 animalTable[choiceIndex - 1]->print();
47 }
運行結果為:

這里用了C++的多態性,根據不同的實體對象能調用相應的print函數,但我更想表達的是表驅動法的應用,請把目光放在表animalTable上吧,這樣的排序,就是將Dog映射成數字1,Cat映射成數字2了。這是人為的映射,但用途更廣的是hash映射,不知道的同學去看看數據結構吧,hash表可以快查找的利器,面試中常常被問到。
第二種表驅動法是索引訪問表,它適用於這樣的情況,假設你經營一家商店,有100種商品,每種商品都有一個ID號,但很多商品的描述都差不多,所以只有30條不同的描述,現在的問題是建立商品與商品描述的表,如何建立?還是同上面做法來一一對應嗎?那樣描述會擴充到100的,會有70個描述是重復的!如何解決這個問題呢?方法是建立一個100長的索引,然后這些索引指向相應的描述,注意不同的索引可以指向相同的描述,這樣就解決了表數據冗余的問題啦。
第三種表驅動法是階梯訪問表,它適用於數據不是一個固定的值,而是一個范圍的問題,比如將百分制成績轉成五級分制(我們用的優、良、中、合格、不合格,西方用的A、B、C、D和F),假定轉換關系是當成績在90-100區間,判為A,成績在80-90區間,判為B,成績在70-80區間,判為C,成績在60-70區間,判為D,成績在60以下,判為F(failure)。現在的問題是,怎么用表格對付這個范圍問題?一種笨笨的方法是申請一個100長的表,然后在這個表中填充相應的等級就行了,但這樣太浪費空間了,有沒有更好的方法?
在《代碼大全》上是用表格記錄范圍上限的,但其實用下限也是可以的,我就嘗試用下限做了下(A級的下限是90,B級的下限是80…):
1 //階梯訪問表,順序查找
2 const char gradeTable[] = {
3 'A', 'B', 'C', 'D', 'F'
4 };
5
6 const int downLimit[] = {
7 90, 80, 70, 60
8 };
9
10 int main()
11 {
12 int score = 87;
13 int gradeLevel = 0;
14 while(gradeTable[gradeLevel] != 'F')
15 {
16 if(score < downLimit[gradeLevel])
17 {
18 ++ gradeLevel;
19 }
20 else
21 {
22 break;
23 }
24 }
25 cout << "等級為 " << gradeTable[gradeLevel] << endl;
26 return 0;
27 }
運行結果如下:

gradeLevel相當於表指針,在程序中就是通過調整這個表指針來使之指向正確的位置的。
程序還有優化的地方,注意到這些下限是有順序(降序),那可以用二分查找啊,程序如下:
1 //階梯訪問表,二分查找 2 const char gradeTable[] = { 3 'A', 'B', 'C', 'D', 'F' 4 }; 5 6 const int DONWLIMIT_LENGTH = 4; 7 8 const int downLimit[] = { 9 90, 80, 70, 60 10 }; 11 12 13 int BinarySearch(int score) 14 { 15 int low = 0; 16 int high = DONWLIMIT_LENGTH - 1; //downLimit的最大的Index 17 while(low <= high) 18 { 19 int mid = (low + high) / 2; 20 if(score < downLimit[mid]) 21 { 22 low = mid + 1; 23 } 24 else if(score > downLimit[mid]) 25 { 26 high = mid - 1; 27 } 28 else 29 { 30 return mid; 31 } 32 } 33 return low; 34 } 35 36 int main() 37 { 38 int score = 87; 39 int gradeLevel = BinarySearch(score); 40 cout << "等級為 " << gradeTable[gradeLevel] << endl; 41 return 0; 42 }
怎么樣,用表驅動法不僅避免了大量的if或switch分支,還應用上了二分查找法,使得查找復雜度由O(N)下降到了O(logN)!
<end>
