如何尽可能有效地将一个元素追加到列表中

发布于 2025-01-15 10:02:07 字数 835 浏览 3 评论 0原文

CLOJURE 代表勇敢和真实 的第 4 章有一个练习:创建一个 append 函数,将新条目附加到列表中。

最有效的方法是什么?

根据我对数据类型的一般理解,如果 conj 将元素添加到列表中,那就意味着一致地附加到列表要么是愚蠢的,要么选择使用列表类型是愚蠢的。

无论如何,我写的解决方案是这样的

(defn append
  [lst item]
  (into '() (conj (into '() lst) item)))

嗯,这实际上与我相信的相同

(defn append
  [lst item]
  (reverse (conj (reverse lst) item)))

,所以可能成本很高,因为我将列表反转了两次?

我能想到的另一种解决方案是

(defn append
  [lst item]
  (apply list (conj (apply vector lst) item)))

但它们似乎都遍历了值序列两次,所以我不明白为什么任何一个应该比另一个更好。

是否有完成任务的正确方法?

At the end of CLOJURE for the BRAVE and TRUE's Chapter 4 there's an exercise: make an append function that appends a new entry to a list.

What's the most efficient way to do so?

From what I understand of datatypes in general, if conj prepends elements to a list, that simply means that consistently appending to a list is either silly or the choice of using a list type was silly.

Anyway, the solution I've written is this

(defn append
  [lst item]
  (into '() (conj (into '() lst) item)))

Well, that's actually the same as

(defn append
  [lst item]
  (reverse (conj (reverse lst) item)))

I believe, so probably is costly because I reverse the list twice?

Another solution I could think of is

(defn append
  [lst item]
  (apply list (conj (apply vector lst) item)))

But they all seem to traverse the sequence of values twice, so I don't see why any one should be better than another.

Is there the proper way to accomplish the task?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

数理化全能战士 2025-01-22 10:02:07

conj 在列表的开头而不是末尾添加新项目是有原因的。因为你必须遍历列表才能在其末尾添加一些内容。这不是很有效。这是因为链表的性质。这就是为什么在 Lisp 中你将新项目cons放入列表中,并在最后反转它。这样,您在构建列表时只需遍历该列表一次。

在 Clojure 中,如果要在末尾添加到列表,不使用列表而是使用向量类型更为惯用。在向量上,conj 将向右添加到末尾。

但假设您想要遍历一个列表一次并添加到末尾。

实际上是这样的:

(defn my-append [lst item] (concat lst (list item)))

(my-append '(1 2 3) 4)
;; (1 2 3 4)

但正如我所说,如果你想在末尾重复添加,不要使用列表,而是使用向量和 conj 到末尾。

;; more idiomatic clojure in thisst to add something at its end. This is not very efficient. And it is because of the nature of a linked list. Tha case

(def v [1 2 3])

(conj v 4) ;; [1 2 3 4] ;; unbeatable in efficience, because no traversal!

;; and convert to a list
;; e.g.
(def l (conj v 4))
(seq l)    ;; (1 2 3 4)

There is a reason, why conj adds a new item at the start of the list and not at its end. Because you have to traverse the list to add something at its end. This is not very efficient. And it is because of the nature of a linked list. That's why in lisp you cons new items onto a list and at the very end reverse it. This way, you traverse the list just once while building it.

In Clojure, if you want to add to a list at the end, it is more idiomatic not to use a list but instead the vector type. On a vector, conj adds right to the end.

But let's say, you want to traverse a list once and add to the end.

That is actually:

(defn my-append [lst item] (concat lst (list item)))

(my-append '(1 2 3) 4)
;; (1 2 3 4)

but as I said, if you want to add repeatedly at the end, don't use a list, but a vector and conj to the end.

;; more idiomatic clojure in thisst to add something at its end. This is not very efficient. And it is because of the nature of a linked list. Tha case

(def v [1 2 3])

(conj v 4) ;; [1 2 3 4] ;; unbeatable in efficience, because no traversal!

;; and convert to a list
;; e.g.
(def l (conj v 4))
(seq l)    ;; (1 2 3 4)

岁月无声 2025-01-22 10:02:07

为了避免双重遍历,您可以使用经典的递归方法。像这样:

(defn append [lst item]
  (loop [res [] lst lst]
    (if (seq lst)
      (recur (conj res (first lst))
             (rest lst))
      (seq (conj res item)))))

所以,它只是从头开始重建列表。
为了使其在 clojure 中的性能更好,您可以通过瞬态集合对其进行优化:

(defn append' [lst item]
  (loop [res (transient []) lst lst]
    (if (seq lst)
      (recur (conj! res (first lst))
             (rest lst))
      (seq (persistent! (conj! res item))))))

但是,是的,正如另一个答案所建议的那样,您应该为每个特定任务仔细选择正确的数据结构,因此在实践中您会想要使用向量。

concat 变体开始,它是免费的(因为它是惰性的),但它有一个已知的陷阱,即应用大量堆叠惰性函数时出现堆栈溢出:

user> (defn append-concat [lst item]
        (concat lst [item]))

user> (reduce append-concat () (range 1000000))
;; Error printing return value (StackOverflowError) at clojure.lang.LazySeq/sval (LazySeq.java:42).

to avoid the double traversal, you could use the classic recursive approach. Something like this:

(defn append [lst item]
  (loop [res [] lst lst]
    (if (seq lst)
      (recur (conj res (first lst))
             (rest lst))
      (seq (conj res item)))))

so, it just rebuilds the list from scratch.
To make it's performance better in clojure, you can optimize it with transient collection:

(defn append' [lst item]
  (loop [res (transient []) lst lst]
    (if (seq lst)
      (recur (conj! res (first lst))
             (rest lst))
      (seq (persistent! (conj! res item))))))

but yes, as another answer proposes, you should carefully pick the proper data structure for every specific task, so in practice you would want to use vector.

As of concat variant, it comes for free (since it is lazy), but it has this known pitfall, which is stack overflow on applying a lot of stacked lazy functions:

user> (defn append-concat [lst item]
        (concat lst [item]))

user> (reduce append-concat () (range 1000000))
;; Error printing return value (StackOverflowError) at clojure.lang.LazySeq/sval (LazySeq.java:42).
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文