Heap在Clojure中的算法(能否高效实现?)
Heap's algorithm in Clojure (can it be implemented efficiently?)
Heap 的算法枚举数组的排列。 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 的性能吗?
与其说 Clojure 完全是为了避免可变状态,还不如说是。就是 Clojure 对什么时候应该使用它有非常强烈的意见。
在这种情况下,我强烈建议找到一种使用 transients 重写算法的方法,因为它们专门设计用于通过避免重新分配内存和只要对集合的引用永远不会离开创建它的函数,就允许集合在本地可变。我最近通过使用它们设法将内存密集型操作的时间减少了近 10 倍。
这篇文章很好地解释了瞬态!
http://hypirion.com/musings/understanding-clojure-transients
此外,您可能希望以一种允许您使用 recur 递归调用 generatePermutations 而不是使用整个名称的方式重写循环结构。您可能会获得性能提升,并且会大大减少对堆栈的负担。
希望对您有所帮助。
Heap 的算法枚举数组的排列。 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 的性能吗?
与其说 Clojure 完全是为了避免可变状态,还不如说是。就是 Clojure 对什么时候应该使用它有非常强烈的意见。
在这种情况下,我强烈建议找到一种使用 transients 重写算法的方法,因为它们专门设计用于通过避免重新分配内存和只要对集合的引用永远不会离开创建它的函数,就允许集合在本地可变。我最近通过使用它们设法将内存密集型操作的时间减少了近 10 倍。
这篇文章很好地解释了瞬态!
http://hypirion.com/musings/understanding-clojure-transients
此外,您可能希望以一种允许您使用 recur 递归调用 generatePermutations 而不是使用整个名称的方式重写循环结构。您可能会获得性能提升,并且会大大减少对堆栈的负担。
希望对您有所帮助。