为任意序列递归实现长度,而不仅仅是列表

时间:2013-10-10 00:21:08

标签: lisp common-lisp

我是Lisp的新手,希望通过递归实现函数length。我编写以下代码只是为了发现它只适用于列表而不适用于字符串。

(defun mylen (l)
  (if (eq (cdr l) nil)
    1
    (+ 1 (mylen (cdr l)))))

我想我是否可以只编写一个既能用于列表又能用于字符串的函数?

4 个答案:

答案 0 :(得分:4)

使用任意序列的序列函数

首先,请注意Common Lisp的length适用于任何序列,而不仅仅是列表(而不仅仅是字符串,& c。)。所以你实际根本不需要写这个。还有一些其他函数通常在序列上运行,例如map。一般来说,如果在列表和向量上都有效,那么编写通用代码就不那么容易了。实现通常通过检查首先提供的序列类型,然后使用专用版本来实现采用任意序列的函数。在可用时使用这些功能对您有利。您可以查看HyperSpec中的17.3 Sequences Dictionary,了解更多可在任意序列上运行的函数。

您可以使用map并实施sequence-length,如下所示:

(defun sequence-length (sequence &aux (length 0))
  (map nil (lambda (x)
             (declare (ignore x))
             (incf length))
       sequence)
  length)

上面的sequence-length突出显示map适用于每个reduce。一个更惯用的解决方案是使用另一个函数来处理序列,1Rainer Joswig points out(此代码基于他的答案中的代码,但使用constantly来创建函数忽略其参数并返回(defun sequence-length (sequence) (reduce '+ sequence :key (constantly 1))) ):

CL-USER> (sequence-length '(1 2 3))
3
CL-USER> (sequence-length "foobar")
6
count-if

另一种选择是在

中使用(count-if (constantly t) sequence)
(constantly t)

计算cons返回true的项目数,即所有项目。

编写自己的序列函数

您正在实现的递归类型确实依赖于(defun list-elements (sequence start end) (if (= start end) '() (cons (elt sequence start) (list-elements sequence (1+ start) end)))) 单元格构建的列表,您可以轻松处理列表的第一个部分,然后休息列表。通常没有函数对序列执行此操作,因为获取向量的其余部分通常意味着获取它的副本。通常,如果需要这样的东西,该函数将采用开始和结束索引,并对这些索引进行递归,而不是对序列进行递归。如,

CL-USER> (list-elements "abcdefghijklmnop" 3 7)
(#\d #\e #\f #\g)
reduce

subseq的大多数实现只是检查序列是列表还是向量,然后调度到以最合适的方式处理这些序列的专用版本。例如,如果你看一下SBCL's implementation of reduce(从第1242行开始),你会看到有四种宏的实现:

  1. 喃喃-减少
  2. 喃喃-减少从 - 端
  3. 列表减少
  4. 列表减少从 - 端
  5. 和一个序列遍历:

    • 减少

    你需要更多地了解SBCL中的定义形式,以了解它们是如何被使用的,但它们确实证明了实现通用序列函数是有点工作的。

    一个低效的第一个元素/序列的其余部分,为了完整性

    在说完所有这些之后,我会指出你可以使用reverse-into-list,它可以在任意序列上运行,在向量和列表上进行这种递归,但它真的效率不高。例如,这是一个简单的(defun reverse-into-list (sequence &optional (result '())) (if (eql 0 (length sequence)) result (reverse-into-list (subseq sequence 1) (cons (elt sequence 0) result)))) 函数。

    CL-USER> (reverse-into-list "hello")
    (#\o #\l #\l #\e #\h)
    CL-USER> (reverse-into-list '(1 2 3))
    (3 2 1)
    
    length

    使用{{1}}确定何时停止,但您可以编写一个更高效的版本,首先检查它是否是列表,如果是,则检查它是否为空(常量时间),如果它不是一个列表,只是调用长度(也应该是常量时间)。

答案 1 :(得分:3)

只是添加约书亚的回答:不是map,请使用reduce

(reduce #'+ sequence :key (constantly 1))

计算字符串的长度在Lisp中是无用的,因为每个向量都将长度存储为属性 - 没有像C中那样的结束标记。因此,向量的长度 - 和字符串是向量 - 不需要在Lisp中计算。

答案 2 :(得分:1)

您的Lisp实现的标准库length函数可能通过访问某些内部属性来处理字符串和其他向量。但是如果你想以递归的方式计算向量的长度,你最好的选择可能是coerce他们到列表。向量本身并不是列表所用的递归数据结构。

顺便说一下,你当前的实现也是列表的错误。对于空列表,它将失败(即返回1而不是0)。您可以通过简化它来使其在空列表中正常工作。

答案 3 :(得分:1)

可以从序列中提取“休息”,如果它是一个向量,则只需要少量:

(defun sequence-rest (seq)
  (if (listp seq)
      (rest seq)
      ;; A full version would also care about element-type etc.
      (make-array (1- (length seq)) :displaced-to seq
                  :displaced-index-offset 1)))

(defun mylen (seq)
  (if (zerop (length seq))
    1
    (+ 1 (mylen (sequence-rest seq)))))