• 沒有找到結果。

表驱动方法

在文檔中 Code Complete 代码大全 (頁 191-200)

第十二章 复杂数据类型

12.2 表驱动方法

表是几乎所有数据结构课本都要讨论的非常有用的数据结构。表驱动方法出于特定的目的 来使用表,下面将对此进行讨论。

程序员们经常谈到“表驱动”方法,但是课本中却从未提到过什么是“表驱动”方法。表 驱动方法是一种使你可以在表中查找信息,而不必用逻辑语句(if 或 case)来把它们找出来的方 法。事实上,任何信息都可以通过表来挑选。在简单的情况下,逻辑语句往往更简单而且更直 接。但随着逻辑链的复杂,表就变得越来越富于吸引力了。

例如,如果你想把字符排序成字母、逗号和数字,将很可能采用如下所示的复杂逻辑链(用 P ascal 写成):

if (( 'a' <= InputChar ) and ( InputChar <= 'z' )) or (( 'A' <= InputChar ) and( InputChar <= 'Z' )) then begin

CharType := Letter end

else if ( InputChar = '' ) or ( InputChar = ',' ) or

( InputChar = '.' ) or ( InputChar = '!' ) or ( InputChar = '(' ) or ( InputChar = ')' ) or ( InputChar = ':' ) or ( InputChar = ';' ) or ( InputChar = '?') or ( InputChar = '-' ) then

begin

CharType := Punctuation end

else if ( '0' <= InputChar and InputChar <= '9' ) then begin

CharType := Digit End;

而如果你使用的是查询表的话,就可以把每个字符类型都存储在由字符类型来存储的数组 中,那么上例中复杂的逻辑链就可以简化成下面这个样子:

CharType := CharTypeTable[InputChar];

在这个程序段中,假定CharTypeTable 已经被建立了,把程序的内容建立在表而不是 if 判断 的基础上。如果使用是适当(如上例)的话,表方法要比复杂的逻辑简单得多,而且也更易于改动,

效率也更高。

12.2.1 表驱动方法的通常问题

使用表驱动方法时,将不得不说明两个问题:

首先,你不得不说明如何寻找表中的入口,你可以使用某些数据来直接存取表。比如,你 需要按月份把数据进行排序,那么进入月份表是非常容易的,你可以使用以 1 到 12 为下标的数 组来实现它。

而若用其它数据来直接查找表的入口则显得有些力不从心了,比如需要把社会保险号码 进行排序时,就不能用社会保险号来直接进入表,除非你把999,999,999 个入口全部存在表中,这 时你将被迫采用比较复杂的方法。下面是查找表的入口的几种方法:

・ 直接存取

・ 变址存取

・ 阶梯存取

后面将对上述每种方法都进行详细的论述。

在使用表驱动方法时需要说明的另一个问题是,你将在表中存储些什么。在某些情况下,

表查寻的结果是数据。如果是这种情况,你可以把数据存储在表中;在其它情况下,表查寻的 结果是动作。在这种情况下,你可以把描述这一动作的代码存储在表中。在某些语言中,也可以 把实现这一动作的子程序的调用存储在表中。但不论是哪种情况,表都已经变得很复杂了。

12.2.2 直接存取

与其它查寻表一样,直接存取表是用来代替比它更复杂的逻辑控制结构的,之所以称其为

“直接存取”是因为用这种方法时,你不必为了找到你想要的信息而在表中绕来绕去。 正如图 12-l 所表示的那样,你可以直接找出你想要的入口。

12-1 直接存取

示例 1:一个月中的天数

假设你需要一个可以返回每个月中天数的函数(为简单起见不考虑闰年),一个比较笨的

方法是写一个大的if 语句:

IF Month=1 THEN Days=3l

ELSEIF Month=2 THEN Days=28 ELSEIF Month=3 THEN Days=31 ELSEIF Month=4 THEN Days=30 ELSEIF Month=5 THEN Days=31 ELSEIF Month=6 THEN Days=30 ELSEIF Month=7 THEN Days=31 ELSEIF Month=8 THEN Days=31 ELSEIF Month=9 THEN Days=30 ELSEIF Month=10 THEN Days=31 ELSEIF Month=11 THEN Days=30 ELSEIF Month=12 THEN Days=31 ENDIF

更简单,效率更高也更容易改动的方法是,把这些数据放在一个表中。在 Basic 中,必须首先 建立表:

′ INITIALIZE TABLE OF "Days Per Month" DATA

DATA 31,28,31,30,31,30,31,31,30,31,30,31 DIM DaysPerMonth(I)

FOR I=1 TO 12

READ DaysPerMonth(I) NEXT I

现在,有了这个表,就可以用一个简单的数组存取来代替上面那段复杂而又臃肿的if 语 句了。

Days=DaysPerMonth(Month)

即使现在你想把闰年也考虑进来,程序仍然是非常简单的:

Days=DaysPerMonth(Month,IsLeapYear)

显然,如果再用 if 语句的程序中计算闰年的话,那么程序将不知有多么复杂。

确定一个月中的天数是一个比较简单的例子,因为你可以用变量Month 来查寻表的入口。

一般来说,可以采用控制着一大串if 语句的数据来直接存取一个表。

示例 2:保险费用

假设你要编写一个计算医疗保险费用的程序,其中保险费用是随着性别、年龄、婚姻状况 和是否吸烟而变化的。如果你用逻辑控制结构做这些工作的话,它应该是与下面这个 Pascal 程 序段类似的:

if ( Sex = Female ) then begin

if ( MaritalStatus = Single ) then begin

if ( SmokingStatus = NonSmoking ) then begin if ( Age < 18 ) then

Rate := 40.00

else if ( Age = 18 ) then Rate := 42.50

else if ( Age = 19 ) then Rate := 45.00

else if ( Age > 65) then Rate := 150.00 end

else begin { SmokingStatus = Smoking}

if ( Age < 18 ) then Rate := 44.00

else if ( Age = 18 ) then Rate := 47.00

else if ( Age = 19 ) then Rate := 50.00

else if ( Age > 65 ) then Rate := 200.00 end

else{ Marital Status = Married }

end;{ if Sex …}

在上例中,只考虑了是否吸烟和性别、年龄而没有考虑婚姻状况,也没有考虑 18 岁到 65 岁 之间的大部分年龄,但是其复杂程度已经相当惊人了。你可以想象一下,如果把影响保险率的所 有因素都考虑进来的话,它将有多么复杂。

你或许会问:“为什么要对每一个年龄都进行判断而不把保险费用放入年龄数组呢?”问得 好,如果把保险费用放入年龄数组的话,将极大地改进上面的程序。

不过,如果把保险费用放入所有影响因素的数组而不仅仅是年龄数组的话,将会使程序更简 单,以下是Pascal 中是如何说明数组的:

type

Smoking_t = (Smoking,Nonsmoking);

Sex_t = (Male,Female);

Marital_t = (Single, Married);

Age_t = 1..100;

var

RateTable=array[Smoking_t,Sex_t,Marital_t,Age_t];

在Pascal 中使用枚举类型的一大特点是你可以用类似 smoking_t 的参数来说明数组,而编 译程序会自动识别出有两种抽烟状态从而知道数组中应该有两个元素。

定义好数组之后,你就需要确定如何把数据放进去。 你可以用赋值语句, 从磁盘中的一个 文件中读入数据计算出数据或其它任何合适的方法来做到这一点。当你建立好数据之后, 便做

好了计算保险费用的一切工作。现在就可以用下面这个简单的语句来代替前面那个复杂的逻辑 结构了:

Rate := RateTable[SmokingStatus, Sex, MaritalStatus, Age];

这种方法的好处是可以用表查寻来代替复杂的逻辑控制,而表查寻的方法往往具有更好的 可读性并且修改容易,同时还具有占用空间少和可读性强的优点。

示例 3: 灵活信息格式

你还可以用表来描述由于变化太快而无法用代码来描述的逻辑,通过上面的几个例子,我们 已经知道某些问题是可以用if 语句来实现的,虽然有时这种方法显得很拙劣,但毕竟是可用的。

然而在某些情况下,有些非常复杂的数据是无法用 if 语句来描述的。

如果你认为对直接表存取方法已经很熟悉了的话,可以跳过下一个例子,因为它只是比前几 个例子稍微复杂一些。

假设你要编写一个打印存储在某一文件中信息的子程序。文件中通常含有500 条信息,信息 共有大约 20 种。这些信息来自于一个给出自身位置和水温的浮标。

每一条信息都有几个域,其开头都是一个表示该信息种类的识别标志。图 12-2 表示了信息 的存储方式:

12-2 无特定次序存储的信息,其中每个信息有一个信息识别标志

由于信息格式是由用户决定的,因而是反复无常的,你也不能寄希望于用户来把格式稳定 下来。图 12-3 表示出了几条详细的信息。

如果你用的是逻辑控制方法,那么你就不得不读取每一条信息,检查它的识别标志, 然后 调用相应子程序来读取,解释和打印该种信息。 如果共有 20 种信息的话, 那么你就需要设计

2 0 种 子 程 序 , 同 时 还 要 有 许 多 低 层 次 子 程 序 来 支 持 它 们 。 比 如 , 你 将 不 得 不 用 PrintBuoyTemparatureMessage()子程序来专门打印浮标温度信息。

12-3 各种信息格式

当任何信息格式变动时,你都不得不改动打印该种信息子程序的内部逻辑。在上图表示的信 息 细 节 中 , 如 果 平 均 温 度 域 从 浮 点 型 变 成 其 它 类 型 , 那 么 你 就 不 得 不 改 动 PrintBuoyTemperatureMessage()中的逻辑结构。

而如果用表驱动方法的话,就可以把每种信息的格式放在一个表中而不必把它们硬性编码 在程序逻辑结构中,这使得编程和修改都变得很容易,而且使用的代码也要少得多。要使用这种 方法,你首先必须列出信息的种类和域的类型。在 Pascal 中,可以像下面这样来定义域的类型:

var

FiledTypes = ( FloatingPoint, Integer, CharString, TimeOfDay, SingleFlag, BitField );

这样你可以用曲指可数的几个打印基本数据类型(整型、浮点型、字符串等)的子程序来代 替专门打印某种信息的 20 个子程序。你可以在表中描述每种信息的内容(包括每个域的名称),

然后根据表中的描述来解释每一条信息。用 Pascal 编写的描述某种信息表入口可能是这样的:

Message[Type1].NumFields := 5 ;

Message[Type1].MessageName := ′Buoy Temperature Message′ ; Message[Type1].FieldType[1] := FloatingPoint;

Message[Type1].FieldLabel[l] := ′Average Temperature′ ; Message[Type1].FieldType[2] := FloatingPoint ;

Message[Type1].FieldLable[2] := ′Temperature Range′ ;

Message[Type1].FieldType[3] := Integer ;

Message[Type1].FieldLabel[3] := ′Number of Samples′ ; Message[Type1].FieldType[4] := CharString ;

Message[Type1].FieldLabel[4] := ′Location′ ; Message[Type1].FieldType[5] := TimeOfDay ;

Message[Type1].FieldLabel[5] := ′Time of Measurement′ ;

现在,你已经用存储在数据中的信息取代了存储在程序逻辑结构中的信息,而数据要比逻辑 结构灵活得多,当一个信息格式变化时,很容易改动相应的数据来适应它。如果不得不在其中加 入一种新信息,那你只需在 Messaae 数组中添加一个元素就可以了。

这时读取信息的代码也会变得简单多了。在以逻辑为基础的方法中,信息读取子程序要用 一个循环来读取每条信息,再根据信息识别标志判别出它的种类,然后再从 20 个打印子程序中 调用相应的子程序来打印它。下面是以逻辑为基础方法的伪代码:

While more message to read Read a message header

Decode the message ID from the message header If the message header is type 1 then

Print a type 1 message

Else if the message header is type 2 then Print a type 2 message

………

Else if the message header is type 19 then Print a type 19 message

Else if the message header is type 20 then Print a type 20 message

上段伪代码事实上是省略的,因为其中只表示了 20 种信息中的几种。在低于这个层次的 逻辑中,20 种信息中的每一处都要求专门的子程序来打印它们。这些子程序也可以用伪代码 来表示。下面是表示浮标温度打印子程序的伪代码:

Print ′ Buoy Temperature Message′

Read a floating-point value Print ′ Average Temperature′

Print the floating-point value

Read a floating-point value Print ′ Temperature Range′

Print the floating-point value

Read an integer value Print ′ Number of Samples′

Print the integer value

Read a character string Print ′ Location′

Print the character string

Read a time of day

Print ′ Time of Measurement′

Print the time of day

而表驱动方法则要比这简单得多。信息读取子程序首先利用循环来读取每条信息的开头,

而表驱动方法则要比这简单得多。信息读取子程序首先利用循环来读取每条信息的开头,

在文檔中 Code Complete 代码大全 (頁 191-200)