从混乱到有序

按语:我在一锅汤里为「不懂编程的人」写了这一系列文章的第十篇,整理于此。它的前一篇是《长长的望远镜》,做为《无名》一篇的补充,介绍了 Emacs Lisp 的动态域与词法域。编程

警告,这一篇虽然很长,不过,基本上想看到哪就看到哪,随便看点,都算是对 Emacs Lisp 又多了一点了解。segmentfault

有这么一个列表:ide

(list 3 1 0 8 10 9 7 5 4 999 30)

因为列表中都是数字原子,没有须要求值的列表,所以也能够将它写成函数

'(3 1 0 8 10 9 7 5 4 999 30)

咱们一眼能够看出,这个列表中的数字原子的排列没有顺序。假若咱们很在乎这个列表,它就可能会让咱们有点不舒服。咱们更愿意看到像测试

'(0 1 3 4 5 7 8 9 10 30 999)

或者设计

'(999 30 10 9 8 7 5 4 3 1 0)

这样的列表。咱们认为这样的列表是有序的,更易于记忆。不妨试验一下,用 7 秒的时间可能很难记住上面的那个没有顺序的列表,可是用一样的时间很容易记住有序的列表。code

用 Emacs Lisp 语言如何编写一个可以将混乱的数字列表转化为有序的列表的程序呢?对象

向量

任何排序,第一步是给要排序的对象创造一个有序的空间,并且保证可以瞬间访问或修改这个空间的任一位置上的元素。在 Emacs Lisp 里,这样的空间称为向量。排序

像使用 list 函数构造列表那样,vector 能够构造向量:递归

(vector 1 2 3 (+ 5 4) (list 1 2 3))

这个表达式的求值结果为:

[1 2 3 9 (1 2 3)]

这是一个由数字原子与列表构成的向量。

要访问向量中的任一位置上的元素,可使用 aref 函数。例如:

(aref (vector 1 2 3 (+ 5 4) (list 1 2 3)) 4)

能够访问向量的第 5 个位置的元素,结果为列表 (1 2 3)

aref 接受的参数是 4,为何能够访问向量的第 5 个位置呢?这是由于向量的位置编号是从 0 开始的,因此编号为 4 的位置是第 5个位置。

务必记住,向量的位置编号是天然数,而天然数是从 0 开始。

下面这个表达式

(aref (vector 1 2 3 (+ 5 4) (list 1 2 3)) -1)

它的求值结果是什么?Emacs Lisp 解释器会报错,说参数超出向量的范围。由于向量的位置编号是天然数,而天然数里没有负数。

下面这个表达式

(aref (vector 1 2 3 (+ 5 4) (list 1 2 3)) 5)

它的求值结果是什么?Emacs Lisp 解释器会报错,说参数超出向量的范围。由于这个向量里面只有 5 个元素,没有第 6 个。

务必记住,访问向量中的元素,不能超出向量的长度。

要肯定一个向量有多长,能够用 length 函数。例如:

(length (vector 1 2 3 (+ 5 4) (list 1 2 3)))

结果为 5。

注:length 也能用于肯定一个列表的长度。

向量中某个位置上的元素不只可以访问,也能修改,须要使用 aset 函数。例如,将上述向量的第 3 个位置上的元素修改成布尔值 t

(setq v (vector 1 2 3 (+ 5 4) (list 1 2 3)))
(aset v 2 t)
v

对向量 v 的求值结果为 [1 2 t 9 (1 2 3)]

在不知道向量中具体包含哪些元素,只知道向量的长度以及向量元素的类型时,可使用 make-vector 函数构造向量。例如:

(make-vector 7 0)

求值结果为 [0 0 0 0 0 0 0]。上述的参数值 0 是向量元素的初始值,可根据须要自行选择其余类型的值。

从列表到向量

要对一个只包含数字原子的列表进行排序,须要将列表中的元素放入与列表等长度的向量里面。如今终于有了一个有助于咱们深入理解列表的好机会。

首先,创造与列表等长度的向量:

(make-vector (length a-list) 0)

有了这样的向量,就能够将列表中的元素一个一个放进向量里:

(defun list-to-vector (src dest n)
  (if (not src)
      dest
    (progn
       (aset dest n (car src))
       (list-to-vector (cdr src) dest (+ n 1)))))

这个函数定义里出现了几个以前从未提到的函数,carcdr 以及 not

对于任何列表访问任务而言,它们是最基本的函数。car 能够访问列表的第一个元素,cdr 能够访问列表的第一个元素以后的部分。假若将列表比喻为毛毛虫,那么 car 能够访问毛毛虫的头部,而 cdr 则能访问毛毛虫的身体。

not 函数用于对一个布尔值取反,即将真(t)变成假(nil),将假变成真。在上述函数的定义中,用 (not src) 来测试 src 是否为空的列表(即 nil'())。这样作,虽然没有错误,可是却有些别扭,更地道的办法是 (null src)

carcdr 以及 null 链接到一个发动机上,就能够访问整个链表:

(defun 周游列表 (列表)
  (if (null 列表)
      <求值结果>
    (progn
      <(car 列表) 参与的运算>
      (周游列表 (cdr 列表)))))

在这个发动机的运转过程当中,它接受的参数是一个在 cdr 函数做用下不断缩小的列表,在它的内部则由 car 函数取出的列表首元素能够参与各类运算。上述的 list-to-vector 就是这种形式的发动机。

上述的代码片断

(if (null 列表)
   <求值结果>

用于断定对列表的访问过程是否终止。之因此要做这种形式的判断,是由于 周游列表 的求值逻辑是每次取出列表的首元素以后,就让剩余元素参与下一轮的运算,这个过程至关于 周游列表 每次轮回都会「砍掉」列表的首部元素,只要列表的长度有限,终将会出现无首部元素可砍的空表,而对一个空表取反,结果为真。

无首部元素可砍的表,其实是一个空表,即 '(),它与 nil 等价,而 (null nil) 的结果一定为真。因此,对于一个有限长度的列表,在 carcdr 以及一个周而复始的函数的做用下,老是能够用上述的条件表达式来断定这个周而复始的过程是否应当就此终止。

能够用下面的语句验证 list-to-vector 函数是否能正确运行:

(setq src '(3 1 0 8 10 9 7 5 4 999 30))
(setq dest (make-vector (length src) 0))
(list-to-vector src dest 0)

求值结果应该是 [3 1 0 8 10 9 7 5 4 999 30]

变量

看一下上一节最后出现的语句:

(setq src '(3 1 0 8 10 9 7 5 4 999 30))
(setq dest (make-vector (length src) 0))
(list-to-vector src dest 0)

setqsrc 与一个列表进行了绑定,将 dest 这与一个向量进行了绑定。srcdest 都是符号,而 setq 彷佛能够将它们与任何一种表达式进行绑定。所以,咱们能够用有限的符号去绑定的无限多的表达式。

从效用上来看,这些符号与函数所接受的参数类似。既然咱们能够将函数的参数视为变量,就应该将这些符号也视为变量才对。再者,因为函数自己能够做为参数传递给其余函数,这意味着函数其实也是变量。定义一个函数,本质上不过是 defun 将一个符号与一个表达式绑定了起来。

变量与函数,不必分得太过于清楚。一个符号,与任意一个表达式存在绑定关系,那么这个符号就是变量。setq 的工做就是将更换一个符号的绑定对象。

从如今开始,除了定义函数以外,我会将符号与表达式绑定起来的这种行为称为定义变量。

Emacs Lisp 语言具备更紧凑的变量定义语法——let 表达式。它的用法能够经过改写上述的三个分离的表达式得以充分体现,即:

(let ((src '(3 1 0 8 10 9 7 5 4 999 30))
      (dest (make-vector (length src) 0)))
      (list-to-vector src dest 0))

let 表达式的通常性结果以下:

(let (<变量 1>
      <变量 2>
      <.....>
      <变量 n>)
  <使用上述变量的表达式>)

不过,上面的 let 表达式是没法求值的。由于在定义 dest 变量时,它引用了变量 src,而 src 的定义必须在 let 语句的第一个表达式求值时才是一个有定义的变量。这就是说,你不能左脚踩着右脚或右脚踩着左脚来提高本身。

将上述代码中的 let 改为 let* 就能够了。务必记住,当变量定义列表中不存在变量之间的引用时,用 let,不然用 let*

为啥不所有使用 let* 呢,由于它作功比 let 多,更耗能。

使用 let/let* 的意义在于,能够将一组变量汇集到一块儿,放在一个局部的环境里。这样,在这个局部环境以外,即便有存在同名的变量,它们也与这个局部环境内部的变量无关。

let/let* 彷佛具备一种神秘的力量,但其实它们的原理很是简单。像下面这样的 let 表达式:

(let ((a 1)
      (b 2)
      (c 3))
  (+ a b c))

它其实是

(funcall (lambda (a b c) (+ a b c)) 1 2 3)

像下面这样的 let* 表达式:

(let* ((a 1)
      (b 2)
      (c (+ a b)))
  (+ a b c))

它其实是

(funcall (lambda (a)
           (funcall (lambda (b)
                      (funcall (lambda (c)
                                 (+ a b c)) (+ a b))) 2)) 1)

至于 Emacs Lisp 解释器是如何将 let/let* 变换成上述的匿名函数形式的,这须要了解 Emacs Lisp 宏。关于宏,这须要一篇专门的文章来说述它。

最简单的排序方法

通过一番跋涉,见识了 Emacs Lisp 世界里的一些景观,如今再回到一开始的问题:将混乱的数字列表转化为有序的列表。

经过 list-to-vector 函数,咱们可以将一个列表转化为与它等长度的向量。所以,如今的问题能够转化为让向量变得有序。之因此要作这样的转化,是为了让程序更省功。经过上文所介绍的访问列表中每个元素的方法,想必你已经看到了,要访问列表中的某个元素,一般须要 周游列表 函数运转屡次,才能找到那个元素,然而要访问向量中的任意一个位置上的元素,却能够瞬间完成。因为在排序过程当中,免不了频繁访问一些元素,在这方面向量远胜列表。

怎样对向量里面的数字原子进行排序呢?

不要着急。在计算机里编程,对一组数字进行排序,这是个很大的问题。尚健在的计算机科学界的大宗师 Knuth 老先生在他的传世著做《计算机程序设计艺术》的第 3 卷里,专门用了一章全面地讨论了这个问题。大宗师写的东西,通常人是看不懂的。我刚好也可能和你同样,都是通常人。

复杂的看不懂,就本身去琢磨一些简单的方法吧。从顺序是什么开始思考。在我看来,所谓数字的顺序就是将一个数字放到一个恰当的位置上,使得位于它左边的数字不大于它,而位于它右边的数字大于它。

对于向量 [3 1 0 8 10 9 7 5 4 999 30],随便从它里面取出一个数字,例如它的首元素 3。向量里面剩余的数字,要么比 3 大,要么不大于 3 小,假若咱们将全部不大于 3 的数字通通放在 3 的左面,而将全部大于 3 的数字放在 3 的右面,那么就能够认为 3 这个数字已经位于它应该在的位置上了。接下来,咱们用一样的办法去处理位于 3 的左侧的数字与右侧的数字就能够了。

「一样的办法」,看到这几个字,咱们永远应该马上首先想起的是制造一个周而复始的发动机——递归函数。这个递归函数能够像下面这样实现:

(defun sort (x begin end)
  (if (>= begin end)
      x
    (let* ((pivot (divide x begin end begin)))
      (progn
        (sort x begin pivot)
        (sort x (+ pivot 1) end)))))

它接受三个参数,参数 x 是待排序的向量,begin 是向量的起始位置,end 是向量的终止位置。在函数递归求值过程当中,beginend 用于标定向量子域的起始位置与终止位置。

递归求值过程的终止条件是待排序的向量只含有一个元素或向量为空。例如,假若比 3 小的数字只有一个,则无需对 3 的左侧再进行排序了。同理,假若比 3 大的数字只有一个,那么 3 的右侧数字也不必再进行排序。

写一个递归函数,可能许多人是像我上面所说的那样思考的,即如今脑子里构造了一个一层又一层深刻下去的函数求值过程,而后思考这个过程总得有个终点,因而他们就开始思考这个终止条件是什么。之前,我也是这样思考递归的。许多人所以也就以为递归这种东西,彷佛只可意会,不可言传。当我将函数的递归求值过程想象为一个周而复始运转的发动机时,我想明白了这个问题。不是递归不可思议,上面所说的那种思考模式,其实是上帝视角。由于任何递归,在上帝面前(假设真的有这种东西),都是尽收眼底的。当咱们企图开启上帝视角来理解递归,就变成了要在脑子里模拟这个递归过程许屡次,甚至还有人须要在纸上一层一层的把递归过程展开个三五层,而后越看越混乱……咱们不是上帝,因此就不要勉强本身。

实际上,采用 POV(Point of View) 视角来理解递归,会更为直观。这种视角就是从最简单的状况开始,往前走两步步看看,一旦发现出现了重复,就意味着递归出现了。

对于 sort 函数而言,最简单的状况是什么呢?是它接受的向量 x 不包含任何元素,即空向量,或者只含有 1 个元素。对于这样的向量,就不必排序了,直接将 x 原封不动地做为排序过程的求值结果便可。很容易为这种状况写代码,即:

(defun sort (x begin end)
  (cond ((>= begin end) x)))

因为 beginend 分别是 x 的第一个元素与最后一个元素的位置,所以只要 (>= begin end) 为真,向量就一定是空向量或只有 1 个元素的向量。

如今,来看向量中含有两个元素的状况。按照上面所述的排序方法,只须要将 x 的首元素调整到一个合适的位置,让它左边的元素都比它小或与它相等,而右边的元素都比它大就能够了。设这个向量是 [a b]。将 divide 函数做用于它,x 无非变成 [a b][b a] 这两种形式之一。不管是哪种,反正 a 的位置被固定下来了,接下来的问题是分别对 a 左侧与右侧的元素进行排序,这至关于对 x 的两个子集进行排序,而 a 所在的位置正是这两个子集的分割点。这两个子集,所包含的元素数量一定不大于 1,而不大于 1 的向量的排序问题,咱们已经有了解决方案,只须要将那个已有的方案拿出来用就是了,这样,递归就出现了,即:

(defun sort (x begin end)
  (cond ((>= begin end) x)
        ((= (- end begin) 1) ((let ((pivot (divide x begin end begin)))
                                (progn
                                  (sort x begin pivot)
                                  (sort x (+ pivot 1) end)))))))

其中 pivot 就是上面所说的将 divide 做用于 x 以后,所造成的分割点,基于这个分割点能够将 x 分为两个部分,而后交给 sort 函数对它们进行排序。

如今,咱们继续考虑向量中含有 3 个元素的状况,结果发现,处理过程与只有 2 个元素的状况彻底一致,这就意味着不必再费劲了,接下来含有 4 个、5 个……n 个元素的向量,也都是这样处理,所以可将 sort 函数修改成:

(defun sort (x begin end)
  (cond ((>= begin end) x)
        (t ((let ((pivot (divide x begin end begin)))
              (progn
                (sort x begin pivot)
                (sort x (+ pivot 1) end)))))))

接下来,再将 cond 表达式换成 if 表达式,这就与以前的 sort 彻底同样了。

经过这种方式去定义递归函数,必定要在解决了最简单的状况以后,迅速变得足够懒惰,这样很容易发现递归第一次出现的踪影,发现了就抓住它。这其实正是咱们惯常使用的思考方式。还记得守株待兔的故事吧,即:宋人有耕田者。田中有株,兔走触株,折颈而死。因释其耒而守株,冀复得兔。兔不可复得,而身为宋国笑。这个宋国的农民太过于着急了,至少得再等到 2 天看看还能不能在原来的地方捡到死兔子,再考虑将待兔做为职业。这种思考递归的方法其实咱们好久之前就学过,数学概括法。

真正有些麻烦的是 divide 函数的实现。divide 函数应当以向量 x 的首个元素为枢纽,将比这个元素小的元素旋转到向量的左部,而将比这个元素大的元素旋转到向量的右部,而且将枢纽所在的位置做为求值结果。

假若你曾经玩过扑克牌,不妨将 divide 函数视为洗牌的拟过程。最多见的洗牌方法是双手各执一组牌,而后让它们交错合并为一组。divide 就是将合并后的牌再从新分开,只不过是以第 1 张牌为枢纽,让牌面小的围绕枢纽向左旋转,这样牌面大的就天然出如今枢纽的右边了。

下面是 divide 的实现:

(defun divide (x i end location)
  (if (> i end)
      location
    (let* ((pivot (aref x location))
           (xi (aref x i))
           (next (+ location 1)))
      (if (> pivot xi)
          (progn
            (if (> i next)
                (aset x i (aref x next)))
            (aset x location xi)
            (aset x next pivot)
            (divide x (+ i 1) end next))
        (divide x (+ i 1) end location)))))

感受语言有点儿无力。divide 函数其实很机械,它充分利用了 Emacs Lisp 的向量的跨函数的可修改性。它的主体部分是对 x 中的元素的顺序访问过程,只是在这个过程当中对 x 进行了修改,变相地达到了「让牌面小的围绕枢纽向左旋转,这样牌面大的就天然出如今枢纽的右边」的效果。例如将向量 [3 1 0 7 5 2] 传递给 divide 函数,即 (divide '[3 1 0 7 5 2] 0 5 0),这个向量会被修改为 [1 0 2 3 5 7],而且 divide 会返回元素 3 的所在的位置。

下面是测试 sort 可否工做的代码:

(let ((x (let* ((src '(3 1 0 8 10 9 5 7 4 999 30))
        (dest (make-vector (length src) 0)))
       (list-to-vector src dest 0))))
  (sort x 0 (- (length x) 1)))

结果获得 [0 1 3 4 5 7 8 9 10 30 999]

从向量到列表

如今,咱们已经解决了向量的排序问题,而咱们最初要解决的问题是列表的排序。所以,还须要将有序的向量转换为有序列表才算得上功德圆满。不过,假若你可以理解上述的所有代码,这种问题对你而言基本上算不上问题。

试试看:

(defun vector-to-list (src i end dest)
  (if (= i end)
      dest
    (

居然写不下去了。由于咱们还不知道怎样基于向量中的元素逐一添加到一个列表里。

Emacs Lisp 为构造列表提供的最基本的函数是 cons,它能够将一个元素添加到列表的首部。例如:

(cons 0 '(1 2 3))

求值结果为 (0 1 2 3)

要构造只含有 1 个元素的列表,也是能够的,例如 (cons 1 '()),求值结果为 (1)

如今,能够继续写出 vector-to-list 了,

(defun vector-to-list (src i end dest)
  (if (= i end)
      dest
    (vector-to-list src (+ i 1) end (cons (aref src i) dest))))

如下代码可验证这个函数的正确性:

(let ((src '[1 2 3])
      (dest '()))
  (vector-to-list src 0 2 dest))

不过,vector-to-list 目前是将一个升序的向量转化为一个降序的列表。假若但愿所得列表也是升序,须要将这个函数定义为:

(defun vector-to-list (src i dest)
  (if (< i 0)
      dest
    (vector-to-list src (- i 1) (cons (aref src i) dest))))

如下代码可验证其正确性:

(let ((src '[1 2 3])
      (dest '()))
  (vector-to-list src 2 dest))

将列表转化为向量,再对向量进行排序,最后将向量转化为列表,这个过程如今能够用下面的代码来描述:

(let* ((x (let* ((src '(3 1 0 8 10 9 5 7 4 999 30))
                 (dest (make-vector (length src) 0)))
            (list-to-vector src dest 0)))
       (end (- (length x) 1)))
  (vector-to-list (sort x 0 end) end '()))

壳化

如今已经完全的解决了这篇文章开始所提出的那个问题,可是想必你也看到了,最后写出来的那段代码

(let* ((x (let* ((src '(3 1 0 8 10 9 5 7 4 999 30))
                 (dest (make-vector (length src) 0)))
            (list-to-vector src dest 0)))
       (end (- (length x) 1)))
  (vector-to-list (sort x 0 end) end '()))

看上去像个丑陋的怪物。可能到如今,你还没搞明白 vector-to-list 的第二个与第三个参数的含义吧?还有 sort 函数的第二个与第三个参数……这些参数,就像一部电器裸露在外的一些混乱的电线同样,使人生厌或生畏。

咱们能够把它们隐藏起来。隐藏一些东西,最简单的办法为给它制做一个外壳。例如,能够像下面这样,将 sort 函数裸露在外的电线隐藏起来:

(defun vector-sort (x)
  (let ((begin 0)
        (end (- (length x) 1)))
    (sort x begin end)))

再像下面这样,将 vector-to-list 裸露在外的电线隐藏起来:

(defun vector-to-list (x)
  (defun to-list (src i dest)
    (if (< i 0)
        dest
      (to-list src (- i 1) (cons (aref src i) dest))))
  (let ((x-end (- (length x) 1))
        (dest '()))
    (to-list x x-end dest)))

没错,在一个函数的定义里,能够定义一个函数。

如今,使用这两个外壳函数,就能够将丑陋的

(vector-to-list (sort x 0 end) end '())

壳化(我发明的专业术语)为

(vector-to-list (vector-sort x))

并且,新的代码理解起来也很容易,就是对一个向量进行排序,而后再将其转化为列表。

同理,可将 list-to-vector 壳化为:

(defun list-to-vector (x)
  (defun to-vector (src dest n)
    (if (null src)
        dest
      (progn
        (aset dest n (car src))
        (to-vector (cdr src) dest (+ n 1)))))
  (let ((dest (make-vector (length x) 0)))
    (to-vector x dest 0)))

对这三个函数作壳化处理后,那段怪物般的代码片断就变得像下面这样友善可亲了,

(vector-to-list
 (vector-sort
  (list-to-vector '(3 1 0 8 10 9 5 7 4 999 30))))

写程序,尽可能先将程序的功能完整且正确地实现出来,而后再考虑如何让代码更美观。这是个人作法。

如今,有个问题,divide 函数也露出了许多电线,要不要也给它作壳化手术呢?我以为不须要。由于,在逻辑上,它并无暴露在 vector-sort 函数的外部。也就是说,对于要使用 vector-sort 函数对一个向量里的元素进行排序的时候,divide 不可见。不可见的东西,就不必壳化了。这是个人观点。

异编程者言

一组数字就像一组扑克牌的牌面。它之因此混乱,是由于周而复始的洗牌,而它们可以得以恢复顺序,是由于周而复始的逆洗牌。无他,就是让一个周而复始的发动机卷去对两个本身求值,让这两个本身分别处理牌面的一个子集。用这种办法洗牌,就能够获得混乱的牌面。用这种办法排序,就能恢复混乱的牌面。

百川东到海,什么时候复西归?到海是洗牌,西归是排序。这两个结果应该同时存在,不然河流就会枯竭,海水就会上涨。天然界的水循环系统像是一台精密的机器,精确地让河流混乱,又精确地将其复原。

咱们对一组数字进行排序,排序的结果仍是原来的那组数字吗?

人不可能两次踏进同一条河流。

附录

下面是本文所述的排序程序的所有代码,也是我有生以来第一次写这么长的 Lisp 代码。

(defun list-to-vector (x)
  (defun to-vector (src dest n)
    (if (null src)
        dest
      (progn
        (aset dest n (car src))
        (to-vector (cdr src) dest (+ n 1)))))
  (let ((dest (make-vector (length x) 0)))
    (to-vector x dest 0)))

(defun divide (x i end location)
  (if (> i end)
      location
    (let* ((pivot (aref x location))
           (xi (aref x i))
           (next (+ location 1)))
      (if (> pivot xi)
          (progn
            (if (> i next)
                (aset x i (aref x next)))
            (aset x location xi)
            (aset x next pivot)
            (divide x (+ i 1) end next))
        (divide x (+ i 1) end location)))))

(defun sort (x begin end)
  (if (>= begin end)
      x
    (let* ((pivot (divide x begin end begin)))
      (progn
        (sort x begin pivot)
        (sort x (+ pivot 1) end)))))

(defun vector-sort (x)
  (let ((begin 0)
        (end (- (length x) 1)))
    (sort x begin end)))

(defun vector-to-list (x)
  (defun to-list (src i dest)
    (if (< i 0)
        dest
      (to-list src (- i 1) (cons (aref src i) dest))))
  (let ((x-end (- (length x) 1))
        (dest '()))
    (to-list x x-end dest)))

(vector-to-list
 (vector-sort
  (list-to-vector '(3 1 0 8 10 9 5 7 4 999 30))))

下一篇咒语

相关文章
相关标签/搜索