M.Hiroi's Home Page

お気楽 OCaml プログラミング入門

ソート (2)


Copyright (C) 2008-2020 Makoto Hiroi
All rights reserved.

はじめに

ソートのお話です。今まで例題としてクイックソートと挿入ソートを取り上げました。データ数を N とすると、挿入ソートの実行時間は N2 に比例します。挿入ソートは遅いソートですが、クイックソートは高速なソートで、実行時間は N * log2 N に比例します。ところがクイックソートにも弱点があり、枢軸の選び方によっては実行時間が N2 に比例する遅いソートになってしまいます。リストの場合、枢軸を自由に選ぶことが難しいので、クイックソートはリスト向きのアルゴリズムとはいえません。

そこで、今回はリストに適したソートアルゴリズムである「マージソート (merge sort)」を取り上げます。データ数を N とすると、マージソートの実行時間は N * log2 N に比例します。マージソートはクイックソートと同様に高速なアルゴリズムですが、実際にプログラムを作って比較してみるとクイックソートの方が高速になります。ただし、クイックソートと違って、データによって性能が劣化することはありません。どのようなデータに対しても力を発揮してくれます。

●リストのマージ

まず最初にマージから説明します。「マージ (併合)」とは、複数のソート済みのリストを一つのソート済みのリストにまとめる操作です。次の図を見てください。

     ┌─ [1; 3; 5]  : リスト a 
[] ←┤
     └─ [2; 4; 6]  : リスト b 

   小さい方をセットする

      ┌─ [3; 5]    : リスト a 
[1] ←┘
           [2; 4; 6] : リスト b 

   1 をセットする

              [3; 5] : リスト a 
[1; 2] ←┐
         └─ [4; 6] : リスト b 

   2 をセットする

データがなくなるまで繰り返す 

    図 1 : リストのマージ

2 つのリスト a と b があります。これらのリストはソート済みとしましょう。これらのリストをソート済みのリストにまとめることを考えます。a と b はソート済みなので先頭のデータがいちばん小さな値です。したがって、上図のように先頭のデータを比較し、小さい方のデータを取り出して順番に並べていけば、ソート済みのリストにまとめることができます。途中でどちらかのリストが空になったら、残ったリストのデータをそのまま追加します。

それでは、実際にプログラムを作ってみましょう。次のリストを見てください。

リスト 9 : リストのマージ

let rec merge xs ys =
  match (xs, ys) with
    ([], _) -> ys
  | (_, []) -> xs
  | (x1:: xs1, y1::ys1) ->
    if x1 < y1 then x1::merge xs1 ys else y1::merge xs ys1

関数 merge の引数 xs と ys がマージするリストです。最初の節はリスト xs が空リストになった場合で、リスト ys をそのまま返します。2 番目の節はリスト ys が空リストになった場合で、リスト xs をそのまま返します。この 2 つの節が再帰呼び出しの停止条件になります。

リスト xs と ys にデータがあれば、先頭要素 x1 と y1 を比較します。x1 が小さい場合は x1 を、そうでなければ y1 を merge が返すリストに追加します。merge を再帰呼び出しするときは、追加する先頭要素をリストから取り除くことに注意してください。これでリストをマージすることができます。

簡単な実行例を示しましょう。

val merge : 'a list -> 'a list -> 'a list = <fun>
# merge [1; 3; 5; 7] [2; 4; 6; 8];;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8]
# merge [10; 20; 30] [1; 2; 3; 4];;
- : int list = [1; 2; 3; 4; 10; 20; 30]

●マージソートの実装

マージソートは、このマージを使ってデータをソートします。次の図を見てください。

 9 5 3 7 6 4 2 8  最初の状態

|5 9|3 7|4 6|2 8| 長さ2の列に併合

|3 5 7 9|2 4 6 8| 長さ4の列に併合 

 2 3 4 5 6 7 8 9  ソート終了

        図 2 : マージソート

マージをソートに応用する場合、最初は各要素をソート済みのリストとして考えます。この状態で隣のリストとマージを行い、長さ 2 のリストを作ります。次に、このリストに対して再度マージを行い、長さ 4 のリストを作ります。このように順番にマージしていくと、最後にはひとつのリストにマージされソートが完了します。

実際にプログラムを作る場合、リストの長さを 1, 2, 4, 8, ... と増やしていくよりも、再帰的に考えた方が簡単です。まず、ソートするリストを 2 つに分割して、前半部分をソートします。次に、後半部分をソートして、その結果をマージすればいいわけです。

再帰呼び出しするたびにリストは 2 つに分割されるので、最後にリストの要素はひとつとなります。これはソート済みのリストなので、ここで再帰呼び出しを終了してマージ処理を行えばいいわけです。プログラムは次のようになります。

リスト 10 : マージソート

(* 先頭から n 個の要素を削除する *)
let rec drop xs n =
  match (xs, n) with
    ((_, 0) | ([], _)) -> xs
  | (_ :: ys, _) -> drop ys (n - 1)

(* マージソート *)
let rec merge_sort n xs =
  match (n, xs) with
    (_, []) -> []
  | (1, x::xs) -> [x]
  | (2, x1::x2::xs) ->
    if x1 < x2 then [x1; x2] else [x2; x1]
  | (_, _) ->
    let m = n / 2 in
    merge (merge_sort m xs) (merge_sort (n - m) (drop xs m))

関数 merge_sort の引数 xs がソートするリスト、引数 n がリストの長さを表します。merge_sort はリストを分割する処理で、新しいリストを作らないことに注意してください。次の図を見てください。

 引数 x
  |
  |←── 長さn ──→|
(1 2 3 4 5 6 7 8)   
  |←n/2→| |←n/2→|
  |          |
 引数 x      引数 y     再帰呼び出し 

        図 3 : リストの分割

merge_sort はソートするリストの範囲を開始位置と長さで表しています。上図のリストを二分割する場合、前半部分は x と n / 2 で表し、後半部分を y と n / 2 で表します。y はリスト x の先頭から n / 2 個の要素を取り除けば求めることができます。この処理を関数 drop で行います。drop は list の先頭から n 個の要素を取り除きます。リストに対して n 回だけ tl を適用すると考えてもかまいません。簡単な例を示しましょう。

val drop : 'a list -> int -> 'a list = <fun>
# drop [1; 2; 3; 4] 0;;
- : int list = [1; 2; 3; 4]
# drop [1; 2; 3; 4] 2;;
- : int list = [3; 4]
# drop [1; 2; 3; 4] 4;;
- : int list = []

あとは再帰呼び出しでリストを分割していき、リストの長さが 1 になったならば新しいリストを返します。リストの長さが 2 の場合は簡単なので、2 つの要素を比較してソート済みのリストを作成して返します。そして、merge_sort の返り値を merge でマージすればいいわけです。

それでは実行してみましょう。

val merge_sort : int -> 'a list -> 'a list = <fun>
# merge_sort 9 [5; 9; 1; 8; 2; 7; 3; 6; 4];;
- : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9]

正常に動作していますね。

●問題

次の関数を定義してください。

  1. リスト xs の中から述語 pred が真を返す最初の要素の位置を求める関数 position_if pred xs
  2. リスト xs から述語 pred が真を返す要素の個数を求める関数 count_if pred xs
  3. リスト xs, ys の直積集合を求める関数 product_set xs ys
  4. リスト xs のべき集合を求める関数 power_set xs
  5. 自然数 n 以下の素数をすべて求める関数 sieve n












●解答

リスト : 解答例

let rec position_if pred = function
    [] -> None
  | x::xs -> if pred x then Some x else position_if pred xs

let count_if pred xs =
  let rec count xs a =
    match xs with
      [] -> a
    | y::ys -> count ys (if pred y then a + 1 else a)
  in count xs 0

(* 別解 *)
let count_if1 pred xs =
  List.fold_left (fun a x -> if pred x then a + 1 else a) 0 xs

let flatmap f xs = flatten (List.map f xs)

let product_set xs ys =
  flatmap (fun x -> List.map (fun y -> (x, y)) ys) xs

let rec power_set = function
    [] -> [[]]
  | x::xs -> (power_set xs) @ (List.map (fun ys -> x::ys) (power_set xs))

let rec iota n m =
  if n > m then []
  else n :: iota (n + 1) m

let sieve n =
  let rec sieve_sub xs a =
    match xs with
      [] -> a
    | x::xs as ys ->
       if x * x > n then List.rev_append a ys
       else sieve_sub (List.filter (fun y -> y mod x <> 0) xs) (x::a)
  in sieve_sub (iota 2 n) []
# position_if (fun x -> x mod 2 = 0) [1;2;3;4;5;6;7;8];;
- : int option = Some 2
# position_if (fun x -> x mod 7 = 0) [1;2;3;4;5;6;7;8];;
- : int option = Some 7
# position_if (fun x -> x mod 9 = 0) [1;2;3;4;5;6;7;8];;
- : int option = None

# count_if (fun x -> x mod 2 = 0) [1;2;3;4;5;6;7;8];;
- : int = 4
# count_if (fun x -> x mod 4 = 0) [1;2;3;4;5;6;7;8];;
- : int = 2
# count_if (fun x -> x mod 9 = 0) [1;2;3;4;5;6;7;8];;
- : int = 0

# product_set [1;2;3] [4;5];;
- : (int * int) list = [(1, 4); (1, 5); (2, 4); (2, 5); (3, 4); (3, 5)]
# product_set [1;2;3] [4;5;6];;
- : (int * int) list =
[(1, 4); (1, 5); (1, 6); (2, 4); (2, 5); (2, 6); (3, 4); (3, 5); (3, 6)]

# power_set [1;2;3;4];;
- : int list list =
[[]; [4]; [3]; [3; 4]; [2]; [2; 4]; [2; 3]; [2; 3; 4]; [1]; [1; 4]; [1; 3];
 [1; 3; 4]; [1; 2]; [1; 2; 4]; [1; 2; 3]; [1; 2; 3; 4]]

# sieve 100;;
- : int list =
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59; 61; 67; 71;
 73; 79; 83; 89; 97]
# sieve 500;;
- : int list =
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29; 31; 37; 41; 43; 47; 53; 59; 61; 67; 71;
 73; 79; 83; 89; 97; 101; 103; 107; 109; 113; 127; 131; 137; 139; 149; 151;
 157; 163; 167; 173; 179; 181; 191; 193; 197; 199; 211; 223; 227; 229; 233;
 239; 241; 251; 257; 263; 269; 271; 277; 281; 283; 293; 307; 311; 313; 317;
 331; 337; 347; 349; 353; 359; 367; 373; 379; 383; 389; 397; 401; 409; 419;
 421; 431; 433; 439; 443; 449; 457; 461; 463; 467; 479; 487; 491; 499]

初版 2008 年 6 月 22 日
改訂 2020 年 6 月 28 日