ANSI Common Lisp例子中一个Bug

ANSI Common Lisp第4章第60页给出了一个二分查找的程序,程序代码如下
(defun bin-search (obj vec)
  (let ((len (length vec)))
    (and (not (zerop len))
         (finder obj vec 0 (- len 1)))))

(defun finder (obj vec start end)
  (let ((range (- end start)))
    (if (zerop range)
        (if (eql obj (aref vec start))
            obj
            nil)
        (let ((mid (+ start (round (/ range 2)))))
          (let ((obj2 (aref vec mid)))
            (if (< obj obj2)
                (finder obj vec start (- mid 1))
                (if (> obj obj2)
                    (finder obj vec (+ mid 1) end)
                    obj)))))))
我把这个程序敲下来运行时发现如果查找的对象小于向量中的最小的对象程序会发栈溢出,但查找的对象大于向量中最大的数时程序返回NIL。
[11]> (setf vec (vector '1 '2 '3 '4 '5 '6 '7 '8 '9))
#(1 2 3 4 5 6 7 8 9)
[12]> (bin-search 0 vec)

*** - Program stack overflow. RESET
[13]> (bin-search 10 vec)
NIL
既然是栈溢出,则很有可能是递归的时候一直满足不了终止条件,而终止条件的判断是这一句:
(if (zerop range)
则很有可能是range从1直接变成了-1。
由于round函数在第四章之前还没有出现过,根据程序代码只能猜到它的作用大体是返回浮点数的整数部分,而向上取整还是向下取整则不清楚,于是试验了一下:
[15]> (round (/ 1 2))
0 ;
1/2
[16]> (round (/ 3 2))
2 ;
-1/2
果然向上取整还是向下取整不唯一,查了一下书的Index,在第9章的145页有对round函数的简单介绍,它向离它最近的整数取整,在浮点数小数部分为0.5时向偶数取整。问题清楚了,在上例中查找0时最后一次时start=0, end=1, mid=0,下一次递归时递归的边界为0和mid减1,即为start=0, end=-1于是就一直无法满足终止条件了。
验证一下,在
(let ((mid (+ start (round (/ range 2)))))
代码之下添加两句
(format t "start:~A, end:~A, mid:~A, ~A~%" start end mid (subseq vec start (+ end 1)))
(read)
再运行:
[17]> (bin-search 0 vec)
start:0, end:8, mid:4, #(1 2 3 4 5 6 7 8 9)
1
start:0, end:3, mid:2, #(1 2 3 4)
1
start:0, end:1, mid:0, #(1 2)
1
start:0, end:-1, mid:0, #()
1
start:0, end:-1, mid:0, #()
同原来想的一样。
上这本书本作者的 个人主页上看了一下,这本书的 勘误表里面已经有这一条:
p. 60. The version of bin-search in figure 4.1 blows up if you give it an obj which is smaller than the smallest element in vec. Reported by Richard Green.
群众的眼睛是雪亮的。
看来就算是大师还是会犯这种简单的错误的。

你可能感兴趣的:(二分查找,bug,lisp,PaulGraham)