表驱动法
前注:希望我的读书笔记能带你快速翻过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)!