19
! " # $ ! % & ' ( " ) * % ( # + * & , $ - , . % !/ !0字谜示例 ! 1*%*2%3 45% 436(* 7.89,+3'% &:87*9;('(' 7 <*=* ! >&'3 .?.'%'+ 7 "" 在这一章! 我们阐述本书的目的和目标并简要复习离散数学以及程序设计的一些概念" 们将要# ! 看到程序对于合理的大量输入的运行性能与其在适量输入下运行性能的同等重要性" ! 概括为本书其余部分所需要的基本的数学基础" ! 简要复习递归" ! 概括用于本书的 <*=* 语言的某些重要特点" !"!#本书讨论的内容 设有一组 !个数而要确定其中第 " 个最大者! 我们称之为选择问题$ (6965%'+ 7@3+ A96: %" 多数学习过一两门程序设计课程的学生写一个解决这种问题的程序不会有什么困难" & 明显 ' 解决方法是相当多的" 该问题的一种解法就是将这 !个数读进一个数组中! 再通过某种简单的算法! 比如冒泡排 序法! 以递减顺序将数组排序! 然后返回位置 " 上的元素" 稍微好一点的算法可以先把前 " 个元素读入数组并$ 以递减的顺序% 对其排序" 接着! 将剩 下的元素再逐个读入" 当新元素被读到时! 如果它小于数组中的第 " 个元素则忽略之! 否则就 将其放到数组中正确的位置上! 同时将数组中的一个元素挤出数组" 当算法终止时! 位于第 " 个位置上的元素作为答案返回" 这两种算法编码都很简单! 建议读者试一试" 此时我们自然要问# 哪个算法更好( 哪个算 法更重要( 还是两个算法都足够好( 使用三千万个元素的随机文件和 " B!C DDD DDD 进行模拟将 发现! 两个算法在合理的时间量内均不能结束) 每种算法都需要计算机处理若干天才能算完 $ 虽然最后还是给出了正确的答案%" 在第 E 章将讨论另一种算法! 该算法将在一秒钟左右给出 问题的解" 因此! 虽然我们提出的两个算法都能算出结果! 但是它们不能被认为是好的算法! 因为对于第三种算法能够在合理的时间内处理的输入数据量而言! 这两种算法是完全不切实 际的" 第二个问题是解决一个流行的字谜" 输入是由一些字 母构成的一个二维数组以及一组单词组成" 目标是要找出 字谜中的单词! 这些单词可能是水平* 垂直或沿对角线上 任何方向放置的" 作为例子! !/ ! 所示的字谜由单词 % &'( * %)+ * -*% % &*% 组成" 单词 % &'( 从第一行第一列的位 置即$! ! ! % 处开始并延伸至$! ! $ %) 单词 %)+ $! ! ! % $# ! ! %) -*% $ $ ! ! % $ " ! # %) % &*% 则从 $ $ ! $ % $! ! ! %" 现在至少也有两种直观的算法来求解这个问题" 对单词表中的每个单词! 我们检查每一个 有序三元组$ * * 方向% 验证是否有单词存在" 这需要大量嵌套的 !"#循环! 但它基本上是 直观的算法" 也可以这样! 对于每一个尚未越出谜板边缘的有序四元组$ * * 方向* 字符数% 我们可 以测试是否所指的单词在单词表中" 这也导致使用大量嵌套的 !"#循环" 如果在任意单词中 的最大字符数已知! 那么该算法有可能节省一些时间" !

3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

  • Upload
    others

  • View
    2

  • Download
    0

Embed Size (px)

Citation preview

Page 1: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

书书书

! " # $

! % & ' (

" ) * % (

# + * & ,

$ - , . %

图 !/!0字谜示例

第 ! 章1*%*2%345%436(*7. 89,+3'%&:87*9;('('7 <*=*! >&'3. ?.'%'+7

引""论

在这一章! 我们阐述本书的目的和目标并简要复习离散数学以及程序设计的一些概念" 我

们将要#

! 看到程序对于合理的大量输入的运行性能与其在适量输入下运行性能的同等重要性"

! 概括为本书其余部分所需要的基本的数学基础"

! 简要复习递归"

! 概括用于本书的<*=*语言的某些重要特点"

!"!#本书讨论的内容

设有一组!个数而要确定其中第"个最大者! 我们称之为选择问题$(6965%'+7 @3+A96:%" 大

多数学习过一两门程序设计课程的学生写一个解决这种问题的程序不会有什么困难" &明显

的' 解决方法是相当多的"

该问题的一种解法就是将这!个数读进一个数组中! 再通过某种简单的算法! 比如冒泡排

序法! 以递减顺序将数组排序! 然后返回位置"上的元素"

稍微好一点的算法可以先把前"个元素读入数组并$以递减的顺序%对其排序" 接着! 将剩

下的元素再逐个读入" 当新元素被读到时! 如果它小于数组中的第 "个元素则忽略之! 否则就

将其放到数组中正确的位置上! 同时将数组中的一个元素挤出数组" 当算法终止时! 位于第 "

个位置上的元素作为答案返回"

这两种算法编码都很简单! 建议读者试一试" 此时我们自然要问# 哪个算法更好( 哪个算

法更重要( 还是两个算法都足够好( 使用三千万个元素的随机文件和"B!C DDD DDD 进行模拟将

发现! 两个算法在合理的时间量内均不能结束) 每种算法都需要计算机处理若干天才能算完

$虽然最后还是给出了正确的答案%" 在第 E 章将讨论另一种算法! 该算法将在一秒钟左右给出

问题的解" 因此! 虽然我们提出的两个算法都能算出结果! 但是它们不能被认为是好的算法!

因为对于第三种算法能够在合理的时间内处理的输入数据量而言! 这两种算法是完全不切实

际的"

第二个问题是解决一个流行的字谜" 输入是由一些字

母构成的一个二维数组以及一组单词组成" 目标是要找出

字谜中的单词! 这些单词可能是水平* 垂直或沿对角线上

任何方向放置的" 作为例子! 图 !/! 所示的字谜由单词

%&'(* %)+* -*%和%&*%组成" 单词%&'(从第一行第一列的位

置即$!! !%处开始并延伸至$!! $%) 单词%)+从$!! !%到

$#! !%) -*%从$$! !%到$"! #%) 而 %&*%则从 $$! $%到

$!! !%"

现在至少也有两种直观的算法来求解这个问题" 对单词表中的每个单词! 我们检查每一个

有序三元组$行* 列* 方向%验证是否有单词存在" 这需要大量嵌套的 !"#循环! 但它基本上是

直观的算法"

也可以这样! 对于每一个尚未越出谜板边缘的有序四元组$行* 列* 方向* 字符数%我们可

以测试是否所指的单词在单词表中" 这也导致使用大量嵌套的 !"#循环" 如果在任意单词中

的最大字符数已知! 那么该算法有可能节省一些时间"

!

Page 2: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏" 这

些字谜通常有 !F 行 !F 列以及 $D 个左右的单词" 然而! 假设我们把字谜变成为只给字谜板

$@4GG96A+*3.%而单词表基本上是一本英语词典! 则上面提出的两种解法均需要相当长的时间来

解决这个问题! 从而这两种方法都是不可接受的" 不过! 这样的问题还是有可能在数秒内解决

的! 甚至单词表可以很大"

在许多问题当中! 一个重要的观念是# 写出一个工作程序并不够" 如果这个程序在巨大的

数据集上运行! 那么运行时间就变成了重要的问题" 我们将在本书看到对于大量的输入如何估

计程序的运行时间! 尤其是如何在尚未具体编码的情况下比较两个程序的运行时间" 我们还将

看到彻底改进程序速度以及确定程序瓶颈的方法" 这些方法将使我们能够发现需要我们集中精

力努力优化的那些代码段"

!"$#数学知识复习

本节列出一些需要记忆或是能够推导出的基本公式! 并从推导过程复习基本的证明方法"

!"$"!#指数#

$

#

%

B#

$H%

#

$

#

%

B#

$I%

$#

$

%

%

B#

$%

#

!

H#

!

B"#

!

"

#

"!

"

!

H"

!

B"

!H!

!"$"$#对数在计算机科学中! 除非有特别的声明! 否则所有的对数都是以 " 为底的"

定义!"!# #

$

B%当且仅当9+,

#

%B$"

由该定义可以得到几个方便的等式"

定理!"!

9+,

$

%B

9+,

&

%

9+,

&

$

) $! %! &JD! $

"

!

证明!

令#B9+,

&

%! 'B9+,

&

$! 以及(B9+,

$

%" 此时由对数的定义! &

#

B%! &

'

B$以及$

(

B%!联

合这三个等式则产生$&

'

%

(

B&

#

B%" 因此! #B'(! 这意味着(B#)'! 定理得证"

#

定理!"$

9+,$%B9+,$H9+,%) $! %JD

证明!

令#B9+,%! 'B9+,$! 以及(B9+,$%" 此时由于假设默认的底为 "! "

#

B$! "

'

B%! 及 "

(

B

$%! 联合最后的三个等式则有 "

#

"

'

B"

(

B$%" 因此#H'B(! 这就证明了该定理"

#

其他一些有用的公式如下! 它们都能够用类似的方法推导"

9+,$)%B9+,$I9+,%

9+,$$

%

% B%9+,$

9+,#K#对所有的#JD 成立

9+,! BD! 9+," B!! 9+,!D"$ B!D! 9+,!D$LCEF B"D

!"$"%#级数最容易记忆的公式是

$

!

*+D

"

*

+"

!,!

-!

.

第!章0

"

#

Page 3: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

$

!

*+D

$

*

+

$

!,!

-!

$-!

在第二个公式中! 如果 D K$K!! 则

$

!

*+D

$

*

%

!

! -$

当!趋于M

时该和趋向于 !N$! I$%! 这些公式是 &几何级数' 公式"

我们可以用下面的方法推导关于$

M

*+D

$

*

$D /$/!% 的公式" 令 0是其和" 此时

0 B! H$H$

"

H$

#

H$

$

H$

C

H+

于是

$0 B$H$

"

H$

#

H$

$

H$

C

H+

如果我们将这两个方程相减$这种运算只允许对收敛级数进行%!等号右边所有的项相消!只留下 !#

0 I$0 B!

0 B

!

! I$

可以用相同的方法计算$

M

*+!

*)"

*

! 它是一个经常出现的和" 我们写成

0 B

!

"

H

"

"

"

H

#

"

#

H

$

"

$

H

C

"

C

H+

用 " 乘之得到

"0 B! H

"

"

H

#

"

"

H

$

"

#

H

C

"

$

H

F

"

C

H+

将这两个方程相减得到

0 B! H

!

"

H

!

"

"

H

!

"

#

H

!

"

$

H

!

"

C

H+

因此! 0 B""

分析中另一种常用类型的级数是算术级数" 任何这样的级数都可以从基本公式计算其值"

$

!

*+!

*+

!$!,!%

"

&

!

"

"

例如! 为求出和 " HC HL H+ H$#"I!%!将其改写为 #$! H" H# H+ H"% I$! H! H! H+ H!%!显

然! 它就是 #"$"H!%)" I"" 另一种记忆的方法则是将第一项与最后一项相加$和为 #"H!%! 第

二项与倒数第二项相加$和也是 #"H!%! 等等" 由于有 ")" 个这样的数对! 因此总和就是

"$#"H!%)"! 这与前面的答案相同"

现在介绍下面两个公式! 不过它们就没有那么常见了"

$

!

*+!

*

"

+

!$!,!%$"!,!%

F

&

!

#

#

$

!

*+!

*

"

&

!

",!

",!

0"

"

-!

当"BI! 时! 后一个公式不成立" 此时我们需要下面的公式! 这个公式在计算机科学中的

使用要远比在数学其他科目中使用得多" 数 1

!

叫作调和数! 其和叫作调和和" 下面近似式中

的误差趋向于!&

DOCE E"! CFF! 称为欧拉常数$?4963P(5+7(%*7%%"

1

!

+

$

!

*+!

!

*

&

9+,

6

!

以下两个公式只不过是一般的代数运算#

20引00论

$

Page 4: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

$

!

*+!

3$!% +!3$!%

$

!

*+4

D

3$*% +

$

!

*+!

3$*% -

$

4

D

-!

*+!

3$*%

!"$"&#模运算如果!整除$I%! 那么就说$与%模!同余! 记为$

'

%$:+. !%" 直观地看! 这意味着无

论是$还是%被!去除! 所得余数都是相同的" 于是! L!

'

F!

'

!$:+. !D%" 如同等号的情形

一样! 若$

'

%$:+. !%! 则$H&

'

%H&$:+. !%以及$5

'

%5$:+. !%"

有许多定理适用模运算! 其中有些特别需要用到数论来证明" 我们将尽量少使用模运算!

这样! 前面的一些定理也就足够了"

!"$"'#证明的方法证明数据结构分析中的结论的两种最常用的方法是归纳法证明和反证法证明$偶尔也被迫

用到只有教授们才使用的证明%" 证明一个定理不成立的最好的方法是举出一个反例"

归纳法证明

由归纳法进行的证明有两个标准的部分" 第一步是证明基准情形$A*(65*(6%! 就是确定定

理对于某个$某些%小的$通常是退化的%值的正确性) 这一步几乎总是很简单的" 接着! 进行归

纳假设$'7.45%'=6&;@+%&6('(%" 一般说来! 它指的是假设定理对直到某个有限数 "的所有的情况

都是成立的" 然后使用这个假设证明定理对下一个值$通常是 "H!%也是成立的" 至此定理得

证$在"是有限的情形下%"

作为一个例子! 我们证明斐波那契数! 6

D

B!! 6

!

B!! 6

"

B"! 6

#

B#! 6

$

BC! +! 6

*

B

6

*I!

H6

*I"

! 满足对*

(

!! 有6

*

K$C)#%

*

$有些定义规定 6

D

BD! 这只不过将该级数做了一次平

移%" 为了证明这个不等式! 我们首先验证定理对简单的情形成立" 容易验证 6

!

B! KC)# 及

6

"

B" K"C)Q! 这就证明了基准情形" 假设定理对于*B!! "! +! "成立! 这就是归纳假设" 为

了证明定理! 我们需要证明6

"H!

K$C)#%

"H!

" 根据定义得到

6

"H!

B6

"

H6

"I!

将归纳假设用于等号右边! 得到

6

"H!

K$C)#%

"

H$C)#%

"I!

K$#)C%$C)#%

"H!

H$#)C%

"

$C)#%

"H!

B$#)C%$C)#%

"H!

H$Q)"C%$C)#%

"H!

化简后为

6

"H!

K$#)C HQ)"C%$C)#%

"H!

B$"$)"C%$C)#%

"H!

K$C)#%

"H!

这就证明了这个定理"

作为第二个例子! 我们建立下面的定理"

定理!"%

如果!

(

!! 则$

!

*+!

*

"

+

!$!,!%$"!,!%

F

"

证明!

用数学归纳法证明" 对于基准情形! 容易看到! 当!B! 时定理成立" 对于归纳假设! 设定

理对 !

%

"

%

!成立" 我们将在该假设下证明定理对于!H! 也是成立的" 我们有

$

!,!

*+!

*

"

+

$

!

*+!

*

"

,$!,!%

"

应用归纳假设得到

$

!,!

*+!

*

"

+

!$!,!%$"!,!%

F

,$!,!%

"

B$!H!%

!$"!H!%

F

H$!H![ ]%B$!H!%

"!

"

HE!HF

F

B

$!H!%$!H"%$"!H#%

F

7

第!章0

C

F

Page 5: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

因此

$

!,!

*+!

*

"

+

$!,!%,$!,!% ,!-,"$!,!% ,!-

F

定理得证"

#

通过反例证明

公式6

"

%

"

"不成立" 证明这个结论的最容易的方法就是计算6

!!

B!$$ J!!

"

"

反证法证明

反证法证明是通过假设定理不成立! 然后证明该假设导致某个已知的性质不成立! 从而原

假设是错误的" 一个经典的例子是证明存在无穷多个素数" 为了证明这个结论! 我们假设定理

不成立" 于是! 存在某个最大的素数8

"

" 令8

!

! 8

"

! +! 8

"

是依序排列的所有素数并考虑

!B8

!

8

"

8

#

+8

"

H!

显然! !是比8

"

大的数! 根据假设!不是素数" 可是! 8

!

! 8

"

! +! 8

"

都不能整除 !! 因为除

得的结果总有余数 !" 这就产生一个矛盾! 因为每一个整数或者是素数! 或者是素数的乘积"

因此! 8

"

是最大素数的原假设是不成立的! 这正意味着定理成立"

!"%#递归简论

我们熟悉的大多数数学函数都是由一个简单公式来描述的" 例如! 我们可以利用公式

&BC$6I#"%)Q

将华氏温度转换成摄氏温度" 有了这个公式! 写一个<*=*方法就太简单了" 除去程序中的说明

和大括号外! 这一行的公式正好翻译成一行<*=*程序"

有时候数学函数以不太标准的形式来定义" 例如! 我们可以在非负整数集上定义一个函数

3! 它满足3$D% BD 且3$9% B"3$9I!% H9

"

" 从这个定义我们看到 3$!% B!! 3$"% BF! 3$#% B

"!! 以及3$$% BCL" 当一个函数用它自己来定义时就称为是递归$36543('=6%的" <*=*允许函数

0图 !/"0一个递归方法

是递归的"

!

"

但重要的是要记住! <*=*提供的仅仅是遵

循递归思想的一种尝试" 不是所有的数学递

归函数都能被有效地$或正确地%由 <*=*的递

归模拟来实现" 上面例子说的是递归函数 3应

该只用几行就能表示出来! 正如非递归函数一

样" 图 !:" 指出了函数3的递归实现"

第 # 行和第 $ 行处理基准情况$A*(65*(6%! 即此时函数的值可以直接算出而不用求助递归"

正如3$9% B"3$9I!% H9

" 若没有3$D% BD 这个事实在数学上没有意义一样! <*=*的递归方法若

无基准情况也是毫无意义的" 第 F 行执行的是递归调用"

关于递归! 有几个重要并且可能会被混淆的概念" 一个常见的问题是# 它是否就是循环推

理$5'3549*39+,'5%( 答案是# 虽然我们定义一个方法用的是这个方法本身! 但是我们并没有用方

法本身定义该方法的一个特定的实例" 换句话说! 通过使用3$C%来得到 3$C%的值才是循环的"

通过使用3$$%得到3$C%的值不是循环的! 当然! 除非 3$$%的求值又要用到对 3$C%的计算" 两

个最重要的问题恐怕就是如何做和为什么做的问题了" 如何和为什么的问题将在第 # 章正式解

决" 这里! 我们将给出一个不完全的描述"

实际上! 递归调用在处理上与其他调用没有什么不同" 如果以参数 $ 的值调用函数3! 那么

程序的第 F 行要求计算 "

)

3$#% H$

)

$" 这样! 就要执行一个计算 3$#%的调用! 而这又导致计

算 "

)

3$"% H#

)

#" 因此! 又要执行另一个计算 3$"%的调用! 而这意味着必须求出 "

)

3$!% H

;0引00论

!

"0对于数值计算使用递归通常不是个好主意" 我们在解释基本概念时已经说过"

E

L

Page 6: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

"

)

" 的值" 为此! 通过计算 "

)

3$D% H!

)

! 而得到3$!%" 此时! 3$D%必须被赋值" 由于这属于

基准情况! 因此我们事先知道3$D% BD" 从而 3$!%的计算得以完成! 其结果为 !" 然后! 3$"%*

3$#%以及最后3$$%的值都能够计算出来" 跟踪挂起的函数调用$这些调用已经开始但是正等待

着递归调用来完成%以及它们的变量的记录工作都是由计算机自动完成的" 然而! 重要的问题

在于! 递归调用将反复进行直到基准情形出现" 例如! 计算 3$ I!%的值将导致调用 3$ I"%*

3$ I#%等等" 由于这将不可能出现基准情形! 因此程序也就不可能算出答案" 偶尔还可能发生

更加微妙的错误! 我们将其展示在图 !/# 中" 图 !/# 中程序的这种错误是第 F 行上的$%&$'%定

义为$%&$'%" 显然! 实际上$%&$'%究竟是多少! 这个定义给不出任何线索" 因此! 计算机将

会反复调用$%&$'%以期解出它的值" 最后! 计算机簿记系统将占满内存空间! 程序崩溃" 一

般情形下! 我们会说该方法对一个特殊情形无效! 而在其他情形是正确的" 但此处这么说则不

0图 !/#0无终止递归方法

正确! 因为$%&$(%调用$%&$'%" 因此! $%&

$(%也不能求出值来" 不仅如此! $%&$)%*

$%&$*% 和 $%&$+% 都要调用 $%&$(%!

$%&$(%算不出值! 它们的值也就不能求出"

事实上! 除了 D 之外! 这个程序对 4 的任何非

负值都无效" 对于递归程序! 不存在像 &特殊

情形' 这样的情况"

上面的讨论导致递归的前两个基本法则#

!O基准情形$A*(65*(6%" 必须总要有某些基准的情形! 它们不用递归就能求解"

"O不断推进$:*R'7,@3+,36((%" 对于那些要递归求解的情形! 递归调用必须总能够朝着一

个基准情形推进"

在本书中我们将用递归解决一些问题" 作为非数学应用的一个例子! 考虑一本大词典" 词

典中的词都是用其他的词定义的" 当查一个单词的时候! 我们不是总能理解对该词的解释! 于

是我们不得不再查找解释中的一些词" 同样! 对这些词中的某些地方我们又不理解! 因此还

要继续这种查找" 因为词典是有限的! 所以实际上或者我们最终要查到一处! 明白了此处解

释中所有的单词$从而理解这里的解释! 并按照查找的路径回查其余的解释%或者我们发现

这些解释形成一个循环! 无法理解其最终含义! 或者在解释中需要我们理解的某个单词不在

这本词典里"

我们理解这些单词的递归策略如下# 如果知道一个单词的含义! 那么就算我们成功) 否则!

就在词典里查找这个单词" 如果我们理解对该词解释中的所有的单词! 那么又算我们成功) 否

则! 通过递归查找一些我们不认识的单词来 &算出' 对该单词解释的含义" 如果词典编纂得完

美无瑕! 那么这个过程就能够终止) 如果其中一个单词没有查到或是循环定义$解释%! 那么这

个过程则循环不定"

打印输出整数

设有一个正整数 4并希望把它打印出来" 我们的例程的名字为 ,#-./01/$.%" 假设仅有

的现成SNT例程将只处理单个数字并将其输出到终端" 我们为这种例程命名为 ,#-./2-3-/)

例如! ,#-./2-3-/$*%将输出 $ 到终端"

递归将为该问题提供一个非常漂亮的解" 要打印 EF"#$! 我们首先需要打印出 EF"#! 然后

再打印出 $" 第二步用语句,#-./2-3-/$.4'5%很容易完成! 但是第一步却不比原问题简单

多少" 它实际上是同一个问题! 因此可以用语句,#-./01/$.6'5%递归地解决它"

这告诉我们如何去解决一般的问题! 不过我们仍然需要确认程序不是循环不定的" 由于我

们尚未定义一个基准情况! 因此很清楚! 我们仍然还有些事情要做" 如果 D

%

4 K!D! 那么基准

情形就是,#-./2-3-/$.%" 现在! ,#-./01/$.%已对每一个从 D 到 Q 的正整数定义! 而更大

的正整数则用较小的正整数定义" 因此! 不存在循环的问题" 整个方法在图 !/$ 中指出"

<

第!章0

Q

!D

Page 7: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

图 !/$0打印整数的递归例程

我们没有努力去高效地做这件事" 我们本可以避免使用:+.例程$它是非常耗时的%! 因为

4U!D B4 I 4)!D

)

!D"

!

"

递归和归纳

让我们多少严格一些地证明上述递归的整数打印程序是可行的" 为此!我们将使用归纳法证明"

定理!"&

递归的整数打印算法对 4

(

D 是正确的"

证明$通过对 4所含数字的个数! 用归纳法证明之%#

首先! 如果 4只有一位数字! 那么程序显然是正确的! 因为它只是调用一次 ,#-./2-3-/"

然后! 设,#-./01/对所有"个或更少位数的数均能正常工作" 我们知道 "H! 位数字的数可

以通过其前"位数字后跟一位最低位数字来表示" 但是前 "位数字形成的数恰好是 4)!D ! 由

归纳假设它能够被正确地打印出来! 而最后的一位数字是 4 :+. !D! 因此该程序能够正确打印

出任意"H!位数字的数" 于是! 根据归纳法! 所有的数都能被正确地打印出来"

#

这个证明看起来可能有些奇怪! 但它实际上相当于是算法的描述" 证明阐述的是在设计递

归程序时! 同一问题的所有较小实例均可以假设运行正确! 递归程序只需要把这些较小问题的

解$它们通过递归奇迹般地得到%结合起来形成现行问题的解" 其数学根据则是归纳法的证明"

由此! 我们给出递归的第三个法则#

#O设计法则$.6(',7 3496%" 假设所有的递归调用都能运行"

这是一条重要的法则! 因为它意味着! 当设计递归程序时一般没有必要知道簿记管理的细

节! 你不必试图追踪大量的递归调用" 追踪具体的递归调用的序列常常是非常困难的" 当然!

在许多情况下! 这正是使用递归好处的体现! 因为计算机能够算出复杂的细节"

递归的主要问题是隐含的簿记开销" 虽然这些开销几乎总是合理的$因为递归程序不仅简

化了算法设计而且也有助于给出更加简洁的代码%! 但是递归绝不应该作为简单 !"#循环的代

替物" 我们将在 #OF 节更仔细地讨论递归涉及的系统开销"

当编写递归例程时! 关键是要牢记递归的四条基本法则#

!O基准情形" 必须总要有某些基准情形! 它无需递归就能解出"

"O不断推进" 对于那些需要递归求解的情形! 每一次递归调用都必须要使状况朝向一种基

准情形推进"

#O设计法则" 假设所有的递归调用都能运行"

$O合成效益法则$5+:@+47. '7%636(%3496%" 在求解一个问题的同一实例时! 切勿在不同的递

归调用中做重复性的工作"

第四条法则$连同它的名称一起%将在后面的章节证明是合理的" 使用递归计算诸如斐波

那契数之类简单数学函数的值的想法一般来说不是一个好主意! 其道理正是根据第四条法则"

只要在头脑中记住这些法则! 递归程序设计就应该是简单明了的"

!"&#实现泛型构件()*+,-.-'

面向对象的一个重要目标是对代码重用的支持" 支持这个目标的一个重要的机制就是泛型

=0引00论

!

"0 9是小于或等于9的最大整数"

!!

Page 8: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

机制$,6763'5:65&*7'(:%#如果除去对象的基本类型外! 实现方法是相同的!那么我们就可以用

泛型实现$,6763'5':@96:67%*%'+7%来描述这种基本的功能" 例如! 可以编写一个方法! 将由一些

项组成的数组排序) 方法的逻辑关系与被排序的对象的类型无关! 此时可以使用泛型方法"

与许多新的语言$例如VHH! 它使用模板来实现泛型编程%不同! 在 !OC 版以前! <*=*并不

直接支持泛型实现! 泛型编程的实现是通过使用继承的一些基本概念来完成的" 本节描述在

<*=*中如何使用继承的基本原则来实现一些泛型方法和类"

247公司在 "DD! 年是把对泛型方法和类的直接支持作为未来的语言增强剂来宣布的" 后

来! 终于在 "DD$ 年末发表了<*=*C 并提供了对泛型方法和类的支持" 然而! 使用泛型类需要理

解 @36/<*=*C 对泛型编程的语言特性" 因此! 对继承如何用来实现泛型程序的理解是根本的关

键! 甚至在<*=*C 中仍然如此"

!"&"!#使用!"#$%&表示泛型<*=*中的基本思想就是可以通过使用像 0$789/这样适当的超类来实现泛型类" 在图 !/C

中所示的:8;"#<=8>>类就是这样一个例子"

图 !/C0泛型:8;"#<=8>>类$@36/<*=*C%

当我们使用这种策略时! 有两个细节必须要考虑" 第一个细节在图 !/F 中阐释! 它描述一

个;%-.方法! 该方法把串 &#E' 写到 :8;"#<=8>>对象中! 然后又从 :8;"#<=8>>对象读

出" 为了访问这种对象的一个特定方法! 必须要强制转换成正确的类型" $当然! 在这个例子

中! 可以不必进行强制转换! 因为在程序的第 Q 行可以调用 /"?/#-.3$%方法! 这种调用对任

意对象都是能够做到的%"

图 !/F0使用泛型:8;"#<=8>>类$@36/<*=*C%

第二个重要的细节是不能使用基本类型" 只有引用类型能够与0$789/相容" 这个问题的

标准工作马上就要讨论"

>

第!章0

!"

!#

Page 9: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

!"&"$#基本类型的包装当我们实现算法的时候! 常常遇到语言定型问题# 我们已有一种类型的对象! 可是语言的

语法却需要一种不同类型的对象"

这种技巧阐释了包装类$)3*@@6359*((%的基本主题" 一种典型的用法是存储一个基本的类

型! 并添加一些这种基本类型不支持或不能正确支持的操作"

在<*=*中我们已经看到!虽然每一个引用类型都和0$789/相容!但是! L 种基本类型却不

能" 于是! <*=*为这 L 种基本类型中的每一种都提供了一个包装类" 例如! -./类型的包装是

@./838#" 每一个包装对象都是不可变的$就是说它的状态绝不能改变%! 它存储一种当该对象被

构建时所设置的原值! 并提供一种方法以重新得到该值" 包装类也包含不少的静态实用方法"

例如! 图 !/E 说明如何能够使用:8;"#<=8>>来存储整数"

图 !/E0@./838#包装类的一种演示

!"&"%#使用接口类型表示泛型只有在使用 0$789/类中已有的那些方法能够表示所执行的操作的时候! 才能使用

0$789/作为泛型类型来工作"

例如! 考虑在由一些项组成的数组中找出最大项的问题" 基本的代码是类型无关的! 但是

它的确需要一种能力来比较任意两个对象! 并确定哪个是大的! 哪个是小的" 因此! 我们不能

直接找出 0$789/的数组中的最大元素...我们需要更多的信息" 最简单的想法就是找出

=";,%#%$>8的数组中的最大元" 要确定顺序!可以使用9";,%#8A"方法!我们知道!它对所

有的=";,%#%$>8都必然是现成可用的" 图 !/L 中的代码做的就是这项工作! 它提供一种

;%-.方法! 该方法能够找出?/#-.3或?B%,8数组中的最大元"

现在! 提出几个忠告很重要" 首先! 只有实现 =";,%#%$>8接口的那些对象才能够作为

=";,%#%$>8数组的元素被传递" 仅有9";,%#8A"方法但并未宣称实现=";,%#%$>8接口的

对象不是=";,%#%$>8的! 它不具有必需的S2/8关系" 因为我们也许会比较两个 ?B%,8的面

积! 因此假设?B%,8实现 =";,%#%$>8接口" 这个测试程序还告诉我们! =-#9>8* ?C1%#8

和D89/%.3>8都是?B%,8的子类"

第二! 如果=";,%#%$>8数组有两个不相容的对象$例如! 一个 ?/#-.3和一个 ?B%,8%!

那么=";,%#8A"方法将抛出异常=>%EE=%E/FG98,/-"." 这是我们期望的性质"

第三! 如前所述! 基本类型不能作为 =";,%#%$>8传递! 但是包装类则可以! 因为它们实

现了=";,%#%$>8接口"

第四! 接口究竟是不是标准的库接口倒不是必需的"

最后! 这个方案不是总能够行得通! 因为有时宣称一个类实现所需的接口是不可能的" 例

如! 一个类可能是库中的类! 而接口却是用户定义的接口" 如果一个类是 !-.%>类! 那么我们

就不可能扩展它以创建一个新的类" !OF 节对这个问题提出了另一个解决方案! 即 #$%&'()%

?0引00论

!$

0

W

!C

Page 10: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

+AX65%" 这种函数对象$-475%'+7 +AX65%%也使用一些接口! 它或许是我们在 <*=*库中所遇到的核

心论题之一"

图 !/L0泛型!-.&:%G例程! 使用?B%,8和?/#-.3演示$@36/<*=*C%

!"&"&#数组类型的兼容性语言设计中的困难之一是如何处理集合类型的继承问题" 设 F;,>"<88@0:$H8#E"." 那

么! 这是不是也意味着数组 F;,>"<88, - @0:$H8#E"., -呢( 换句话说! 如果一个例程接受

H8#E".,-作为参数! 那么我们能不能把F;,>"<88,-作为参数来传递呢(

乍一看! 该问题不值得一问! 似乎F;,>"<88,-就应该是和H8#E".,-类型兼容的" 然而!

这个问题却要比想象的复杂" 假设除 F;,>"<88外! 我们还有 ?/1&8./@0:$H8#E".! 并设

F;,>"<88,-是和H8#E".,-类型兼容的" 此时考虑下面两条赋值语句#

两句都编译! 而 %## ,5-实际上是引用一个 F;,>"<88! 可是 ?/1&8./@0:!AB:$

F;,>"<88" 这样就产生了类型混乱" 运行时系统$347%':6(;(%6:%$<*=*虚拟机.译者注%不能

抛出=>%EE=%E/FG98,/-".异常! 因为不存在类型转换"

CD

第!章0

Page 11: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

避免这种问题的最容易的方法是指定这些数组不是类型兼容的" 可是! 在<*=*中数组却是类型

兼容的" 这叫作协变数组类型$5+=*3'*7%*3**;%;@6%" 每个数组都明了它所允许存储的对象的类型"

如果将一个不兼容的类型插入到数组中!那么虚拟机将抛出一个I##%<?/"#8FG98,/-".异常"

在较早版本的<*=*中是需要数组的协变性的! 否则在图 !/L 的第 "Q 行和第 #D 行的调用将

编译不了"

!"'#利用 ,-.-'泛型特性实现泛型构件

<*=*C 支持泛型类! 这些类很容易使用" 然而! 编写泛型类却需要多做一些工作" 本节将

叙述编写泛型类和泛型方法的基础" 我们不打算涉及语言的所有结构! 那样将是相当复杂的!

而且有时是很难处理的" 我们将介绍用于全书的语法和习语"

0图 !/Q0:8;"#<=8>>类的泛型实现

!"'"!#简单的泛型类和接口图 !/Q 是前面图 !/C 描述的 :8;"#<=8>>

的泛型版代码" 这里! 我们把名字改成了

J8.8#-9:8;"#<=8>>! 因为两个类都不在包

中! 所以名字也就不能相同"

当指定一个泛型类时! 类的声明则包含一

个或多个类型参数! 这些参数被放在类名后面

的一对尖括号内" 第 ! 行指出! J8.8#-9K

:8;"#<=8>>有一个类型参数" 在这个例子

中! 对类型参数没有明显的限制! 所以用户可

以创建像J8.8#-9:8;"#<=8>>K?/#-.3J和J8.8#-9:8;"#<=8>>K@./838#J这样的类

型! 但是不能创建J8.8#-9:8;"#<=8>>K-./J这样的类型" 在J8.8#-9:8;"#<=8>>类声

明内部! 我们可以声明泛型类型的域和使用泛型类型作为参数或返回类型的方法" 例如在

图 !/Q 的第 C 行! 类J8.8#-9:8;"#<=8>>K?/#-.3J的 L#-/8方法需要一个 ?/#-.3类型

的参数" 如果传递其他参数那将产生一个编译错误"

也可以声明接口是泛型的" 例如! 在 <*=*C 以前! =";,%#%$>8接口不是泛型的! 而它的

9";,%#8A"方法需要一个0$789/作为参数" 于是! 传递到9";,%#8A"方法的任何引用变量

即使不是一个合理的类型也都会编译! 而只是在运行时报告 =>%EE=%E/FG98,/-".错误" 在

图 !/!D0<*=*C 版本的=";,%#%$>8

接口! 它是泛型接口

M%N%C 中! =";,%#%$>8接口是泛型的! 如

图 !/!D 所示" 例如! 现在 ?/#-.3类实现

=";,%#%$>8K?/#-.3J并有一个9";,%#8K

A"方法! 这个方法以一个 ?/#-.3作为其参

数" 通过使类变成泛型类! 以前只有在运行

时才能报告的许多错误如今变成了编译时的

错误"

!"'"$#自动装箱/拆箱图 !/E 中的代码写得很麻烦! 因为使用包装类需要在调用L#-/8之前创建@./838#对象!

然后才能使用-./O%>18方法从@./838#中提取 -./值" 在 <*=*C 以前! 这是需要的! 因为

如果一个-./型的量被放到需要@./838#对象的地方! 那么编译将会产生一个错误信息! 而

如果将一个@./838#对象的结果赋值给一个 -./型的量! 则编译也将产生一个错误信息"

图 !/E 中的代码准确地反映出基本类型和引用类型之间的区别! 但还没有清楚地表示出程序员

把那些-./存入集合$5+9965%'+7%的意图"

<*=*C 矫正了这种情形" 如果一个 -./型量被传递到需要一个 @./838#对象的地方! 那

么! 编译器将在幕后插入一个对 @./838#构造方法的调用" 这就叫作自动装箱" 而如果一个

DD0引00论

!F

!E

Page 12: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

@./838#对象被放到需要-./型量的地方! 则编译器将在幕后插入一个对-./O%>18方法的调

用! 这就叫作自动拆箱" 对于其他 E对基本类型6包装类型! 同样会发生类似的情形" 图 !/!!*用

<*=*C 描述了自动装箱和自动拆箱的使用" 注意! 在J8.8#-9:8;"#<=8>>中引用的那些实体

仍然是@./838#对象) 在J8.8#-9:8;"#<=8>>的实例化中! -./不能够代替@./838#"

图 !/!!*0自动装箱和拆箱$<*=*C%

!"'"%#菱形运算符在图 !/!!*中! 第 C 行有些烦人! 因为既然 ;是 J8.8#-9:8;"#<=8>>K@./838#J类型

的! 显然创建的对象也必须是J8.8#-9:8;"#<=8>>K@./838#J类型的! 任何其他类型的参数

都会产生编译错误" <*=*E增加了一种新的语言特性! 称为菱形运算符! 使得第 C行可以改写为

菱形运算符在不增加开发者负担的情况下简化了代码! 我们通篇都会使用它" 图 !/!!A 给

出了带菱形运算符的<*=*E 版代码"

图 !/!!A0自动装箱和拆箱$<*=*E! 使用菱形运算符%

!"'"&#带有限制的通配符图 !/!" 显示一个E/%/-9方法! 该方法计算一个 ?B%,8数组的总面积$假设 ?B%,8是含

有%#8%方法的类) 而=-#9>8和?C1%#8则是继承 ?B%,8的类%" 假设我们想要重写这个计

算总面积的方法! 使得该方法能够使用=">>89/-".K?B%,8J这样的参数" =">>89/-".将

在第 # 章描述" 当前! 唯一重要的是它能够存储一些项! 而且这些项可以用一个增强的 !"#循

环来处理" 由于是增强的!"#循环! 因此代码是相同的! 最后的结果如图 !/!# 所示" 如果传递

一个=">>89/-".K?B%,8J! 那么! 程序会正常运行" 可是! 要是传递一个 =">>89/-".

K?C1%#8J会发生什么情况呢( 答案依赖于是否=">>89/-".K?C1%#8J@0:$=">>89/-".

K?B%,8J" 回顾 !O$O$ 节可知! 用技术术语来说即是否我们拥有协变性"

.D

第!章0

!L

Page 13: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

图 !/!"0?B%,8,-的/"/%>I#8%方法

图 !/!#0/"/%>I#8%方法! 如果传递一个=">>89/-".K?C1%#8J! 则该方法不能运行

我们在 !O$O$ 节提到! <*=*中的数组是协变的" 于是! ?C1%#8,- @0:$?B%,8,-" 一方面!

这种一致性意味着! 如果数组是协变的! 那么集合也将是协变的" 另一方面! 我们在 !O$O$ 节

看到! 数组的协变性导致代码得以编译! 但此后会产生一个运行时异常 $一个

I##%<?/"#8FG98,/-".%" 因为使用泛型的全部原因就在于产生编译器错误而不是类型不匹

配的运行时异常! 所以! 泛型集合不是协变的" 因此! 我们不能把=">>89/-".K?C1%#8J作

为参数传递到图 !/!# 中的方法里去"

现在的问题是! 泛型$以及泛型集合%不是协变的$但有意义%! 而数组是协变的" 若无附加

的语法! 则用户就会避免使用集合$5+9965%'+7%! 因为失去协变性使得代码缺少灵活性"

<*=*C 用通配符$)'9.5*3.%来弥补这个不足" 通配符用来表示参数类型的子类$或超类%"

图 !/!$描述带有限制的通配符的使用! 图中编写一个将 =">>89/-".KAJ作为参数的方法

/"/%>I#8%!其中A@0:$?B%,8" 因此!=">>89/-".K?B%,8J和=">>89/-".K?C1%#8J都是

可以接受的参数" 通配符还可以不带限制使用$此时假设为8G/8.&E0$789/%!或不用8G/8.&E而

用E1,8#$来表示超类而不是子类%)此外还存在一些其他的语法!我们就不在这里讨论了"

图 !/!$0用通配符修正后的/"/%>I#8%方法! 如果传递一个

=">>89/-".K?C1%#8J! 则方法能够正常运行

2D0引00论

!Q

Page 14: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

!"'"'#泛型'&(&)%方法从某种意义上说! 图 !/!$ 中的/"/%>I#8%方法是泛型方法! 因为它能够接受不同类型的

参数" 但是! 这里没有特定类型的参数表! 正如在J8.8#-9:8;"#<=8>>类的声明中所做的那

样" 有时候特定类型很重要! 这或许因为下列的原因#

!O该特定类型用做返回类型)

"O该类型用在多于一个的参数类型中)

#O该类型用于声明一个局部变量"

如果是这样! 那么! 必须要声明一种带有若干类型参数的显式泛型方法"

例如! 图 !/!C 显示一种泛型E/%/-9方法! 该方法对值G在数组 %##中进行一系列查找"

通过使用一种泛型方法! 代替使用0$789/作为参数的非泛型方法! 当在?B%,8对象的数组中

查找I,,>8对象时我们能够得到编译时错误"

图 !/!C0泛型E/%/-9方法搜索数组

泛型方法特别像是泛型类! 因为类型参数表使用相同的语法" 在泛型方法中的类型参数位

于返回类型之前"

!"'"0#类型限界假设我们想要编写一个 !-.&:%G例程" 考虑图 !/!F 中的代码" 由于编译器不能证明在

第 F 行上对 9";,%#8A"的调用是合法的! 因此! 程序不能正常运行) 只有在 I.<A<,8是

=";,%#%$>8的情况下才能保证9";,%#8A"存在" 我们可以使用类型限界$%;@6A+47.%解决

这个问题" 类型限界在尖括号内指定! 它指定参数类型必须具有的性质" 一种自然的想法是把

性质改写成

图 !/!F0泛型E/%/-9方法查找一个数组中的最大元素! 该方法不能正常运行

我们知道! 因为=";,%#%$>8接口如今是泛型的! 所以这种做法很自然" 虽然这个程序能

够被编译! 但是更好的做法却是

然而! 这个做法还是不能令人满意" 为了看清这个问题! 假设 ?B%,8实现 =";,%#%$>8

7D

第!章0

"D

"!

Page 15: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

K?B%,8J! 设?C1%#8继承 ?B%,8" 此时! 我们所知道的只是 ?C1%#8实现 =";,%#%$>8

K?B%,8J" 于是! ?C1%#8 @0:$=";,%#%$>8K?B%,8J! 但它 @0:!AB:$=";,%#%$>8

K?C1%#8J/ 0

应该说! I.<A<,8@0:$=";,%#%$>8KAJ! 其中! A是I.<A<,8的父类" 由于我们不需

要知道准确的类型A! 因此可以使用通配符" 最后的结果变成

图 !/!E显示!-.&:%G的实现" 编译器将接受类型A的数组! 只是使得A实现=";,%#%$>8

K?J接口! 其中A@0:$?" 当然! 限界声明看起来有些混乱" 幸运的是! 我们不会再看到任何

比这种用语更复杂的用语了"

图 !/!E0在一个数组中找出最大元的泛型E/%/-9方法" 以例说明类型参数的限界

!"'"1#类型擦除泛型在很大程度上是<*=*语言中的成分而不是虚拟机中的结构" 泛型类可以由编译器通过

所谓的类型擦除$%;@663*(436%过程而转变成非泛型类" 这样! 编译器就生成一种与泛型类同名

的原始类$3*)59*((%! 但是类型参数都被删去了" 类型变量由它们的类型限界来代替! 当一个

具有擦除返回类型的泛型方法被调用的时候! 一些特性被自动地插入" 如果使用一个泛型类而

不带类型参数! 那么使用的是原始类"

类型擦除的一个重要推论是! 所生成的代码与程序员在泛型之前所写的代码并没有太多的

差异! 而且事实上运行的也并不快" 其显著的优点在于! 程序员不必把一些类型转换放到代码

中! 编译器将进行重要的类型检验"

!"'"2#对于泛型的限制对于泛型类型有许多的限制" 由于类型擦除的原因!这里列出的每一个限制都是必须要遵守的"

基本类型

基本类型不能用做类型参数" 因此! J8.8#-9:8;"#<=8>>K-./J是非法的" 我们必须

使用包装类"

)*'&(*%$+,检测

-.E/%.98"!检测和类型转换工作只对原始类型进行" 在下列代码中#

这里的类型转换在运行时是成功的! 因为所有的类型都是 J8.8#-9:8;"#<=8>>" 但在最后一

行! 由于对 #8%&的调用企图返回一个 ?/#-.3对象从而产生一个运行时错误" 结果! 类型转

;D0引00论

""

Page 16: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

换将产生一个警告! 而对应的-.E/%.98"!检测是非法的"

'&(&)%的语境

在一个泛型类中! E/%/-9方法和 E/%/-9域均不可引用类的类型变量! 因为在类型擦除

后类型变量就不存在了" 另外! 由于实际上只存在一个原始的类! 因此 E/%/-9域在该类的诸

泛型实例之间是共享的"

泛型类型的实例化

不能创建一个泛型类型的实例" 如果A是一个类型变量! 则语句

是非法的" A由它的限界代替!这可能是0$789/$或甚至是抽象类%!因此对.8L的调用没有意义"

泛型数组对象

也不能创建一个泛型的数组" 如果A是一个类型变量! 则语句

是非法的" A将由它的限界代替! 这很可能是 0$789/A! 于是$由类型擦除产生的%对 A, -的

类型转换将无法进行! 因为0$789/,- @0:!AB:$A,-" 由于我们不能创建泛型对象的数组! 因

此一般说来我们必须创建一个擦除类型的数组! 然后使用类型转换" 这种类型转换将产生一个

关于未检验的类型转换的编译警告"

参数化类型的数组

参数化类型的数组的实例化是非法的" 考虑下列代码#

正常情况下! 我们认为第 $行的赋值会生成一个I##%<?/"#8FG98,/-".! 因为赋值的类型有错

误" 可是! 在类型擦除之后! 数组的类型为J8.8#-9:8;"#<=8>>,-! 而加到数组中的对象也是

J8.8#-9:8;"#<=8>>!因此不存在I##%<?/"#8FG98,/-".异常" 于是!该段代码没有类型转

换! 它最终将在第 C行产生一个=>%EE=%E/FG98,/-".异常! 这正是泛型应该避免的情况"

!"0#函数对象

在 !OC 节我们指出如何编写泛型算法" 例如! 图 !/!F 中的泛型方法可以用于找出一个数组

中的最大项"

然而! 这种泛型方法有一个重要的局限# 它只对实现=";,%#%$>8接口的对象有效! 因为它

使用9";,%#8A"作为所有比较决策的基础" 在许多情形下!这种处理方式是不可行的" 例如!尽

管假设D89/%.3>8类实现 =";,%#%$>8接口有些过分! 但即使实现了该接口! 它所具有的

9";,%#8A"方法恐怕还不是我们想要的方法" 例如! 给定一个 " Y!D的矩形和一个 C YC 的矩形!

哪个是更大的矩形呢( 答案恐怕依赖于我们是使用面积还是使用长度来决定" 或者! 如果我们试

图通过一个开口构造该矩形! 那么或许较大的矩形就是具有较大最小周长的矩形" 作为第二个例

子! 在一个字符串的数组中如果想要找出最大的串$即字典序排在最后的串%! 默认的9";,%#8A"

不忽略字符的大小写! 则 &Z?[\8' 按字典序排在 &*99',*%+3' 之前! 这可能不是我们想要的"

上述这些情形的解决方案是重写!-.&:%G! 使它接受两个参数# 一个是对象的数组! 另一

个是比较函数! 该函数解释如何决定两个对象中哪个大哪个小" 实际上! 这些对象不再知道如

何比较它们自己) 这些信息从数组的对象中完全去除了"

一种将函数作为参数传递的独创方法是注意到对象既包含数据也包含方法! 于是我们可以

<D

第!章0

"#

"$

Page 17: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

定义一个没有数据而只有一个方法的类! 并传递该类的一个实例" 事实上! 一个函数通过将其

放在一个对象内部而被传递" 这样的对象通常叫作函数对象$-47%'+7 +AX65%%"

图 !/!L 显示函数对象想法的最简单的实现" !-.&:%G的第二个参数是 =";,%#%/"#类型

的对象" 接口=";,%#%/"#在 7%N%P1/->中指定并包含一个 9";,%#8方法" 这个接口在

图 !/!Q 中指出"

图 !/!L0利用一个函数对象作为第 " 个参数传递给!-.&:%G) 输出Z?[\8

图 !/!Q0=";,%#%/"#接口

实现接口=";,%#%/"#KI.<A<,8J类型的任何类都必须要有一个叫作 9";,%#8的方

法! 该方法有两个泛型类型$I.<A<,8%的参数并返回一个-./型的量! 遵守和 9";,%#8A"相

同的一般约定" 因此! 在图 !/!L 中的第 Q 行对 9";,%#8的调用可以用来比较数组的项" 第 $

行的带有限制的通配符用来表示如果查找数组中的最大的项! 那么该9";,%#%/"#必须知道如

何比较这些项! 或者这些项的超类型的那些对象" 我们可以在第 "F 行看到! 为了使用这种版本

的!-.&:%G! !-.&:%G通过传递一个?/#-.3数组以及一个实现9";,%#%/"#K?/#-.3J的

=D0引00论

"C

Page 18: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

对象而被调用" 这个对象属于=%[email protected]/-N8=";,%#8类型! 它是我们编写的一个类"

在第 $ 章我们将给出关于一个类的例子! 这个类需要将它存储的项排序" 我们将利用

=";,%#%$>8编写大部分的代码! 并指出其中需要使用函数对象的改动部分" 在本书的其他地

方! 我们将避免函数对象的细节以使得代码尽可能地简单! 我们知道以后将函数对象添加进去

并不困难"

小结

本章为该书的其余部分创建一个平台" 面对大量的输入! 一种算法所花费的时间将是评判

决策好坏的重要标准$当然! 正确性是最重要的%" 速度是相对的" 对于一个问题在一台机器上

速度是快的! 有可能对另外一个问题或在一台不同的机器上就变成速度是慢的" 我们将从下一

章开始处理这些问题! 并且要用到本章讨论过的数学来建立一个正式的模型"

练习

!O!0 编写一个程序解决选择问题" 令"B!)"" 画出表格显示程序对于!种不同的值的运行时间"

!O"0 编写一个程序求解字谜游戏问题"

!O#0 只使用处理SNT的,#-./2-3-/方法! 编写一种方法以输出任意&"1$>8型量$可以是负的%"

!O$0 V允许拥有形如

00

00 的语句! 它将3*EF4GHF读入并将其插入到-.9>1&8语句处" -.9>1&8语句可以嵌套) 换句话说!

文件-'967*:6本身还可以包含-.9>1&8语句! 但是显然一个文件在任何链接中都不能包含它自

己" 编写一个程序! 使它读入被一些-.9>1&8语句修饰的文件并且输出这个文件"

!OC0 编写一种递归方法! 它返回数!的二进制表示中 ! 的个数" 利用这样的事实# 如果!是奇数! 那

么其 ! 的个数等于!)" 的二进制表示中 ! 的个数加 !"

!OF0 编写带有下列声明的例程#

00

00 第一个例程是个驱动程序! 它调用第二个例程并显示 ?/#-.3E/#中的字符的所有排列" 如果

E/#是]%$9]! 那么输出的串则是%$9! %9$! $%9! $9%! 9%$和9$%" 第二个例程使用递归"

!OE0 证明下列公式#

*O9+,#K#对所有的#JD 成立

AO9+,$$%

%

B%9+,$

!OL0 计算下列各和#

*O

$

M

*+D

!

$

*

AO

$

M

*+D

*

$

*

0 0

)

5O

$

M

*+D

*

"

$

*

0 0

))

.O

$

M

*+D

*

!

$

*

!OQ0 估计$

!

*+!)"

!

*

"

)

!O!D0"

!DD

$:+. C%是多少(

!O!!0令6

*

是在 !O" 节中定义的斐波那契数" 证明下列各式#

*O

$

!-"

*+!

6

*

+6

!

-"

>D

第!章0

"F

Page 19: 3..’%’+7images.china-pub.com/ebook4925001-4930000/4927783/ch01.pdf · 2016. 3. 1. · 上述两种方法相对来说都不难编码并可求解通常发表于杂志上的许多现实的字谜游戏"

AO6

!

K

"

!

! 其中"

B$ 槡! H C%)"

0 0

))

5O给出6

!

准确的封闭形式的表达式"

!O!"0证明下列公式#

*O

$

!

*+!

$"*-!% +!

"

AO

$

!

*+!

*

#

+

$

!

*+!

( )*

"

!O!#0设计一个泛型类=">>89/-".! 它存储0$789/对象的集合$在数组中%! 以及该集合的当前大小"

提供,1$>-9方法-EF;,/<* ;%Q8F;,/<* -.E8#/* #8;"N8和-EH#8E8./" 方法-EH#8E8./$G%

当且仅当在该集合中存在$由8C1%>E定义%等于G的一个0$789/时返回/#18"

!O!$0设计一个泛型类0#&8#8&=">>89/-".! 它存储 =";,%#%$>8的对象的集合$在数组中%! 以及

该集合的当前大小" 提供,1$>-9方法-EF;,/<* ;%Q8F;,/<* -.E8#/* #8;"N8* !-.&:-.和

!-.&:%G" !-.&:-.和!-.&:%G分别返回该集合中最小的和最大的 =";,%#%$>8对象的引用

$如果该集合为空! 则返回.1>>%"

!O!C0定义一个D89/%.3>8类! 该类提供38/R8.3/B和38/S-&/B方法" 利用图 !K!L 中的!-.&:%G

例程编写一种 ;%-.方法! 该方法创建一个 D89/%.3>8数组并首先找出依面积最大的

D89/%.3>8对象! 然后找出依周长最大的D89/%.3>8对象"

参考文献

有许多好的教科书涵盖了本章所复习的数学内容! 其中的一小部分为,!-* ,"-* ,#-* ,!!-* ,!#-和

,!$-" 参考材料,!!-是特别配合算法分析的教材! 它是三卷丛书中的第一卷! 并将在本书随处引用" 更

深入的材料包含于,L-中"

本书全书将假设读者具备<*=*,$-* ,F-* ,E-的知识" 本章中的材料可以作为我们将在本书中用到

的一些要点的概括" 我们还假设读者熟悉递归$本章中关于递归的总结是对递归的快速回顾%! 在书中适

当的地方我们将提供使用它们的一些提示" 不熟悉递归的读者应该参考,!$-或任意一本好的中等水平的

程序设计教材"

一般的程序设计风格在多部著作均有所讨论! 其中一些经典的文献如,C-* ,Q-和,!D-"

?D0引00论

"E

"L