【问题标题】:map part of the vector efficiently in clojure在 clojure 中有效地映射向量的一部分
【发布时间】:2013-12-26 02:24:02
【问题描述】:

我想知道如何在 Clojure 中以惯用且有效的方式做到这一点:

1) 给定一个包含 n 个整数的向量: [A0 A1 A2 A3 ... An]

2) 将最后的 x 项加 1(假设 x 为 100),因此向量将变为: [A0 A1 A2 A3 ... (An-99 + 1) (An-98 + 1)... (A n-1 + 1) (An + 1)]

一个简单的实现如下所示:

(defn inc-last [x nums]
  (let [n (count nums)]
      (map #(if (>= % (- n x)) (inc %2) %2)
           (range n) 
           nums)))

(inc-last 2 [1 2 3 4]) 
;=> [1 2 4 5]

在这个实现中,基本上你只是通过检查每个项目来将整个向量映射到另一个向量,看看它是否需要增加。

但是,这是一个 O(n) 操作,而我只想更改向量中的最后 x 个项目。理想情况下,这应该在 O(x) 而不是 O(n) 中完成。

我正在考虑使用诸如 split-at/concat 之类的函数来实现它,如下所示:

(defn inc-last [x nums]
  (let [[nums1 nums2] (split-at x nums)]
    (concat nums1 (map inc nums2))))

但是,我不确定这个实现是 O(n) 还是 O(x)。我是 Clojure 的新手,不太确定在 Clojure 中对持久数据结构进行 concat/split-at 等操作的时间复杂度是多少。

所以我的问题是:

1) 第二个实现的时间复杂度是多少?

2)如果还是O(n),有没有Clojure中只需要O(x)的惯用高效的实现来解决这个问题?

感谢任何评论。谢谢。

更新:

noisesmith 的回答告诉我 split-at 会将向量转换为列表,这是我之前没有意识到的事实。由于我将对结果进行随机访问(在处理向量后调用nth),我希望有一个有效的解决方案(O(x) 时间)同时保留向量而不是列表,否则nth 会减慢我的速度程序也是如此。

【问题讨论】:

  • 仔细检查您的问题,我惊讶地发现 split-at 的实现不仅在 O(n) 时间内不起作用(预计是因为它是在非关联数据结构上实现的),但它也会遍历前 (count - n) 个元素(或版本中的前 n 个参数,实际上是 inc-first)两次!

标签: algorithm data-structures vector clojure


【解决方案1】:

Concat 和 split-at 都将输入转换为 seq,实际上是链表表示,时间为 O(x)。以下是如何使用向量来实现 O(n) 性能。

user> (defn inc-last-n
        [n x]
        (let [count (count x)
              update (fn [x i] (update-in x [i] inc))]
          (reduce update x (range (- count n) count))))
#'user/inc-last-n
user> (inc-last-n 3 [0 1 2 3 4 5 6])
[0 1 2 3 5 6 7]

这将在非关联输入(如 seq/lazy-seq)上失败,因为在非关联类型中没有 O(1) 访问时间。

【讨论】:

  • 请注意,这正是问题所要求的;也就是说,这个答案的 O(n) 是问题的 O(x)(由于参数名称的不同使用)。可以对这种方法进行调整,以使用更复杂的 update 函数来使用瞬态,然后它会正确使用它们(即以函数方式)。
【解决方案2】:

inc-last 是一个使用transient 的实现,它允许在恒定时间内获得一个可修改的“就地”向量并在恒定时间内返回一个persistent! 向量,它允许在 O( X)。 最初的实现使用命令式doseq 循环,但正如 cmets 中所提到的,瞬态操作可以返回一个新对象,因此最好继续以函数式方式进行操作。

我在对inc-last-2 的调用中添加了doall,因为它返回一个惰性序列,但inc-lastinc-last-3 返回一个向量,因此需要doall 才能比较它们。

根据我进行的一些快速测试,inc-lastinc-last-3 实际上在性能上并没有太大差异,即使对于巨大的向量(10000000 个元素)也是如此。不过,对于inc-last-2 实现,即使对于 1000 个元素的向量,也存在很大差异,仅修改最后 10 个元素,它的速度要慢约 100 倍。对于较小的向量或当n 接近(count nums) 时,差异并没有那么大。

(感谢 Michał Marczyk 提供有用的 cmets)

(def x (vec (range 1000)))

(defn inc-last [n x]
  (let [x (transient x)
        l (count x)]
    (->>
      (range (- l n) l)
      (reduce #(assoc! %1 %2 (inc (%1 %2))) x)
      persistent!)))

(defn inc-last-2 [x nums]
  (let [n (count nums)]
    (map #(if (>= % (- n x)) (inc %2) %2)
         (range n) 
         nums)))

(defn inc-last-3 [n x]
  (let [l (count x)]
    (reduce #(assoc %1 %2 (inc (%1 %2))) x (range (- l n) l))))

(time
  (dotimes [i 100]
    (inc-last 50 x)))

(time
  (dotimes [i 100]
    (doall (inc-last-2 10 x))))

(time
  (dotimes [i 100]
    (inc-last-3 50 x)))

;=> "Elapsed time: 49.7965 msecs"
;=> "Elapsed time: 1751.964501 msecs"
;=> "Elapsed time: 67.651 msecs"

【讨论】:

  • 重要的是要注意瞬态不是意味着要在命令式循环中就位;您仍然应该在功能上使用它们(例如,通过在inc-last-3 中将assoc 替换为assoc!,使用向量的瞬态版本作为reduce 的累加器并在结果上调用persistent!)。这是因为瞬态操作可能偶尔会返回新对象;这发生在今天的映射中(当一个瞬态数组映射被转换为一个瞬态哈希映射时),虽然它目前还没有发生在向量上,但 Clojure 对未来没有做出任何承诺。
  • 另外,inc-last-3——相当于noisesmith的inc-last-n——不返回惰性序列;它返回一个向量。事实上,它是一个很棒的功能解决方案;重写为使用瞬态它可能在性能方面是最佳的。
  • 我不知道瞬态操作可以返回一个新对象。我将更新问题,将inc-last 中的命令式循环替换为reduce
  • @omiel 这根本不是真的。 Clojure 的向量在内部实现为树,对它们的所有操作都涉及将单个路径从根复制到叶,并且是 O(log n),具有非常好的常量。所以事实上inc-last-3 在时间 O(x log n) 中运行,所有类似的方法也是如此。使用瞬态可以适当地提高常数因子,但不会改变 big-O 复杂度。
  • @juan.facorro 不用担心。我提出的一个额外建议是使用Criterium 进行基准测试——time 不适合此目的,即使使用了dotimes,因为JIT 编译器。一个简单的基准测试可能看起来像(require '[criterium.core :as c]) (c/bench (inc-last-3 50 x))。当然,这并不会真正改变这种情况下的结论,这只是一般的好政策。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-07-22
  • 1970-01-01
  • 1970-01-01
  • 2010-11-14
  • 1970-01-01
  • 1970-01-01
  • 2014-04-30
相关资源
最近更新 更多