お問い合わせ・ご相談についてはこちら

[sort解説] マージインサーションソートを非常にわかりやすく解説する

ComputerScience
この記事は約6分で読めます。

この記事では、
自分の理解の範囲内で、
マージインサーションソートをソート初心者に説明することを目標にする。

別名などは特に記載せず、
ロジックを可能な限りわかりやすく説明する記事にしたい。

初めて勉強する人は、
この記事をみておけばだいたい実装イメージが湧くと思う。

マージインサーションソートとは

ひとことでいうと、

元の数列をmain_chainとsub_chainに分け、
適切にmain_chainにsub_chainの要素をマージしていくソート方法

である。

最悪比較回数を最小にするためのソート方法なのだが、
main_chainとsub_chainってなんぞやだと思うので早速次で説明する。

1.main_chainとsub_chainを作る

例えば9個の以下の数列

[500, 350, 400, 250, 0, 50, 100, 200, 150]があるとする。

その時、

  1. 隣同士とかで適当にペア作る
  2. ペア内で大小関係を作る
  3. ペアの大きい方をもとにペアごとソートする。(再帰的に。)
  4. 下の画像のように、
    上側(大きい方の羅列a[50, 200, 400, 500])をmain_chainとしてaとして表し、
    下側(小さい方の羅列b[0, 100, 250, 350])をsub_chainとしてbとして表す。
    a0とb0, a1とb1などはペアとしてお互いに対応している。

2: sub_chainの要素bをmain_chainにマージする

いま、
main_chain a[50, 150, 400, 500]
sub_chain b[0, 100, 250, 350]である。

ここで、
sub_chainの要素をmain_chainに入れていくことを考えると、
sub_chainの要素b1(100)は、対応するmain_chainの要素a1(150)以下が確定のため、
a1の左側へ必ず入ることがわかる。

よって、a1(150)より左側への挿入を考えればいい。

例えば、b3=350をaに入れようとすると、
a0~a2の、[50, 150, 400]に対して、
4つの箇所いずれかにb3を挿入することを考える。

このときb0~bnがあるが、
どの順でaに挿入していけば最悪比較回数を最小にできるだろうか?

ちなみに。
これは少し言い換えると、
b0を挿入するのにかかった比較回数+
b1を挿入するのにかかった比較回数+…+
bnを挿入するのにかかった比較回数 を最小にする

つまりbの要素の比較回数の合計を最小にする挿入順に最適化せよ!ってことだ。

少し考えてみてほしい。

これは2つ押さえれば簡単だ。

1つ目
「比較回数とは、二分木の深さのことである」ということ

こんな感じ。

①と②では、木の深さが2なので最悪比較回数は2回。
③からは、木の深さが3に突入してしまうため、最悪比較回数は3回になってしまう。

③よりもあと配列の要素が4こ多かった場合、
更に次の深さにノードが置かれるので、深さは4、最悪比較回数も4になる。

となると、

b0を挿入するのにかかった比較回数+
b1を挿入するのにかかった比較回数+…+
bnを挿入するのにかかった比較回数

を最小にするには、

これらのb要素のうち、
できるだけを木の深さ1で比較する。

できるだけを木の深さ2で比較する。

できるだけを木の深さ3で比較する。

。。。

とやっていけば、
比較合計数は最小にできる。

これは「欲張りプラン」と名付ける



2つ目
木の深さは「逆順」で固定できる


b(n) -> b(n-1) ->b(n-2)->…->b(1)->b(0)の順に「逆順」としてmain_chainに挿入を考える。

ここでさっき言った、
b(n)を挿入するとき、
a(n) >= b(n)なので、b(n)はa(n)よりも必ず左側に入ること
(a(0)~a(n-1)を比較対象にすればいいこと)
を思い出しておく。

(i)b(n)の挿入を考えるとき、
比較すべきはa(0)~a(n-1)のn個のノードで比較し挿入位置を決める

(ii)b(n-1)の挿入を考えるとき、
比較すべきはa(0)~a(n-2)のn-1個のノードと、(i)で挿入したb(n)1個、
合計n個のノードで比較し挿入位置を決める。

(iii)(i)(ii)に引き続いて同じ(たまにラッキーなこともあるがそれはさておく)

このように、
a側は逆順でindexがデクリメントしていくので1ずつ減り、
b側は前回挿入したbを挿入するので1ずつ増える。

結果として、
逆順にすることで、比較すべき対象ノードをn個に固定することができる

3:前項の「欲張りプラン」を実行する

前項ででた抑えてほしいポイント2つを使えば欲張りプランは実行できる

もう一度載せるが、

b0を挿入するのにかかった比較回数+
b1を挿入するのにかかった比較回数+…+
bnを挿入するのにかかった比較回数

を最小にするには、

これらのb要素のうち、
できるだけを木の深さ1で比較する。

できるだけを木の深さ2で比較する。

できるだけを木の深さ3で比較する。

。。。

とやっていけば、
比較合計数は最小にできる。

1から順に出来るだけのbを、
ノード数を固定しながらいれていけばいい

これを頭に入れればあとはスムーズに理解できる。

bをmain_chainに入れていく順序は以下の順だ。

初期、
main_chain a[50, 150, 400, 500]
sub_chain b[0, 100, 250, 350]である。

  1. b(0)=0は確定でa(0)=50より左側なので無条件に挿入。
  2. b(2)->b(1)の順に挿入を考えることで、比較ノードを3に固定できる。
    (深さは2の完全2分岐でできるだけbをいれる)
  3. b(4)->b(3)の順に挿入を考えることで、比較ノードを7に固定できる。
    (深さは3の完全2分岐でできるだけbをいれる)
  4. b(10)->b(9)->… ->b(6) ->b(5)の順で。。。(深さは4の完全2分岐)

とやっていくと、

b(0)->b(2)->b(1)->b(4)->b(3)->b(10)->b(9)->…

この順なら欲張りプランを満たせる。

どうして、2や4や10から逆順していけばいいと思いつくのか。
それは、
欲張りプランの
できるだけを木の深さ1で比較する。

できるだけを木の深さ2で比較する。

できるだけを木の深さ3で比較する。
の部分を満たそうとすると自然に出てくる。

例えば上のステップの3、比較ノードを7に固定したいとき、
ステップ2の完了段階ではa(2)より左側にはb(2), b(1), b(0)がすでに挿入されている。

[b(0), a(0), b(1), b(2), a(1), a(2), a(3), a(4), a(5), …, a(n) ]

ステップ2でindex = x(x+1個のノード)から逆順を始めたとき、
ステップ2の完了時(ステップ3の初期時)、
とあるa(x)の左側にはすでにbが(x + 1)個挿入されています。

a(x)~a(0)の個数とb(x)~b(0)の個数は同じなので

ステップが完了するとa(x)の左側には2 * (x + 1)個ノードがあることになる。

今回だとa(2)含め左側には、上の通り、6個すでにノードがある。

ノードを7に固定したい場合、
1つしかノードを追加できないので、
b(4)の挿入を考え、a(4)より左側、a(3)のみを新たに追加する。

4:この順序は数列として表せるか

b(0)->b(2)->b(1)->b(4)->b(3)->b(10)->b(9)->…

この数列、

0, 2, 4, 10, 20, …それぞれの項で前項の直後までデクリメントしているが、

いったいどんな数列として表せるだろうか。

数列として一般化できたらプログラムに落とし込めるのだが。

そこで、一旦この数列前項に1を足してみる。(おまじない)

1, 3, 5, 11, 21, …

これは、

J(n) = 2 * J(n – 2) + J(n – 1) …★

をみたしているではないか!!
(ちょっとズルすぎる気もするが。)

とにかくこの数列をindex化させるために、
各項で-1を行い、(これがさっきのおまじないの正体か?)

0, 2, 4, 10, 20, …

各項から逆順に前項の直後までデクリメントすることで

b(0)->b(2)->b(1)->b(4)->b(3)->b(10)->b(9)->…

が得られる。

この順にsub_chain要素をmain_chainにマージすることで、
マージインサーションソート(最悪比較回数を最小にするソート)は達成される。

(ちなみに★の数列はJacobsthal numbersという名前で、
CS分野でたまに応用されるらしい)

コメント

タイトルとURLをコピーしました