【问题标题】:Common Lisp: Zip arbitrary number of listsCommon Lisp:压缩任意数量的列表
【发布时间】:2018-11-13 05:50:47
【问题描述】:

假设您有一个列表列表,例如'(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr"))'(("ab" "cd" "ef") ("01" "23" "45"))

在给定列表中压缩列表的规范方法是什么? IE。 func 将如何定义

(func '(("ab" "cd" "ef") ("01" "23" "45")) :sep "|" :combiner #'concat) 
  ;; => ("ab|01" "cd|23" "ef|45")

(func '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr")) ...)
  ;; => ("abc|012|jkl" "def|345|mno" "ghi|678|pqr")

其中concat := (lambda (args) ...) 是组合各个列表头部的函数。

大概,这种类型的操作称为rotationzipMany(根据answers不同语言的相关问题)。

我有这样的事情 (double-apply)

(apply #'mapcar #'(lambda (&rest args) (apply #'concatenate 'string args)) lst)

lst'(("ab" "cd" "ef") ("01" "23" "45")) 例如。 combiner 将是 concatenate。请注意,此示例实现中没有给出分隔符。

但这似乎非常令人费解。

那么,这种操作的规范实现是什么

【问题讨论】:

    标签: functional-programming common-lisp


    【解决方案1】:

    您的函数有效,但依赖于APPLY。 您可以传递给apply 的列表数量受CALL-ARGUMENTS-LIMIT 的限制,这在给定的实现中可能足够大。但是,您的函数应该接受任意数量的列表,因此,如果您想以可移植方式编写代码,则不应使用 apply

    压缩

    zip 函数组合了列表列表中的所有第一个元素,然后是所有第二个元素等。 如果您输入的列表列表如下:

    '((a b c d)
      (0 1 2 3)
      (x y z t))
    

    然后 zip 将构建以下列表:

    (list (combine (list a 0 x))
          (combine (list b 1 y))
          (combine (list c 2 z))
          (combine (list d 3 t)))
    

    如果您仔细观察,您会发现您可以将正在完成的工作拆分为combine 函数的映射,而不是初始列表列表的转置 (视为矩阵):

     ((a b c d)            ((a 0 x) 
      (0 1 2 3)    ===>     (b 1 y) 
      (x y z t))            (c 2 z) 
                            (d 3 t))
    

    因此,zip 可以定义为:

    (defun zip (combine lists)
      (mapcar #'combine (transpose lists)))
    

    或者,如果您关心重用调用transpose 时分配的内存,您可以使用MAP-INTO

    (defun zip (combiner lists &aux (transposed (transpose lists)))
      (map-into transposed combiner transposed))
    

    转置

    转置列表列表的方法有多种。这里我将使用REDUCE,在其他一些函数式语言中称为fold。在归约的每一步,中间归约函数都会取一个部分结果和一个列表,并产生一个新的结果。我们的结果也是一个列表列表。 下面,我展示了每个减少步骤的当前列表,以及列表的结果列表。

    第一步

    (a b c d)  =>  ((a)
                    (b)
                    (c)
                    (d))
    

    第二步

    (0 1 2 3)  =>  ((0 a)
                    (1 b)
                    (2 c)
                    (3 d))
    

    第三步

    (x y z t)  =>  ((x 0 a)
                    (y 1 b)
                    (z 2 c)
                    (t 3 d))
    

    请注意,元素被推到每个列表的前面,这就解释了为什么每个结果列表都被颠倒了。预期的结果。 因此,transpose 函数应该在执行归约后反转每个列表:

    (defun transpose (lists)
      (mapcar #'reverse
              (reduce (lambda (result list)
                        (if result
                            (mapcar #'cons list result)
                            (mapcar #'list list)))
                      lists
                      :initial-value nil)))
    

    像以前一样,最好避免分配过多的内存。

    (defun transpose (lists)
      (let ((reduced (reduce (lambda (result list)
                               (if result
                                   (mapcar #'cons list result)
                                   (mapcar #'list list)))
                             lists
                             :initial-value nil)))
        (map-into reduced #'nreverse reduced)))
    

    示例

    (transpose '(("ab" "cd" "ef") ("01" "23" "45")))
    => (("ab" "01") ("cd" "23") ("ef" "45"))
    
    (import 'alexandria:curry)
    
    (zip (curry #'join-string "|")
         '(("ab" "cd" "ef") ("01" "23" "45")))
    => ("ab|01" "cd|23" "ef|45")
    

    编辑:其他答案已经提供了join-string 的示例定义。您还可以使用cl-strings 中的join 函数。

    【讨论】:

    • 答案很明确,但join-string 来自哪里?
    • @MartinBuchmann 我认为没有必要再举一个加入的例子,但我编辑了问题以指向一个尚未引用的库。谢谢。
    • 好吧,对于某些人来说这是必要的 ;-) 谢谢!
    【解决方案2】:

    最初,我尝试使用backquote 和拼接,@ 并定义一个宏来摆脱applys。但正如@coredump 指出的那样,这会带来其他问题。

    无论如何,如果你想在连接的元素之间添加分隔符,你需要 Pythonic 函数 join() 作为组合器。 concatenate 如果函数应该按照您希望的方式运行,则会使事情变得更加复杂。

    由于我使用@Sylwester 的非常优雅的join() 定义,我的答案将与他的非常相似。也许我的回答最接近你原来的例子。

    (defun join (l &key (sep ", "))
      (format nil (format nil "~a~a~a" "~{~a~^" sep "~}") l))
    

    使用它,我们可以将您的函数定义为:

    (defun func (lists &key (sep "|") (combiner #'join))
      (apply #'mapcar
             #'(lambda (&rest args) (funcall combiner args :sep sep))
             lists))
    

    或者没有apply - @Rainer Joswig 如何指出 - 以及所有以前的回答者 - 因为它可以接受的参数数量有限制(~50):

    (defun func (lists &key (sep "|") (combiner #'join))
      (reduce #'(lambda (l1 l2)
                  (mapcar #'(lambda (e1 e2) 
                              (funcall combiner (list e1 e2) :sep sep)) l1 l2))
              lists))
    

    或略短:

    (defun func (lists &key (sep "|") (combiner #'join))
      (reduce #'(lambda (l1 l2)
                  (mapcar #'(lambda (&rest l) (funcall combiner l :sep sep)) l1 l2))
              lists))
    

    测试:

    (func '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr"))) 
    ;; ("abc|012|jkl" "def|345|mno" "ghi|678|pqr")
    

    注意:sep "" 使join 等价于函数concatenate

    (func '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr")) :sep "")
    ;; ("abc012jkl" "def345mno" "ghi678pqr")
    

    感谢@Sylwester、@coredump 和@Rainer Joswig!

    【讨论】:

    • 请不要使用宏进行计算。试图用宏摆脱 apply 意味着你最终要么需要调用 eval 要么有文字输入。
    • 是的,我现在看到了一个问题是列表有文字输入...
    • @coredump:所以现在改为一个函数并使用一次apply。感谢您的提示!
    【解决方案3】:

    加入

    (defun %d (stream &rest args)
      "internal function, writing the dynamic value of the variable
    DELIM to the output STREAM. To be called from inside JOIN."
      (declare (ignore args)
               (special delim))
      (princ delim stream))
    
    (defun join (list delim)
      "creates a string, with the elements of list printed and each
    element separated by DELIM"
      (declare (special delim))
      (format nil "~{~a~^~/%d/~:*~}" list))
    

    目标

    不要使用 &rest args 或 apply - 这会限制我们的列表长度。

    递归MAPCARS

    (defun mapcars (f l)
      (if (null (car l))
          '()
        (cons (funcall f (mapcar #'car l))
              (mapcars f (mapcar #'cdr l)))))
    

    迭代MAPCARS

    (defun mapcars (f l)
      (loop for l1 = l then (mapcar #'cdr l1)
            while (car l1)
            collect (funcall f (mapcar #'car l1))))
    

    用法

    CL-USER 10 > (mapcars (lambda (l) (join l "|")) l)
    ("abc|012|jkl" "def|345|mno" "ghi|678|pqr")
    

    【讨论】:

    • 带有递归 mapcar 的好解决方案 :)
    【解决方案4】:

    为什么不在 params 中使用 &rest 并在 lambda 函数中返回它。

    CL-USER> (mapcar (lambda (&rest l) l) '(1 2 3) '(a b c) '("cat" "duck" "fish"))
    ((1 A "cat") (2 B "duck") (3 C "fish")) 
    

    那么你可以像这样使用apply:

    (apply #'mapcar (lambda (&rest l) l) '(("ab" "cd" "ef") ("01" "23" "45"))) 
    ; => (("ab" "01") ("cd" "23") ("ef" "45"))
    

    到这里,普通的 zip 会做什么,现在您想将该子列表加入一个字符串,并带有一些自定义分隔符。

    因为最好的答案是使用格式,也喜欢这个answer 来自stackoverflow这里https://stackoverflow.com/a/41091118/1900722

    (defun %d (stream &rest args)
      "internal function, writing the dynamic value of the variable
    DELIM to the output STREAM. To be called from inside JOIN."
      (declare (ignore args)
               (special delim))
      (princ delim stream))
    
    (defun join (list delim)
      "creates a string, with the elements of list printed and each
    element separated by DELIM"
      (declare (special delim))
      (format nil "~{~a~^~/%d/~:*~}" list))
    

    那么你只需要在这个函数和来自zip的结果列表上再次使用mapcar

    (mapcar (lambda (lst) (join lst "|")) '(("ab" "01") ("cd" "23") ("ef" "45")))
     ; => ("ab|01" "cd|23" "ef|45")
    

    【讨论】:

    • 这很漂亮,像这样使用&rest
    【解决方案5】:

    怎么样:

    (defun unzip (lists &key (combiner #'list))
      (apply #'mapcar combiner lists))
    

    那你应该join:

    (defun join (l &key (sep ", "))
      (format nil (format nil "~a~a~a" "~{~a~^" sep "~}") l))
    

    然后为您的特定用途制作包装器或 lambda:

    (unzip '(("abc" "def" "ghi") ("012" "345" "678") ("jkl" "mno" "pqr"))
           :combiner (lambda (&rest l) (join l :sep "|")))
    ; ==> ("abc|012|jkl" "def|345|mno" "ghi|678|pqr")
    

    【讨论】:

      【解决方案6】:

      对于任意数量的列表,您需要去掉apply #'mapcar,否则列表的数量将受到call-arguments-limit 的限制。

      一种典型的方法是使用reduce,一次合并两个列表:

      (defun zip (list-of-lists &key (combiner #'concat))
        (reduce (lambda (list-a list-b)
                  (mapcar combiner list-a list-b))
                list-of-lists))
      

      如果您不喜欢明确的lambda 形式,您可能会喜欢curry,例如。 G。来自alexandria

      (defun zip (list-of-lists &key (combiner #'concat))
        (reduce (curry #'mapcar combiner)
                list-of-lists))
      

      其他循环结构有loopdodolist,还有一些循环库,例如。 G。 iterate, for.

      【讨论】:

      • 是的,使用reduce 是一个不错的解决方案。好吧,如果我使用循环结构,我将不得不自己管理状态,不是吗?所以,reduce 是(在我尝试过的解决方案中,它也使用了最少的内存,~2Kb)
      猜你喜欢
      • 2011-09-05
      • 2016-12-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-04
      • 1970-01-01
      • 2013-05-16
      相关资源
      最近更新 更多