алгоритм быстрой сортировки для нескольких упорядоченных массивов - PullRequest
4 голосов
/ 09 июля 2020

У меня 4 массива заказаны. Я хотел бы иметь возможность объединить их вместе в единую отсортированную структуру данных и лениво брать из нее.

Есть ли эффективный способ сделать это?

[1 3 4 6 9 10 15]
[2 3 6 7 8 9 10]
[1 3 6 7 8 9 10]
[1 2 3 4 8 9 10]

=> [1 1 1 2 2 3 3 3 3 4]

Ответы [ 5 ]

3 голосов
/ 09 июля 2020

есть также хороший способ сделать это, просто подсчитав частоту элементов в отсортированной карте, а затем развернув ее с помощью repeat:

(def data [[1 3 4 6 9 10 15]
           [2 3 6 7 8 9 10]
           [1 3 6 7 8 9 10]
           [1 2 3 4 8 9 10]])

(->> data
     (apply concat)
     (reduce #(update %1 %2 (fnil inc 0)) (sorted-map))
     (mapcat (fn [[k v]] (repeat v k))))

;;=> (1 1 1 2 2 3 3 3 3 4 4 6 6 6 7 7 8 8 8 9 9 9 9 10 10 10 10 15)

, также есть менее хитрый подход:

(defn min-first-idx [data]
  (when-let [items (->> data
                        (keep-indexed (fn [i x] (when (seq x) [x i])))
                        seq)]    
    (second (apply min-key ffirst items))))

(defn min-and-more [data-v]
  (when-let [i (min-first-idx data-v)]
    [(first (data-v i)) (update data-v i rest)]))

user> (min-and-more [[1 2 3] [0 1 4] [4 5]])
;; [0 [[1 2 3] (1 4) [4 5]]]

, чтобы вы использовали его для итеративного извлечения наименьшего элемента и остатка из коллекции:

(->> [nil (vec data)]      
     (iterate (comp min-and-more second))
     rest
     (take-while (comp seq second))
     (map first))

;; (1 1 1 2 2 3 3 3 3 4 4 6 6 6 7 7 8 8 8 9 9 9 9 10 10 10 10 15)
3 голосов
/ 09 июля 2020

Clojure поставляется с библиотекой функций, создающих или оперирующих ленивыми последовательностями, например map, iterate и take-while. Я считаю, что алгоритм слияния может быть выражен путем их объединения, примерно так.

(defn insert-into-sorted [dst x]
  (let [x0 (first x)
        a (take-while #(< (first %) x0) dst)
        b (drop (count a) dst)]
    (vec (concat a [x] b))))

(defn next-arrays [arrs]
  (let [[f & r] arrs
        restf (rest f)]
    (if (empty? restf)
      r
      (insert-into-sorted r restf))))

(defn merge-sorted-arrays [arrs]
  (->> arrs
       (filter seq)
       (sort-by first)
       (iterate next-arrays)
       (take-while seq)
       (map ffirst)))

И мы можем назвать это так:

(merge-sorted-arrays [[1 3 4 6 9 10 15]
                      [2 3 6 7 8 9 10]
                      [1 3 6 7 8 9 10]
                      [1 2 3 4 8 9 10]])
;; => (1 1 1 2 2 3 3 3 3 4 4 6 6 6 7 7 8 8 8 9 9 9 9 10 10 10 10 15)

Это правда, что вы могли бы что-то сделать например (sort (apply concat ...)), но это может оказаться неэффективным, если у вас много данных.

Обновление : предыдущая версия этого кода содержала вызов count, который ограничивал его применимость слияние последовательностей конечной длины. Изменив его на использование empty?, такого ограничения нет, и теперь мы можем использовать его для объединения последовательностей бесконечной длины:

(take 12 (merge-sorted-arrays [(iterate (partial + 1.1) 1) (iterate (partial + 1.11) 1)]))
;; => (1 1 2.1 2.1100000000000003 3.2 3.2200000000000006 4.300000000000001 4.330000000000001 5.4 5.440000000000001 6.5 6.550000000000002)
2 голосов
/ 09 июля 2020
• 1000 быстро как остальные:
(let [L 20000
        N 10]
    (mapv (fn [f]
            (let [arrs (vec (for [i (range N)]
                              (vec (range L))))]
              (time (doall (f arrs)))))
          [merge-sorted       ;; Rulle's
           merge-sorted-2     ;; Peter's
           merge-sorted-3     ;; Leetwinski's
]))


"Elapsed time: 721.649222 msecs"  ;; Rulle's
"Elapsed time: 373.058068 msecs"  ;; Peter's
"Elapsed time: 754.717533 msecs"  ;; Leetwinski's
1 голос
/ 18 июля 2020

Вы можете просто записать это явно для версии с ленивой производительностью.

(defn merges 
  ([x] x)

  ([x y]
    (cond 
      (empty? x) y
      (empty? y) x
      (< (first x) (first y)) 
        (cons (first x) (lazy-seq (merges y (rest x))))       
     :else 
      (cons (first y) (lazy-seq (merges x (rest y))))))

   ([x y & more]
     (apply merges 
       (for [[a b] (partition-all 2 (list* x y more))]
         (merges a b)))))


(apply merges [[1 3 4 6 9 10 15]
               [2 3 6 7 8 9 10]
               [1 3 6 7 8 9 10]
               [1 2 3 4 8 9 10]])

Изменить: эта версия объединяет попарно двоичное дерево для количества последовательностей в журнале глубины, а не предыдущее линейное сокращение.

1 голос
/ 10 июля 2020

И решение Рулля, и второе решение Литвински используют iterate в весьма анаморфной c манере (особенно во втором). Давайте определим unfold с помощью iterate (обычно делается наоборот) и напишем явно анаморфное решение c:

(defn unfold [f s]
  (->> s
       (list nil)
       (iterate (comp f second))
       rest
       (take-while some?)
       (map first)))

(defn merge-sorted [s]
  (->> s
       (filter seq)
       (unfold
         (fn [s]
           (if (seq s)
             (loop [[[mf & mn :as m] & s] s, r ()]
               (if-let [[[xf :as x] & s] s]
                 (let [[m x] (if (< xf mf) [x m] [m x])]
                   (recur (cons m s) (cons x r)))
                 (list mf (if mn (cons mn r) r)))))))))

ОБНОВЛЕНИЕ

Вот версия merge-sorted, которая использует reduce вместо loop и recur:

(defn merge-sorted [s]
  (->> s
       (filter seq)
       (unfold
         (fn [s]
           (if (seq s)
             (let [[[mf & mn] r]
                   (reduce
                     (fn [[m r] x]
                       (if (< (first x) (first m))
                         [x (cons m r)]
                         [m (cons x r)]))
                     [(first s) ()]
                     (rest s))]
               (list mf (if mn (cons mn r) r))))))))

UPDATE '

Впечатленный эффективностью решения А. Уэбба после редактирования и считая эту проблему интересной и относительно важной, я просмотрел статьи в Википедии о объединении и k-way merge алгоритмов и этой статьи . Я обнаружил, что есть много возможностей для анализа / экспериментов / улучшений, и решил (пере) реализовать и протестировать несколько алгоритмов. Вот они, упакованные в карту, которым предшествуют некоторые вспомогательные функции, а за ними следуют некоторые функции, полезные для тестирования:

(require ['clojure.core.reducers :as 'reducers])

(defn mapmap [f m]
  (reduce #(update %1 %2 f) m (keys m)))

(defn unfold [f s]
  (->> s
       (list nil)
       (iterate (comp f second))
       rest
       (take-while some?)
       (map first)))

(defn foldh [f s]
  ((fn rec [v]
     (f (if (> (count v) 2)
          (let [h (quot (count v) 2)]
            (map rec [(subvec v 0 h) (subvec v h)]))
          v)))
   (vec s)))

(defn fold2 [f s]
  (loop [s s]
    (if (nnext s)
      (recur (map f (partition-all 2 s)))
      (f s))))

(def merge-sorted
  (merge
    ;direct lazy algorithms
    (mapmap
      (fn [[prepare choose insert]]
        (fn [s]
          (->> s
               (filter seq)
               prepare
               (unfold
                 (fn [s]
                   (if (seq s)
                     (let [[[xf & xn] s] (choose s)]
                       [xf (if xn (insert s xn) s)])))))))
      {:min
       [identity
        (fn [s]
          (reduce
            (fn [[x s] y]
              (if (< (first x) (first y))
                [x (cons y s)]
                [y (cons x s)]))
            [(first s) ()]
            (rest s)))
        conj]
       :sort
       [(partial sort-by first)
        (juxt first rest)
        (fn [s [xf :as x]]
          (let [[a b] (loop [a () b (seq s)]
                        (if-let [[bf & bn] b]
                          (if (< (first bf) xf)
                            (recur (cons bf a) bn)
                            [a b])
                          [a b]))]
            (into (cons x b) a)))]
       :lsort
       [(partial sort-by first)
        (juxt first rest)
        (fn [s [xf :as x]]
          ((fn rec [s]
             (lazy-seq
               (if-let [[sf] (seq s)]
                 (if (< (first sf) xf)
                   (cons sf (rec (rest s)))
                   (cons x s))
                 (list x))))
           s))]
       :heap
       [(fn [s]
          (let [h (java.util.PriorityQueue.
                    (count s)
                    #(< (first %1) (first %2)))]
            (run! #(.add h %) s)
            h))
        (fn [h] [(.poll h) h])
        (fn [h x] (.add h x) h)]})
    ;folding lazy algorithms
    (mapmap
      (letfn [(merge2 [s]
                (lazy-seq
                  (if-let [[x & s] (seq (filter seq s))]
                    (if-let [[y] s]
                      ((fn rec [x y]
                         (lazy-seq
                           (let [[[xf & xn] y]
                                 (if (< (first x) (first y))
                                   [x y]
                                   [y x])]
                             (cons xf (if xn (rec xn y) y)))))
                       x y)
                      x))))]
        (fn [fold] (partial fold merge2)))
      {:foldl #(reduce (comp %1 list) %2)
       :foldh foldh
       :fold2 fold2})
    ;folding eager algorithms
    (mapmap
      (letfn [(merge2 [s]
                (if-let [[x & s] (seq (filter seq s))]
                  (if-let [[y] s]
                    (loop [x x y y acc ()]
                      (let [[[xf & xn] y]
                            (if (< (first x) (first y))
                              [x y]
                              [y x])
                            acc (conj acc xf)]
                        (if xn
                          (recur xn y acc)
                          (into y acc))))
                    x)
                  ()))]
        (fn [fold] (partial fold merge2)))
      {:efoldp #(reducers/fold 2 (comp %1 list) (comp %1 list) (vec %2))
       :efoldh foldh
       :efold2 fold2})))

(defn gen-inp [m n]
  (->> 0
       (repeat m)
       (map
         (comp
           doall
           (partial take n)
           rest
           (partial iterate #(+ % (rand-int 100)))))
       doall))

(defn test-merge-sorted [m n & algs]
   (->> (or algs (sort (keys merge-sorted)))
        (map (juxt name merge-sorted))
        (run!
          (let [inp (gen-inp m n)]
            (fn [[id alg]]
              (println id)
              ;(java.lang.System/gc)
              (try
                (time (doall (alg inp)))
                (catch java.lang.StackOverflowError _
                  (prn "Stack overflow"))))))))

Прямые ленивые алгоритмы следуют общей схеме, параметризованной тем, как выполняются следующие действия:

  • предварительная обработка входа
  • вычисление одной выбранной последовательности и остальных
  • вставка хвоста выбранной последовательности в остальные

:min похоже на мое первое решение, которое вычисляет минимум на каждой итерации.

:sort похоже на решение Rulle, которое сначала сортирует последовательности и выполняет сортировку-вставку на каждой итерации.

:lsort похоже на :sort, но с ленивой вставкой. Это может вызвать переполнение стека из-за вложенных ленивых последовательностей.

:heap - это простая, но неоптимальная реализация слияния кучи с использованием Java s PriorityQueue s.

Складывание lazy Алгоритмы следуют общей схеме, параметризованной тем, как <=2 -арное слияние расширяется до произвольных арностей.

:foldl похоже на решение А. Уэбба перед редактированием, которое выполняет свертывание влево с использованием reduce. Это может вызвать переполнение стека из-за вложенных ленивых последовательностей.

:foldh - это реализация слияния «разделяй и властвуй», которое складывается путем разделения пополам.

:fold2 похоже на Решение А. Уэбба после редактирования, которое складывается путем разбиения на пары.

Алгоритмы стремительного сворачивания следуют схеме, аналогичной схеме ленивых, но с использованием активного <=2 -арного слияния. *:efoldp - это параллельная реализация слияния «разделяй и властвуй» с использованием clojure.core.reducers/fold, которое выполняет различные <=2 -арные слияния одновременно и, возможно, параллельно, «разветвляясь» каждый раз, когда разделяется пополам.

:efoldh и :efold2 похожи на :foldh и :fold2, но стремятся.

Вкратце о производительности я бы сказал, что для быстрого ленивого слияния один из :foldh, Следует выбрать :fold2 или, возможно, :heap. Что касается активного слияния, то в зависимости от способности оборудования к параллелизму и формы ввода :efoldp может быть быстрее остальных. Для получения дополнительной информации см. Связанные статьи.

...