最近学Clojure,但不知道怎么写一个矩阵转置函数,我想Clojure,Emacs Lisp都是Lisp方言,应该有相似的地方,有哪位能用Emacs Lisp写一个函数作为参考吗,用Clojure那是最好的了
(defun my-transpose (matrix)
(apply #'cl-mapcar #'list matrix))
(my-transpose '((1 2 3) (4 5 6)))
;; => ((1 4) (2 5) (3 6))
(defn transpose [m]
(apply mapv vector m))
(let [m [[1 2 3]
[4 5 6]
[7 8 9]]]
(transpose m))
;; => [[1 4 7] [2 5 8] [3 6 9]]
但是直接用vector,可能不是很好,如果你真的要做矩阵的运算的时候。 考虑下这些选项。
写一个标准实现:
(mapcar '1+ (3 4 5) )
;; => (4 5 6)
(defun transpose (mat)
(if (eq (car mat) nil)
'()
(cons (mapcar 'car mat)
(transpose (mapcar 'cdr mat)))))
(transpose '((1 2 3)
(4 5 6)
(7 8 9)
(10 11 12)))
;; =>
;; ((1 4 7 10)
;; (2 5 8 11)
;; (3 6 9 12))
爆栈警告.jpg
那个,不自量力地写了一个
(defn transpose [m]
(loop [temp (map vec m) res []]
(if (every? empty? temp)
res
(recur (map rest temp)
(conj res (vec (map first temp)))))))
(vec (map -> (mapv
这个代码的目的只是表达算法本身,而不是为了提供一个经过优化后的算法。
不想爆栈的话,把它改成迭代好了,无非是增加一个累加器而已,但是可读性会降低,并且代码里会出现append
或者reverse
(然后你肯定还会问我为什么要调用append或reverse。。。最终你将不得不使用mutable)
有个函数叫 zip
迭代版:
(defun transpose (mat)
(defun iter (acc rest)
(if (eq (car rest) nil)
acc
(iter (append acc (list (mapcar 'car rest))) (mapcar 'cdr rest)))
)
(iter '() mat))
(transpose '((1 2 3)
(4 5 6)
(7 8 9)
(10 11 12)))
;; =>
;; ((1 4 7 10)
;; (2 5 8 11)
;; (3 6 9 12))
我们可以明显的看到:
和递归版相比,迭代版必须使用一个辅助函数iter,以及一个累积变量acc。同时,由于list的结合性问题,需要一个append操作,可读性不如递归版来高。
PS:这个append其实也是可以消除的,方法是使用reverse,虽然能提高性能,但可读性会进一步下降。
请问你说的zip
,在这里又是解决什么问题呢?
懒得翻文档了吗?你们根本不需要定义一个叫 transpose
的函数,直接调用 zip
就完事了。
像Python里面有,但是Clojure和Elisp里面没有zip的。
我知道zip,我的意思是:zip在这里怎么解决爆栈的问题呢?
zip和我提供的算法之间有什么本质区别?
PS:@cirue 提供的 (apply #'cl-mapcar #'list matrix)
本质上就是zip,它同样会爆栈。
你@都能@歪来。
cl-mapcar
里面用的是循环,换算成你们纯洁的函数式就是尾递归。不过我们是反复push然后nreverse,所以虽然看似有野蛮的副作用,但他就是不会爆栈,而且暴露给外界的表现还是pure的,气不气?
突然好奇一个问题,map 会爆栈吗?apply map 会爆栈吗
Elisp的话,dash
有个-zip
,不过设计有失误,对于两个列表的情况会被zip成cons pair,所以不太好用。
clojure的zip似乎不是解决这个的
@glgl-schemer 说的可能是 scheme srfi-1
里的 zip
但是与cl-mapcar
类似,zip
同样是一个不定长参数的函数。
你可以这么写:
(zip '(1 2 3) '(4 5 6) '(7 8 9))
>> '((1 4 7) (2 5 8) (3 6 9))
但不能这么写:
(zip '((1 2 3) (4 5 6) (7 8 9)))
>> '(((1 2 3)) ((4 5 6)) ((7 8 9)))
使用scheme里的zip
实现transpose
,需要使用apply
:
(apply zip '((1 2 3) (4 5 6) (7 8 9)))
>> '((1 4 7) (2 5 8) (3 6 9))
Python里也有一个zip
,但同样不能直接用,原因和scheme类似。
>>> zip([1,2,3],[4,5,6],[7,8,9])
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
解决方法是使用zip*
>>> zip*([1,2,3],[4,5,6],[7,8,9])
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
但是Python返回的list里的元素是tuple,而不是list,所以还要再做后处理
>>> map(list, zip(*a))
[[1, 4, 7], [2, 5, 8], [3, 6, 9]
Haskell里倒是提供了一个"真正的zip
",它直接被命名为transpose
。
有点意思
(This is just a spam message)
最近我又重新思考了这个问题,有了一些新的理解。
之前的回答有一些错误(已删除):
-
map
在大部分语言里都是不会爆栈的map
作为一个primitive操作,大部分语言都会对其做优化。一般不会傻乎乎的写成这样:
```scheme (define (map f list) (if (null? list) nil (cons (f (car list)) (map f (cdr list))))) ```
这样写,在某些strict language里是会爆栈的,但是在lazy language里不会爆栈。
然而,即使是strict language,即使你把
map
写成这个样子,语言的实现仍然可以做成不爆栈,比如:使用 trampolined Interpreter换句话说,首先语言的实现得先使用栈,其次才谈的上爆栈的问题。
-
map
不会爆栈 和map
是否可以看成并行函数 无关比如:
foldr
是不能并行的,但是它仍然可以做成不爆栈的版本。 -
foldr
操作 和 是否会爆栈 无关很多人认为
foldr
会爆栈,而foldl
不会,这种观点错误(见鬼,我之前也是这么认为)。之所以
foldr
会带给人这样的错觉,是因为在foldr op v
这个代码里,op
往往能合并值。由于foldr是右结合,它必须不断求值 list 直到 nil 之后才能开始 reduce,这导致了大量临时变量被 push 到栈上(相比foldl
是左结合,每一次递归它都可以进行一次合并,因此不会有额外的内存消耗)然而,它们均与爆栈无关。
这里仍然包含两层意思:
(1) 首先你的语言实现得使用栈
(2) 即使语言实现使用了栈,
foldr
的实现仍然可以做成不爆栈的版本。比如:foldr
函数可以通过两次应用foldl
实现:先reverse
,再foldl
(或者,高级一点,也可以通过composef
函数的部分应用实现)
换句话说,我在4楼写的那个函数,确实是有可能爆栈,因为elisp是strict language,并且语言的实现使用了栈,从这个角度讲 @cireu 说的并没有错。
补充一下:
这里所说的“爆栈”取自于5楼和16楼的说法,这不是一个精确的词汇。
一般有两种理解:
(1) 如果op
能合并值,尽早合并值,避免不必要的内存消耗。
(2) 如果语言的实现使用了栈结构,避免临时内存被分配在栈上。
这两种理解在不同的语言上有着不同的操作手法。
以上主要是针对(2)来说的。
对于(1)
在strict language里,如果op
能合并值,则foldl
能尽早合并值。
在lazy language里(如:Haskell),同样情况下直接使用foldl
是不行的,注意:我上面有提到“尽早”,所以你必须使用strict版本的foldl
,即foldl'
。
在lazy language里,如果op
不会合并值,则foldr
能够最大程度的利用laziness,而foldl
不行。
当然,这些都与具体数据结构和算法有关,比如:list本身是右结合,然后你的算法本身也要求右结合,在这种情况下,即便op
可以合并,你也只能使用foldr
。
对于(2)
首先得确保你的语言实现使用了栈,如果语言的实现没有使用栈,则根本没必要考虑(2)。
比如:你想在Haskell里实现一个map
,你应该直接这么写:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x : xs) = f x : map f xs
不要自作聪明把result累积在参数上写成尾递归,这样反而更糟。。。
其次,如果语言的实现使用了栈,那么尽量使用语言和库提供的函数,而不要自己写。 因为这些语言和库提供的函数已经考虑到了(2)中的优化问题。