diff --git a/others/appendix/list/list-zh-cn.tex b/others/appendix/list/list-zh-cn.tex index f6b0484cfa..cc4b2df34a 100644 --- a/others/appendix/list/list-zh-cn.tex +++ b/others/appendix/list/list-zh-cn.tex @@ -2377,7 +2377,7 @@ \subsubsection{分组} \begin{Exercise} \begin{enumerate} \item 选择一门命令式语言,实现in-place的take和drop算法。请注意处理越界情况。建议同时使用带有GC和不带有GC的语言进行练习。 -\item 选择一门命令式语言,实现take-while和drop-while算法。建议同时使用动态类型和静态类型(不带有类型推导)的语言进行练习。考虑在静态类型语言中,如何声明通用的条件函数类型? +\item 选择一门命令式语言,实现take-while和drop-while算法。建议同时使用动态类型和静态类型(不带有类型推导)的语言进行练习。请考虑在静态类型语言中,如何声明通用的条件函数类型? \item 考虑下面span的定义 \[ span(p, L) = \left \{ @@ -2403,7 +2403,7 @@ \subsection{从右侧fold} \index{列表!foldr} \index{列表!fold from right} -回顾此前给出的求和和求积定义,它们的结构非常相似。 +回顾此前给出的求和与求积定义,它们的结构非常相似: \[ sum(L) = \left \{ @@ -2425,7 +2425,7 @@ \subsection{从右侧fold} \right. \] -不仅是求积于求和,如果我们列出插入排序的定义,会发现它的结构也是如此。 +不仅是求和与求积,如果我们列出插入排序的定义,会发现它的结构也是如此。 \[ sort(L) = \left \{ @@ -2437,14 +2437,14 @@ \subsection{从右侧fold} \right. \] -这提示我们可以抽象出它们之后本质上通用的结构,以避免不断重复。观察$sum$、$product$、和$sort$,我们可以参数化其结构中的两部分。 +这提示我们可以抽象出本质上通用的结构,以避免不断重复。观察$sum$、$product$、和$sort$,我们可以参数化其结构中的两部分: \begin{itemize} \item 边界条件时的结果不同。求和时结果为0;求积时结果为1;排序时结果为空列表; \item 对第一个元素和中间计算结果的处理函数不同。求和时,这一函数是相加;求积时,这一函数是相乘;排序时,这一函数是按序插入。 \end{itemize} -如果我们将边界条件时的结果参数化为$z$(代表抽象的零的概念),在递归时使用的函数为$f$(它接受两个参数,一个是列表中的第一个元素,另一个是列表中剩余元素递归处理的结果),则这一通用结构可以定义如下。 +如果我们将边界条件时的结果参数化为$z$(代表抽象零的概念),在递归时使用的函数为$f$(它接受两个参数,一个是列表中的第一个元素,另一个是列表中剩余元素递归处理的结果),则这一通用结构可以定义如下。 \[ proc(f, z, L) = \left \{ @@ -2480,7 +2480,7 @@ \subsection{从右侧fold} proc(f, z, L) = x_1 \oplus_f (x_2 \oplus_f (... (x_n \oplus_f z))...) \] -注意其中的括号,它限制了计算的顺序,计算从最右侧开始($x_n \oplus_f z$),不断向左侧进行指导$x_1$。这和下图描述的中国折扇的收起过程相似。中国折扇有竹子和纸制成。若干竹子的扇骨在末端被一个轴穿在一起。图\ref{fig:fold-fan} (a)给出的是扇形的纸被完全展开时的样子;扇子可以通过将纸折叠收起。图\ref{fig:fold-fan} (b)展示了扇子右侧被部分收起时的样子。当折叠过程完全结束后,扇子的形状变成了一根杆状,如图\ref{fig:fold-fan} (c)所示。 +注意其中的括号,它限制了计算的顺序,计算从最右侧开始($x_n \oplus_f z$),不断向左侧进行直到$x_1$。这和下图描述的中国折扇的收起过程相似。中国折扇由竹子和纸制成。所有竹子的扇骨在末端被一个轴穿在一起。图\ref{fig:fold-fan} (a)给出的是扇形的纸被完全展开时的样子;扇子可以被折叠收起。图\ref{fig:fold-fan} (b)展示了扇子右侧被部分收起时的样子。当折叠过程完全结束后,扇子的形状变成了一根杆状,如图\ref{fig:fold-fan} (c)所示。 \begin{figure}[htbp] \centering @@ -2514,7 +2514,7 @@ \subsection{从右侧fold} \end{array} \] -在函数式编程中,我们称这一过程为\underline{fold},特别地,由于我们从最内层的结构开始,它位于最右侧,这一类型的fold实际叫做\underline{fold right}。 +在函数式编程中,我们称这一过程为\underline{fold},特别地,由于我们从最内层的结构开始,它位于最右侧,这一类型的fold叫做\underline{右侧fold}(fold right)。 \be foldr(f, z, L) = \left \{ @@ -2526,7 +2526,7 @@ \subsection{从右侧fold} \right. \ee -使用fold-right,求和与求积可以重新定义如下。 +使用右侧fold,求和与求积可以重新定义如下。 \be \begin{array}{rl} @@ -2542,7 +2542,7 @@ \subsection{从右侧fold} \end{array} \ee -插入排序算法也可以使用fold-right定义。 +插入排序算法也可以使用右侧fold定义。 \be sort(L) = foldr(insert, \phi, L) @@ -2552,11 +2552,11 @@ \subsection{从左侧fold} \index{列表!foldl} \index{列表!fold from left} -在尾递归一节,我们提到递归形式的求积和求和都是从右向左进行,我们必须在递归时记录下所有的中间结果和上下文环境。由于fold-right是从这一结构中抽象出的,因此从右侧fold时同样需要记录这些信息。当列表很长时,它的代价很大。 +在尾递归一节,我们提到递归形式的求积与求和都是从右向左进行,我们必须在递归时记录下所有的中间结果和上下文环境。由于右侧fold是从这一结构中抽象出的,因此从右侧fold时同样需要记录这些信息。当列表很长时,它的代价很大。 -由于求和于求积可以变换成尾递归形式,我们也可以抽象出另一种fold算法,它从左向右处理列表,并且复用相同的环境,以支持尾递归优化。 +由于求和与求积可以变换成尾递归形式,我们也可以抽象出另一种fold算法,它从左向右处理列表,并且复用相同的环境,以支持尾递归优化。 -我们无需再次从求和、求积和插入排序归纳,可以直接将右侧fold变换为尾递归形式。观察到初始结果$z$,实际上表示的是中间结果。我们可以用它作为累积器。 +我们无需再次从求和、求积、插入排序中归纳抽象,可以直接将右侧fold变换为尾递归形式。观察到初始结果$z$,实际上表示的是中间结果。我们可以用它作为累积器。 \be foldl(f, z, L) = \left \{ @@ -2576,7 +2576,7 @@ \subsection{从左侧fold} \begin{array}{rl} \sum_{i=1}^{5}i & = foldl(+, 0, \{1, 2, 3, 4, 5\}) \\ & = foldl(+, 0 + 1, \{ 2, 3, 4, 5 \}) \\ - & = foldl(+, (0 + 1) + 2 \{3, 4, 5 \} \\ + & = foldl(+, (0 + 1) + 2, \{3, 4, 5 \} \\ & = foldl(+, ((0 + 1) + 2) + 3, \{4, 5\}) \\ & = foldl(+, (((0 + 1) + 2) + 3) + 4, \{5\}) \\ & = foldl(+, ((((0 + 1) + 2 + 3) + 4 + 5, \phi) \\ @@ -2598,7 +2598,7 @@ \subsection{从左侧fold} foldl(f, z, L) = ((...(z \oplus_f l_1) \oplus_f l_2) \oplus_f ...) \oplus l_n \ee -使用左侧fold,求和、求积、和插入排序可以通过调用$foldl$依次实现为$sum(L) = foldl(+, 0, L)$、$product(L) = foldl(+, 1, L)$、以及$sort(L) = foldl(insert, \phi, L)$。和右侧fold相比,它们看似一致,但是其内部实现却有所不同。 +使用左侧fold,求和、求积、以及插入排序可以通过调用$foldl$依次实现为$sum(L) = foldl(+, 0, L)$、$product(L) = foldl(+, 1, L)$、以及$sort(L) = foldl(insert, \phi, L)$。和右侧fold相比,它们看似一致,但是其内部实现却有所不同。 \subsubsection{命令式fold和抽象fold概念} @@ -2652,7 +2652,7 @@ \subsubsection{命令式fold和抽象fold概念} 使用左侧fold就无法达到这一目的。因为只有处理完全部列表,才能开始外层的计算。这涉及具体的惰性求值特性,超出了本书的范围。读者可以参考\cite{Haskell-wiki}了解更多信息。 -虽然本附录的主要内容是关于单向链表算法的,但是fold本身是一个更一般性的概念。它并不限于列表,也可以应用到其他数据结构。我们可以对一棵树、一个队列、甚至一个更复杂的数据结构进行fold。只要它满足下面这两个条件: +虽然本附录的主要内容是关于单向链表算法的,但是fold本身是一个一般性的概念。它并不限于列表,也可以应用到其他数据结构。我们可以对一棵树、一个队列、甚至一个更复杂的数据结构进行fold。只要它满足下面这两个条件: \begin{itemize} \item 作为边界情况,能定义一个空的数据结构(例如空树); @@ -2679,7 +2679,7 @@ \subsection{fold的应用} fold(\lambda_{L, i} \cdot map(switch(i), L), \{(1, 0), (2, 0), ..., (n, 0) \}, \{1, 2, ..., n\}) \] -为了简洁,我们可以不使用$\lambda$记法,而直接用$map(switch(i), l)$。fold的结果是灯最终的明暗状态,我们需要利用映射,从组成每个元素的一对值中取出第二个值,然后将亮着的灯加到一起。 +为了简洁,我们可以不使用$\lambda$记法,而直接用$map(switch(i), L)$。fold的结果是灯最终的明暗状态,我们需要利用映射,从一对值中取出第二个值,然后将亮着的灯加到一起。 \be sum(map(snd, fold(map(switch(i), L), \{(1, 0), (2, 0), ..., (n, 0) \}, \{1, 2, ..., n\}))) @@ -2690,7 +2690,7 @@ \subsection{fold的应用} \subsubsection{连接列表的列表} \index{列表!concats} -在第\ref{concat}节中,我们讲述了如何将两个列表连接成一个。实际上,对多个列表进行连接和将多个数累加有很多共通之处。我们可以设计要给通用的算法,将多个列表的列表连接成一个大的列表。 +在第\ref{concat}节中,我们讲述了如何将两个列表连接成一个。实际上,对多个列表进行连接和将多个数累加有很多共同之处。我们可以设计一个通用的算法,将多个列表的列表连接成一个大的列表。 本节中,我们要用fold来实现这一算法。如同累加可以表示为$sum(L) = foldr(+, 0, L)$,我们很自然希望连接可以表示为: @@ -2912,7 +2912,7 @@ \subsection{find和filter} \right. \ee -为了得到线性时间的命令式算法,我们可以逆序构造结果列表,然后在执行一次$O(n)$时间的反转操作(参考前面的内容)获得最终的结果。我们将这一实现留给读者作为练习。 +为了得到线性时间的命令式算法,我们可以逆序构造结果列表,然后再执行一次$O(n)$时间的反转操作(参考前面的内容)获得最终的结果。我们将这一实现留给读者作为练习。 从右向左构造结果列表的事实,提示我们可以通过右侧fold来实现filter。我们需要定义一个组合函数$f$,使得$filter(p, L) = foldr(f, \phi, L)$。函数$f$接受两个参数,一个是列表中的元素;另一个是从右侧开始构建的中间结果。我们可以这样定义$f(x, A)$,它检查$x$是否满足条件,如果满足就将结果更新为$cons(x, A)$,否则结果$A$保持不变。 @@ -2946,7 +2946,7 @@ \subsection{find和filter} f x xs = if p x then x : xs else xs \end{lstlisting} -和映射与fold相同,filter也是一个通用的概念,我们可以对任何可遍历的数据结构应用一个判定条件,获得我们感兴趣的信息。读者可以参考\cite{learn-haskell}中关于幺半群的内容。 +与映射、fold相同,filter也是一个通用的概念,我们可以对任何可遍历的数据结构应用一个判定条件,获得我们感兴趣的信息。读者可以参考\cite{learn-haskell}中关于幺半群的内容。 \subsection{匹配} \index{列表!matching} @@ -2971,9 +2971,9 @@ \subsection{匹配} \right. \ee -显然这是一个线性时间的算法。但是我们不同用同样的方法来检查一个列表是否是另一个的后缀。这是因为定位到两个列表的尾部,然后从右向左前进的代价很大。与数组不同,由于数组支持随机访问,因此可以从后面开始遍历。 +显然这是一个线性时间的算法。但是我们不能用同样的方法来检查一个列表是否是另一个的后缀。这是因为定位到两个列表的尾部,然后从右向左前进的代价很大。与数组不同,由于数组支持随机访问,因此可以从后面开始遍历。 -由于我们只需要是、否这样的答案,为了实现一个线性时间的后缀检查算法,我们可以将两个列表都反转(反转是线性时间的),然后使用前缀检查算法进行判断。如果$P$是$L$的前缀,记$L \supseteq P$。 +由于我们只需要是、否这样的答案,为了实现一个线性时间的后缀检查算法,我们可以将两个列表都反转(反转是线性时间的),然后使用前缀检查算法进行判断。如果$P$是$L$的后缀,记$L \supseteq P$。 \be L \supseteq P = reverse(P) \subseteq reverse(L) @@ -3006,7 +3006,7 @@ \subsection{匹配} \right. \ee -这里有一个细节值得注意。若$P$为空,显然$P$是任何其他列表的中缀。这种情况实际上由上式中的第一条处理,这是因为空列表同样是任何列表的前缀。在大多数支持模式匹配的语言中,我们不能把上式中的第二条作为第一个边界条件,否则在计算$infix?(\phi, \phi)$时,结果将为false。(Prolog是一个例外,但是这设计语言特性,我们不在这里详细讨论。) +这里有一个细节值得注意。若$P$为空,显然$P$是任何其他列表的中缀。这种情况实际上由上式中的第一条处理,这是因为空列表同样是任何列表的前缀。在大多数支持模式匹配的语言中,我们不能把上式中的第二条作为第一个边界条件,否则在计算$infix?(\phi, \phi)$时,结果将为false。(Prolog是一个例外,但是这涉及语言特性,我们不在这里详细讨论。) 由于前缀检测需要线性时间,并且在遍历时被不断调用,这一算法的复杂度为$O(nm)$,其中$n$和$m$分别是待匹配列表和目标列表的长度。即使将数据结构从链表换成支持随机索引的数组,我们也没有简单方法能将这种“逐一检查”的扫描算法提高到线性时间。 @@ -3029,10 +3029,10 @@ \subsection{匹配} \begin{Exercise} \begin{itemize} -\item 选择编程语言,用命令式和函数式的方法实现线性存在检查程序。 +\item 选择编程语言,用命令式和函数式的方法实现线性时间的存在检查程序。 \item 选择一门命令式语言,实现look-up算法。 \item 实现线性时间的filter算法,首先将结果列表逆序构建,然后在将其反转。请用循环和尾递归这两种方法进行实现。 -\item 选择一门命令式语言,实现签注检查算法。 +\item 选择一门命令式语言,实现前缀检查算法。 \item 给定一个列表,枚举出它的所有后缀。 \end{itemize} \end{Exercise} @@ -3061,7 +3061,7 @@ \section{zip和unzip} \right. \ee -这一算法还可以处理两个列表长度不同的情况。结果列表将和较短的一个长度相同。在支持惰性求值的环境中,还可以用这一算法将一个无穷列表和一个有限列表zip到一起。下面给出了另外一种初始化$n$盏灯状态的方法。 +这一算法还可以处理长度不同的列表。结果列表的长度将和较短的一个相同。在支持惰性求值的环境中,还可以用这一算法将一个无穷列表和一个有限列表zip到一起。下面给出了另外一种初始化$n$盏灯状态的方法。 \[ zip(\{0, 0, ...\}, \{1, 2, ..., n\} @@ -3152,7 +3152,7 @@ \section{zip和unzip} unzip = foldr \(a, b) (as, bs) -> (a:as, b:bs) ([], []) \end{lstlisting} -zip和unzip的概念可以推广到更一般的情况,而不限于列表。例如可以将两个列表zip成一棵树,树中存储的数据是成对的值,分别来自两个列表。抽象的zip和unzip还可以用于耿总复杂的数据的遍历路径,从而模拟命令式环境中的父节点指针。读者可以参考\cite{learn-haskell}中的最后一章。 +zip和unzip的概念可以推广到更一般的情况,而不限于列表。例如可以将两个列表zip成一棵树,树中存储的数据是成对的值,分别来自两个列表。抽象的zip和unzip还可以用于跟踪复杂数据结构的遍历路径,从而模拟命令式环境中的父节点指针。读者可以参考\cite{learn-haskell}中的最后一章。 \begin{Exercise} \begin{itemize}