在列表中查找最接近另一个点的点

时间:2016-05-05 21:06:32

标签: clojure clojurescript

简介

假设您要确定列表中哪个点最接近另一个给定点。函数应该返回点本身和距离。

例如,这些数据:

(def pts [[2 4] [1 9] [9 4] [2 8]])
(def p [7 6])

首先,需要一些辅助函数:

(def abs js/Math.abs)
(def pow js/Math.pow)
(def sqrt js/Math.sqrt)
(def pow2 #(pow % 2))

(defn distance [p1 p2]
  (sqrt (+ (pow2 (abs (- (p1 0) (p2 0))))
           (pow2 (abs (- (p1 1) (p2 1)))))))

两项提案

我的第一个方法如下:

(defn find-closest [p pts]
  (->> (map #(vector (distance p %) %) pts)
       (reduce (fn [m v]
                 (if (< (v 0) (m 0))
                   v
                   m)))))

(find-closest p pts)
=> [2.8284271247461903 [9 4]] ;; this is a correct result

通过尝试使该功能更具性能,我提出了第二个版本:

(defn find-closest2 [p pts]
  (let [init (first pts)]
    (reduce (fn [m v]
              (let [d (distance p v)]
                (if (< d (m 0))
                  [d v]
                  m)))
            [(distance p init) init]
            (rest pts))))

事实上,后来的功能变得更快(在chrome-browser 49中测试):

=> (time (dotimes [_ 100000] (find-closest p pts)))
"Elapsed time: 445.720000 msecs"
=> (time (dotimes [_ 100000] (find-closest2 p pts)))
"Elapsed time: 248.900000 msecs"

除了一个注释:有没有人提示,为什么在Clojure中相同的功能要慢一些:?

user> (time (dotimes [_ 100000] (find-closest p pts)))
"Elapsed time: 6886.850965 msecs"                                                                                                                              
user> (time (dotimes [_ 100000] (find-closest2 p pts)))
"Elapsed time: 6574.486679 msecs"

这会慢10倍以上,我觉得很难相信。

问题

无论如何,由于我需要ClojureScript项目的功能,这是我的问题:你如何解决这个问题? find-closest看起来对我很好,但速度较快的版本find-closest2看起来有点令人困惑。有没有更好的方法呢?

2 个答案:

答案 0 :(得分:1)

与您根据微观基准做出决策的所有情况一样,值得使用基准库(如criterium)来确保您看到统计上显着的结果。

在这种情况下,区别在于计算立即丢弃的中间懒惰序列map正在生成所有潜在答案的序列,并为每个答案分配内存。由于您对使用中间结果不感兴趣,因此浪费了此时间并且仅降低版本的速度更快。

直到最近,Clojure程序有时必须在使用map reduce filter等进行简单和可组合之间进行选择,并且通过不产生中间结果来快速进行选择。 这是通过trannsducers 修复的,所以现在您可以使用地图版本而不会引入中间结果,并且您可以以非常通用且适应性强的方式执行此操作。

user> (import '[java.lang.Math])
nil
user> (def pow2 #(Math/pow % 2))
      (defn distance [p1 p2]
       (Math/sqrt (+ (pow2 (Math/abs (- (p1 0) (p2 0))))
                  (pow2 (Math/abs (- (p1 1) (p2 1)))))))
#'user/pow2
#'user/distance
user> (defn closer-point
        ([] [Long/MAX_VALUE [Long/MAX_VALUE Long/MAX_VALUE]])
        ([p1] p1)
        ([[distance1 point1 :as p1]
          [distance2 point2 :as p2]]
         (if (< distance1 distance2)
           p1
           p2)))
#'user/closer-point
user> (transduce (map #(vector (distance p %) %))
                 closer-point
                 pts)
[2.8284271247461903 [9 4]]

答案 1 :(得分:1)

min-key函数专为此问题而设计。这是JVM版本。请注意,我们只是最小化平方距离,而不用计算Math/sqrt的实际距离:

(ns clj.core
  (:use tupelo.core)
  (:require [clojure.core       :as clj]
            [schema.core        :as s]
            [tupelo.types       :as tt]
            [tupelo.schema      :as ts]
            [criterium.core     :as crit]
  ))

; Prismatic Schema type definitions
(s/set-fn-validation! true)   ; #todo add to Schema docs

(def pts [[2 4] [1 9] [9 4] [2 8]])
(def p [7 6])

(defn square [x] (* x x))

(defn dist2 [p1 p2]
  (+ (square (- (p1 0) (p2 0)))
     (square (- (p1 1) (p2 1)))))

(doseq [curr-p pts]
  (println "curr-p: " curr-p " -> " (dist2 p curr-p)))

(newline)
(spyx (apply min-key #(dist2 p %) pts))

(newline)
(crit/quick-bench (apply min-key #(dist2 p %) pts))

(defn -main [] )

我不会过分担心代码的过早优化,只需先让它变得简单易懂。使用内置函数几乎总是一个良好的开端(就像在你真的不需要平方根时最小化数量的平方的旧技巧)。请注意,由于(abs ...)会自动执行此操作,因此我也摆脱了(square ...)调用。

以下是试运行的结果:

curr-p:  [2 4]  ->  29
curr-p:  [1 9]  ->  45
curr-p:  [9 4]  ->  8
curr-p:  [2 8]  ->  29

(apply min-key (fn* [p1__8701#] (dist2 p p1__8701#)) pts) => [9 4]

WARNING: Final GC required 7.5524163302816705 % of runtime
Evaluation count : 1132842 in 6 samples of 188807 calls.
             Execution time mean : 527.711887 ns
    Execution time std-deviation : 3.437558 ns
   Execution time lower quantile : 524.840276 ns ( 2.5%)
   Execution time upper quantile : 531.911280 ns (97.5%)
                   Overhead used : 1.534138 ns