讀書筆記_代碼大全_第18章_表驅動法


表驅動法

前注:希望我的讀書筆記能帶你快速翻過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>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM