Pythonのマージソートが速い“裏側”を完全解説

マージソート、名前からして強そうだけど「分割して…再帰して…最後にマージ?」で脳が停止しがちですよね。

しかも計算量の話が出てくると、更に置いていかれる感じ。

この記事は**“マージ(合体)こそ本体”**という視点で、初心者がつまずくポイントを先回りしながら、図解→実装→使いどころまで一気に整理します。





1. 学ぶ前に押さえる前提

1-1. ソートと安定性の超基本

1-1-1. 安定ソートが必要になる場面

まず「ソート」は、データを決まったルールで並べる事です。

例えば「小さい順」「名前のあいうえお順」みたいな並べ替えです。 (ウィキペディア)

ここで大事なのが 安定(あんてい)ソート
安定ソートとは、同じ値(同じキー)の物同士の“元の順番”を守って並べ替えるソートの事です。 (EverPlay(エバープレイ))

例えば、こんな名簿があるとします。

  • 田中(A組)
  • 佐藤(B組)
  • 鈴木(A組)
  • 高橋(B組)

ここで「組」で並べ替えると、A組→B組に並びます。
でも A組の中で「田中→鈴木」という元の順番がそのまま残るのが「安定」です。 (EverPlay(エバープレイ))

これが何に役立つかというと、2回ソートしたい時です。

例:
1回目:入学した日付順(古い→新しい)
2回目:クラス順(A→B→C)

このとき、2回目を安定ソートでやれば、**クラスごとに“入学日順が崩れない”**状態になります。

だから「データの意味を壊さずに並べ替えたい」場面で安定ソートは強いです。 (EverPlay(エバープレイ))

そしてマージソートは、作り方次第で 安定に出来るのが大きなポイントです。 (ウィキペディア)


2. マージソートの全体像

2-1. 分割→整列→マージの流れ

2-1-1. 例で追う:8要素を分けて戻す

マージソートは、ざっくり言うとこうです。

  1. 配列を半分に分ける
  2. さらに半分に分ける
  3. 1個ずつになったら「もう整列済み」と考える
  4. 2つずつくっつけて(マージして)整列済みの大きい配列にする
  5. それを繰り返して、最後に全体が整列する

この「分けて、後でくっつける」考え方を 分割統治と言います。 (Qiita)

例:[8, 4, 5, 2, 9, 1, 7, 3]

分ける(イメージ):

[8,4,5,2,9,1,7,3]
→ [8,4,5,2] + [9,1,7,3]
→ [8,4] + [5,2] + [9,1] + [7,3]
→ [8]+[4]+[5]+[2]+[9]+[1]+[7]+[3]

ここまで来たら、1個の配列は必ず整列しています(だって1個だから)。

次に「くっつける」:

  • [8][4] を比べて → [4,8]
  • [5][2][2,5]
  • [9][1][1,9]
  • [7][3][3,7]

さらに

  • [4,8][2,5][2,4,5,8]
  • [1,9][3,7][1,3,7,9]

最後に

  • [2,4,5,8][1,3,7,9] → 完成!

この時本当に大事なのは、「分ける」より **“くっつける(マージ)”**です。
マージが出来るようになると、マージソート全体が分かってきます。 (東京学芸大学)


3. 一番大事な「マージ処理」

3-1. 2つの整列済みリストを併合

3-1-1. 「<=」か「<」かで安定性が変わる

マージとは、既に並んでいる2つの列を、もう一回綺麗に1本にする作業です。 (東京学芸大学)

コツは「先頭同士を見て、小さい方を先に出す」を繰り返すだけ。

例:
左:[2, 5, 8]
右:[3, 5, 7]

  • 2と3 → 2を出す
  • 5と3 → 3を出す
  • 5と5 → どっちを先に出す?

ここがポイントです。
同じ(5と5)なら 左を先に出すと、元の順番を守りやすくなり、安定になります。 (IT用語辞書)

だから、実装ではよくこう書きます。

if L[i] <= R[j]:
    # 同じなら左を先に

もし < にしてしまうと、「同じだったら右が先に出る」事が起きて、安定じゃなくなる場合があります(同値の順番が入れ替わる)。 (IT用語辞書)

マージソートを理解する最短ルートは、ここを覚える事です。
「左右を比べて、小さい方を取る。等しいときは左を取る」
これが出来たら、後はそれを何回もやっているだけです。 (東京学芸大学)


4. 再帰で書くマージソート(Python)

4-1. 最小構成の実装

4-1-1. 補助配列でバグを減らすコツ

初心者がつまずきやすいのは **再帰(さいき:自分をもう一回呼ぶ)**です。
でも、形を決めてしまえばそんなに怖くないです。

安全な作り方はこう:

  • 「いま見ている範囲」を [l, r)(l以上、r未満)で表す
  • 要素が1個以下なら終わり(これが止まる条件)
  • 真ん中 m で分けて、左と右をそれぞれ整列
  • 最後にマージ

そして、マージの時に **補助配列(作業用の配列)**を使うと、混乱が減ります。
「一旦作業用に並べてから、元に戻す」感じです。 (Qiita)

易しめ実装(説明用):

def merge_sort(a):
    work = [0] * len(a)

    def sort(l, r):
        if r - l <= 1:   # 1個以下なら終わり
            return

        m = (l + r) // 2
        sort(l, m)       # 左を整列
        sort(m, r)       # 右を整列

        # ここからマージ(左右は整列済み)
        i, j, k = l, m, l
        while i < m and j < r:
            if a[i] <= a[j]:   # 同じなら左→安定
                work[k] = a[i]; i += 1
            else:
                work[k] = a[j]; j += 1
            k += 1

        while i < m:
            work[k] = a[i]; i += 1; k += 1
        while j < r:
            work[k] = a[j]; j += 1; k += 1

        a[l:r] = work[l:r]     # 元に戻す

    sort(0, len(a))
    return a

この形だと「どこを見ているか」がはっきりします。
初心者はまずこれをそのまま写して動かして、次に自分で少しずつ変えると理解が速いです。 (ウィキブックス)





5. 再帰が苦手ならボトムアップ

5-1. 反復(iterative)版の考え方

5-1-1. 1,2,4…と幅を倍にしてマージ

再帰がどうしても苦手なら、再帰を使わないマージソートがあります。
それが **ボトムアップ(下から積む)**の考え方です。 (EverPlay(エバープレイ))

イメージはこうです。

  • 最初は「長さ1の整列済みの塊」が沢山あると思う
  • 隣り同士をマージして「長さ2の整列済み」にする
  • 次は長さ2同士をマージして「長さ4」にする
  • 次は8…と倍々で広げる

だから「1,2,4,8…」と幅(width)が増えていきます。 (EverPlay(エバープレイ))

この方法は、再帰を使わないので頭がスッキリしやすいです。
更に、現実のデータは「ちょっと並んでる」事が多いので、そういう性質をうまく使う TimSortみたいな仕組みもあります(PythonのsortはTimSort)。 (Python documentation)


6. 計算量とメモリの腹落ち

6-1. なぜO(n log n)なの?

6-1-1. 深さlog n × 各層O(n)のイメージ

「O(n log n)って何?」を、数学をなるべく使わずに言うとこうです。

だから
(段の数)log n ×(1段の手間)n = n log n
という感じです。 (Qiita)

もう1つ大事なのがメモリ。
マージソートは、作業用の配列を使うことが多いので、追加で 大体n個分の場所が必要になります(O(n))。 (ウィキペディア)

まとめると:

  • 速さ:だいたいO(n log n)で安定
  • ただし:メモリは多めに使いがち

この「いい所・弱い所」をセットで覚えると、実際に使う場面が見えてきます。 (ウィキペディア)


7. いつ使う?他ソートとの比較

7-1. クイック/ヒープと比べる

7-1-1. 最悪ケース保証と安定性の価値

「マージソートって、いつ使うの?」は良くある疑問です。
答えはシンプルで、次の2つを大事にしたい時です。

① 最悪でもそこそこ速いのがいい

マージソートは、データの並び方にあまり左右されにくく、安定してO(n log n)の感じになりやすいです。 (ウィキペディア)
(クイックソートは平均的に速いけど、実装や状況で一気に遅くなる話が出ます。だから「最悪を踏みたくない」ならマージソートが安心、という考え方になります。)

② 同じ値の順番(安定性)を守りたい

名簿や表データみたいに「同じ点数の人が多い」とか、「2回ソートしたい」みたいな場面では、安定性が効きます。 (EverPlay(エバープレイ))

逆に、メモリが厳しいなら、別の方法(ヒープソートなど)を選ぶ事もあります。
大事なのは「どれが一番」ではなく、目的に合うかです。 (ウィキブックス)


8. 練習問題と次の一歩

8-1. 手を動かして定着させる

8-1-1. 転倒数・stable_sortでレベルアップ

マージソートは、理解したつもりでも、手を動かすと穴が見つかります。

お勧めの練習はこの順番です。

ステップ1:まず普通にソート出来るようにする

自作のマージソートを動かして、色んな配列で試します。

  • 既に並んでいる
  • 逆順
  • 同じ数字が沢山
  • 1個、2個、3個…(小さい入力)

こういうテストをすると、境界ミスに気づきます。

ステップ2:転倒数を数えてみる

転倒数は「本当はこっちが先なのに、逆になってるペアの数」です。
例:[3, 1, 2] だと
(3,1) と (3,2) が逆なので転倒数は2です。

これ、マージの途中で数えられます。
マージ中に「右の要素の方が小さいから先に取る」場面があると、左に残っている要素の数だけ「逆のペア」が増える、という考え方です。 (プログラミング原人の進化論)

転倒数はアルゴリズム問題でも良く出るので、ここまで出来ると一気に強くなります。 (ITエンジニア向け転職・就活・学習サービス〖paiza〗)

ステップ3:標準ソートも観察してみる(安定性)

Pythonの sortsortedTimSortで、安定で、複数キーのソートにも向いています。 (Python documentation)
「自作」→「標準は何してる?」まで行くと、勉強が実用に近付きます。

まとめ

マージソートは、**「分ける → それぞれを整える → くっつける(マージ)」**で並べ替える方法です。

まず配列をどんどん半分に分けて、1個になったらそれはもう並んでいると考えます。

後は、並んだ2つの列を、先頭同士を比べながら小さい順に合体していき、最後に全体が綺麗に並びます。 (ウィキペディア)

マージソートの良い所は、主に2つです。

  • 速さが安定しやすい:大体 O(n log n) の計算量で、極端に遅くなりにくいのが強みです。 (ウィキペディア)
  • 安定ソートにしやすい:多くの実装では、同じ値同士の元の順番を保ったまま並べ替えできます(=安定)。 (ウィキペディア)

一方で注意点もあります。配列でマージソートを作ると、マージする時に作業場所(別の配列)を使う事が多く、追加でO(n)位のメモリが必要になりがちです。 (ウィキペディア)

また、実務や学習では「安定性」を知っておくと便利です。例えばPythonの sorted()list.sort() は、**同じキーの要素の順番を保つ(安定)**ことが保証されています。

だから「部署で並べてから年齢で並べる」など、複数回の並べ替えもやりやすいです。 (Python documentation)

最後に一言。

マージソートを理解するコツは、分け方よりも **「マージ(合体)の仕組み」**をしっかり押さえる事です。

マージが分かれば、全体の流れも自然に理解出来るようになります。 (ウィキペディア)

 




コメント

この記事へのコメントはありません。

CAPTCHA


カレンダー
2025年12月
1234567
891011121314
15161718192021
22232425262728
293031  
月別投稿数
PAGE TOP

You cannot copy content of this page