多重マージ(k-way マージ)の理論と実践 — アルゴリズム・外部ソート・実装最適化ガイド

はじめに — 多重マージとは何か

多重マージ(k-way マージ、または多重併合)は、複数(k個)の既にソートされた入力列を1つのソート済み出力列に統合する操作を指します。単純な2つの列のマージ(merge)の一般化であり、内部メモリソートや外部ソート、データベースのマージ演算、ストリーム処理、分散処理のシャッフル後統合など、幅広い場面で中心的な役割を果たします。

基本概念と目的

目的は、総要素数をN、入力列数をkとしたときに、正しい順序で出力を作りつつ、計算時間と追加メモリ(およびI/O回数)を最小化することです。典型的な性質は以下の通りです。

  • 出力の長さはNで、全要素は一度ずつ出力される。
  • 各入力列は既にソートされている前提。
  • 操作コストは比較回数(CPU)と、外部記憶を扱う場合は読み書き(I/O)に依存する。

代表的アルゴリズムと計算量

主な実装戦略は大きく分けて次の通りです。

  • 逐次ペアワイズ結合(Naive sequential merge) — 既存の出力と次の列を順に2-way マージしていく方式。実装は単純だが、最悪で O(kN) になり得る(例えば最初に大きな出力を何度もマージする場合)。
  • バランスされたペアワイズ結合(Balanced pairwise / tournament style) — 完全二分木の形でペアワイズにマージしていく方法。各レベルで総和はNなので、レベル数は O(log k)、結果として O(N log k) の時間となる。定数因子は実装による。
  • ヒープ(優先度付きキュー)を使う k-way マージ — 各入力列の先頭要素をサイズ k の最小ヒープに置き、最小要素を取り出すたびに対応する列から次の要素を挿入する。各出力に対してヒープ操作が O(log k) のため合計 O(N log k)、追加メモリ O(k)。実装の簡潔さと安定した性能が利点。
  • トーナメント木(Loser tree / winner tree) — ヒープの代替となるデータ構造で、1 出力あたりの比較回数やコピーを減らす目的で使う。初期構築に O(k) 比較、各要素の出力ごとに O(log k) 比較で済み、実務上はヒープより小さな定数因子で高速になることが多い。

まとめると、効率的な k-way マージは一般に O(N log k) 時間、O(k) 追加メモリが目安です。ただし、実際には定数因子(比較・データ移動回数)やメモリの局所性、I/O 性能が総合的な性能を決めます。

外部ソートと多重マージの関係

外部ソート(メモリに収まらない巨大データのソート)では、多重マージが中心的役割を担います。典型的な外部ソートの流れは以下:

  • 入力をメモリに収まるサイズ(M)ごとに読み込み、内部ソートしてラン(run)を作成する。
  • 生成された複数のランを k-way マージで統合し、最終出力を得る。k は同時に開けるファイル数や利用可能メモリに依存。

外部ソートにおける重要指標は「パス数(merge pass の回数)」と「I/O 総量」です。k を大きくするほどパス数は減るが、そのぶん各入力バッファ等のメモリが必要になります。一般に、必要なパス数はおおむね ceil(log_{k} R)(R は初期ラン数)で、各パスでデータ全体を読み書きするため、パス数を減らすことが最も I/O を削減するカギです。

ランの生成を延ばす技法:リプレースメントセレクション

リプレースメントセレクション(replacement selection)は、初期ランを単純に M 要素ずつソートして作るよりも長いランを生成する手法です。ヒープ(主に内部メモリ)を使い、取り出した要素より小さい要素は次のラン用として待避させることにより、平均ラン長は約 2M に達すると言われます(ランダムな入力時)。これにより初期ラン数が減り、後続のマージ回数を削減できます。

ポリフェーズマージと最適ファンイン

ポリフェーズマージ(polyphase merge)は、複数のテープ(またはファイル)を用いて、各パスでのダミーランを最小化する特別な分配方式です。テープ数(ファンイン)とランの分配をフィボナッチ系列的に調整することで、全体のパス数と無駄な I/O を減らすことができます。実装はやや複雑ですが、媒体がテープや同等の条件下では有利です。

実装上のポイントと最適化

  • バッファリング:外部マージでは入出力バッファサイズが性能を左右します。小さすぎるとシークやシステム I/O が増え、大きすぎるとメモリや同時ファイル数を圧迫します。
  • ファイルディスクリプタ数制限:OS の制限により同時に開ける入力ストリーム数に上限があるため、k を調整あるいは階層的にマージする必要があります。
  • 局所性とコピー回数:データのムーブを減らすためにポインタ(または参照)操作で済ませる、メモリプールを使うなどを検討します。トーナメント木は比較回数・コピー量を減らす利点があります。
  • 安定性:安定ソートが要求される場面では、マージは安定に実装するのが容易です(同一キー時の元の順序を保つ)。
  • 並列化:入出力帯域や CPU コアが豊富なら、ランの生成やマージ自体を並列化できます。例えば、複数のマージタスクに分散し、最終段階で k-way 統合を行う方法が考えられます。

実例:ライブラリとコマンドでの利用

実装例としては、Python 標準ライブラリの heapq.merge は複数のソート済イテレータを効率的に k-way マージします(遅延評価でメモリ効率良好)。C++ 標準ライブラリには std::merge がありこれは2-way マージ用ですが、複数列をマージする際はヒープやトーナメント木を自前で用いるのが一般的です。外部ソートのコマンドとしては GNU coreutils の sort が大規模ファイルを扱う実装を持っており、内部で外部マージを活用しています。

注意点・誤解されやすい点

  • 「k を増やせば常に速くなる」わけではありません。メモリやファイルハンドル、I/O 特性によって最適な k が存在します。
  • シンプルな逐次マージは実装は簡単だが、入力の偏り次第で極端に遅くなり得ます。性能保証が必要ならヒープやバランスされたマージを選ぶべきです。
  • 多重マージと「Git の octopus merge(複数ブランチを同時にマージする機能)」などの意味合いを混同しないこと。用途と期待する振る舞いが異なります。

テストとデバッグの実践

多重マージ実装を検証する際のチェック項目:

  • 出力が完全にソートされているか(昇順/降順の一貫性)。
  • 重複キー/等しい要素に対する安定性の確保。
  • メモリ使用量とファイルディスクリプタ数の上限テスト。
  • 最悪ケース(極端に長い/短いランの混在、偏った入力分布)での時間特性測定。
  • 外部ソートでは I/O カウント(読み書きバイト数、シーク数)を測る。

実装スニペット(概念的)

ここではヒープを使った k-way マージの擬似コードを示します(概念理解用):

1) 各入力イテレータ i の先頭要素をミニマムヒープに (value, i) の形で入れる。2) ヒープが空になるまで:最小を取り出して出力し、その要素を取り出した入力 i から次の要素があればヒープに挿入する。

まとめ

多重マージは一見単純だが、入力数 k や総要素数 N、利用可能なメモリ、I/O 特性によって最適戦略が変わる奥深い問題です。内部メモリでの k-way マージは一般にヒープまたはトーナメント木で O(N log k) の性能が得られ、外部ソートではランの生成方法(リプレースメントセレクション)やマージ戦略(fan-in、ポリフェーズなど)が I/O コストに直結します。実装にあたっては、バッファリングやファイルハンドル制限、安定性と並列化の方針を踏まえて選択してください。

参考文献