読者です 読者をやめる 読者になる 読者になる

Arantium Maestum

プログラミング、囲碁、読書の話題

Clojureでクイックソート

書いてみる。

まずはletの中にベタ書き。

(defn quick-sort [col]
  (if (>= 1 (count col))
    col
    (let [i      (rand-int (count col))
          pivot  (nth col i)
          heads  (take i col)
          tails  (drop (inc i) col)
          col2   (concat heads tails)
          lesser (filter #(< % pivot) col2)
          bigger (filter #(>= % pivot) col2)]
      (concat (quick-sort lesser) 
              [pivot] 
              (quick-sort bigger)))))

末尾再帰じゃないが、基本的に再帰の階層の数はlogNなので大丈夫なはず。Cとかでもquick sortの実装は大抵再帰を使うし。

なんとなく、「並べるものが何もなければcolをそのまま返す」と書いた方が少し意図がはっきりするように感じるので変更。

(defn quick-sort [col]
  (if (empty? (rest col))
    col
    (let [i      (rand-int (count col))
          pivot  (nth col i)
          heads  (take i col)
          tails  (drop (inc i) col)
          col2   (concat heads tails)
          lesser (filter #(< % pivot) col2)
          bigger (filter #(>= % pivot) col2)]
      (concat (quick-sort lesser) 
              [pivot] 
              (quick-sort bigger)))))

まあこれは好みの問題だろう。速度的には、私のPCだと後者の方が10%ほど早かったが正直そこは現段階ではpremature optimizationの部類だろう。

各local変数を別々に定義しているのは、それぞれの関係性がはっきりしない。destructuringを使って関連する変数は同時に束縛したい。

まずはpivot, heads, tails:

(defn quick-sort [col]
  (if (empty? (rest col))
    col
    (let [[heads [pivot & tails]]
           (split-at (rand-int (count col)) col)
           
          col2   (concat heads tails)
          lesser (filter #(< % pivot) col2)
          bigger (filter #(>= % pivot) col2)]
      (concat (quick-sort lesser) 
              [pivot] 
              (quick-sort bigger)))))

(split-at (rand-int (count col)) col)でcolを二つに分割。先頭のリストは空リストになり得るが、後ろの方は必ず一つは要素が含まれる。

後者のリストもさらにpivotとtailsにdestructuringする。pivotは必ず存在するが、tailsは空リストの可能性がある。

ちょっと長くなるので二行に分けざるを得ないのが玉に瑕だが、一つのsequenceを三つに分割しているのが明確に表現されていると思う。

lesserとbiggerもいっぺんに定義できる:

(defn quick-sort [col]
  (if (empty? (rest col))
    col
    (let [[heads [pivot & tails]]
           (split-at (rand-int (count col)) col)
           
          {lesser true, bigger false}
          (group-by #(< % pivot) (concat heads tails))]
      (concat (quick-sort lesser) 
              [pivot] 
              (quick-sort bigger)))))

group-by #(< % pivot) col)で{true pivot未満のリスト, false pivot以上のリスト}というhash-mapに変換。

destructuringでlesserとbiggerに変数束縛できる。

その一つの利点としては、(concat heads tails)を一回しか使わないので別に定義する必要がない。正直な話、ちゃんと意味のある変数名をつけようと思ったら「col-without-pivot」のような微妙なものになるので、できれば命名せずに済ましたかった。

そしてやはり、一つのリストをpivot未満・以上にわけて二つの変数に束縛している、というのがコードにちゃんと現れている(気がする)。

ちなみにパフォーマンス的にはdestructuringを使った方がいい。一番初めのコードに比べて大体25%ほど向上しているようだ。

追記:

しかし見直すと、最初のdestructuringが少し仕事をしすぎている気がする。

(defn quick-sort [col]
  (if (empty? (rest col))
    col
    (let [pivot-index
          (rand-int (count col))

          [heads [pivot & tails]]
          (split-at pivot-index col)
           
          {lesser true, bigger false}
          (group-by #(< % pivot) (concat heads tails))]
      (concat (quick-sort lesser) 
              [pivot] 
              (quick-sort bigger)))))

こんなもんでどうじゃろ。

追追記:

そうすると前言撤回してこう書いてもいい気がしてくる。

(defn quick-sort [col]
  (if (empty? (rest col))
    col
    (let [pivot-index
          (rand-int (count col))

          [heads [pivot & tails]]
          (split-at pivot-index col)
          
          col-ex-pivot
          (concat heads tails)
           
          {lesser true, bigger false}
          (group-by #(< % pivot) col-ex-pivot)]
      (concat (quick-sort lesser) 
              [pivot] 
              (quick-sort bigger)))))