Clojure中堆的算法(能不能高效实现?)

Posted

技术标签:

【中文标题】Clojure中堆的算法(能不能高效实现?)【英文标题】:Heap's algorithm in Clojure (can it be implemented efficiently?) 【发布时间】:2016-02-01 15:25:55 【问题描述】:

堆的算法枚举数组的排列。 Wikipedia's article on the algorithm 说 Robert Sedgewick 得出结论,该算法是“当时通过计算机生成排列的最有效算法”,因此尝试实现自然会很有趣。

该算法是关于在可变数组中进行连续交换,所以我正在考虑在 Clojure 中实现它,它的序列是不可变的。我将以下内容放在一起,完全避免了可变性:

(defn swap [a i j]
  (assoc a j (a i) i (a j)))

(defn generate-permutations [v n]
  (if (zero? n)
    ();(println (apply str a));Comment out to time just the code, not the print
    (loop [i 0 a v]
      (if (<= i n)
        (do
          (generate-permutations a (dec n))
          (recur (inc i) (swap a (if (even? n) i 0) n)))))))

(if (not= (count *command-line-args*) 1)
  (do (println "Exactly one argument is required") (System/exit 1))
  (let [word (-> *command-line-args* first vec)]
    (time (generate-permutations word (dec (count word))))))

对于 11 个字符的输入字符串,算法在 7.3 秒内(在我的计算机上)运行(平均超过 10 次运行)。

使用字符数组的等效 Java 程序运行时间为 0.24 秒。

所以我想让 Clojure 代码更快。我使用了带有类型提示的 Java 数组。这是我尝试过的:

(defn generate-permutations [^chars a n]
  (if (zero? n)
    ();(println (apply str a))
    (doseq [i (range 0 (inc n))]
      (generate-permutations a (dec n))
      (let [j (if (even? n) i 0) oldn (aget a n) oldj (aget a j)]
        (aset-char a n oldj) (aset-char a j oldn)))))

(if (not= (count *command-line-args*) 1)
  (do
    (println "Exactly one argument is required")
    (System/exit 1))
  (let [word (-> *command-line-args* first vec char-array)]
    (time (generate-permutations word (dec (count word))))))

嗯,它。现在,11 个字符的数组平均需要 9.1 秒(即使有类型提示)。

我知道可变数组不是 Clojure 的方式,但是有什么方法可以接近 Java 的这种算法的性能吗?

【问题讨论】:

您在比较中是否考虑了 JVM 预热/JIT?对我来说,通过criterium/bench 运行您的初始代码可以为 11 个字符的字符串提供大约 80-82 µs 的执行时间。阵列版本将其缩短至 59-60 µs。 这当然是很好的建议,谢谢你,虽然这里的预热比我预期的要多,因为使用 Java 的 System.currentTimeMillis 和 Clojure 的 time 宏进行简单时钟时间测量之间的区别仍然比我预期的要广泛得多。关于criterium 的好消息,我会研究一下。 当你应该这样做的时候使用CamelBack,我的眼睛会燃烧。 天哪,我很抱歉。已编辑。我希望你的眼睛能康复。是的,谢谢你的注意。我自己也经常对语言约定感到迂腐。 这是一个非常有趣的话题。我尝试了您的代码,并尝试对其进行调整,但没有太大改进。当我尝试优化它时,我实际上是在尝试优化算法,因此将算法分为“面向功能实现”和“面向命令实现”是合理的。 【参考方案1】:

Clojure 完全不是为了避免可变状态。就是 Clojure 对何时应该使用它有非常强烈的意见。

在这种情况下,我强烈建议找到一种方法来使用 transients 重写您的算法,因为它们专门设计用于通过避免重新分配内存并允许集合可变来节省时间只要对集合的引用永远不会离开创建它的函数,就可以在本地进行。我最近设法通过使用它们将内存密集型操作的时间缩短了近 10 倍。

这篇文章很好地解释了瞬变!

http://hypirion.com/musings/understanding-clojure-transients

此外,您可能希望考虑以允许您使用 recur 递归调用 generatePermutations 而不是使用整个名称的方式重写您的循环结构。您可能会获得性能提升,并且会减少堆栈的负担。

我希望这会有所帮助。

【讨论】:

以上是关于Clojure中堆的算法(能不能高效实现?)的主要内容,如果未能解决你的问题,请参考以下文章

java中堆和栈的区别

二叉堆的高效实现

数据结构与算法之美-堆的应用

Clojure 内容类型?

java中堆和栈的区别

计算机程序的思维逻辑 (47) - 堆和PriorityQueue的应用