C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++でソート済み2つのリストの中央値を求める方法【二分探索による効率的な実装】

2つのソート済みリストが与えられたとき、それらを統合した結果の中央値を求める問題を考えます。例えば、配列が [1,5,8] と [2,3,6,9] の場合、統合すると [1,2,3,5,6,8,9] となり、中央値は 5 になります。

この問題は、二分探索を利用することで非常に効率的に解くことができます。単純に2つの配列をマージしてから中央値を計算する方法では O((m+n) log(m+n)) の計算量が必要ですが、分割位置(パーティション)を二分探索で調整する手法を使えば、O(log(min(m, n))) まで削減できます。

アルゴリズムの手順

  • solve() 関数を定義します。引数として配列 nums1 と nums2 を受け取ります。
  • nums1 のサイズが nums2 より大きい場合は、solve(nums2, nums1) を呼び出して順序を入れ替えます(常に小さい方の配列を基準に探索するため)。
  • x := nums1 のサイズ、y := nums2 のサイズ
  • low := 0、high := x
  • totalLength := x + y
  • low ≤ high である間、以下を繰り返します。
    • partitionX := low + (high − low) / 2
    • partitionY := (totalLength + 1) / 2 − partitionX
    • maxLeftX := (partitionX が 0 の場合は −∞、それ以外は nums1[partitionX − 1])
    • minRightX := (partitionX が x の場合は +∞、それ以外は nums1[partitionX])
    • maxLeftY := (partitionY が 0 の場合は −∞、それ以外は nums2[partitionY − 1])
    • minRightY := (partitionY が y の場合は +∞、それ以外は nums2[partitionY])
    • maxLeftX ≤ minRightY かつ maxLeftY ≤ minRightX を満たす場合(正しい分割が見つかった状態):
      • totalLength が偶数なら、(max(maxLeftX, maxLeftY) + min(minRightX, minRightY)) / 2 を返す
      • 奇数なら、max(maxLeftX, maxLeftY) を返す
    • そうでなく maxLeftX > minRightY の場合は、high := partitionX − 1 として左側へ探索範囲を狭める
    • それ以外の場合は、low := partitionX + 1 として右側へ探索範囲を狭める
  • ループを抜けた場合は 0 を返します(不正な入力に対するフォールバック)。

C++での実装例

以下のコードで実際の動作を確認できます。

#include <bits/stdc++.h>
using namespace std;

class Solution {
public:
    double solve(vector<int>& nums1, vector<int>& nums2) {
        if (nums1.size() > nums2.size())
            return solve(nums2, nums1);
        int x = nums1.size();
        int y = nums2.size();
        int low = 0;
        int high = x;
        int totalLength = x + y;
        while (low <= high) {
            int partitionX = low + (high - low) / 2;
            int partitionY = (totalLength + 1) / 2 - partitionX;

            int maxLeftX  = (partitionX == 0 ? INT_MIN : nums1[partitionX - 1]);
            int minRightX = (partitionX == x ? INT_MAX : nums1[partitionX]);

            int maxLeftY  = (partitionY == 0 ? INT_MIN : nums2[partitionY - 1]);
            int minRightY = (partitionY == y ? INT_MAX : nums2[partitionY]);

            if (maxLeftX <= minRightY && maxLeftY <= minRightX) {
                if (totalLength % 2 == 0) {
                    return ((double)max(maxLeftX, maxLeftY)
                          + (double)min(minRightX, minRightY)) / 2;
                }
                else {
                    return max(maxLeftX, maxLeftY);
                }
            }
            else if (maxLeftX > minRightY)
                high = partitionX - 1;
            else
                low = partitionX + 1;
        }
        return 0;
    }
};

int main() {
    Solution ob;
    vector<int> v1 = {1, 5, 8}, v2 = {2, 3, 6, 9};
    cout << ob.solve(v1, v2);
}

入力

[1,5,8], [2,3,6,9]

出力

5

仕組みのポイント

このアルゴリズムの核心は、「小さい方の配列に対してのみ二分探索を行う」点にあります。partitionX を決めると、全体の半分の要素数から自動的に partitionY が決まります。そして「左側の最大値が右側の最小値以下」という条件が両方の配列で成り立てば、その分割が正しい中央値の境界であることが保証されます。

境界条件(partition が 0 または配列末尾)では、それぞれ −∞(INT_MIN)や +∞(INT_MAX)を代入することで、比較処理を例外なく統一的に扱えるのが巧妙な点です。

計算量

  • 時間計算量: O(log(min(m, n))) — 小さい方の配列に対してのみ二分探索を行うため
  • 空間計算量: O(1) — 追加の配列を作成せず、定数個の変数のみを使用

LeetCode の「Median of Two Sorted Arrays」などでも頻出のテクニックなので、ぜひマスターしておきましょう。

  1. C++でグラフ内の2つのノード間の経路を探索するプログラム

    本記事では、C++を使って、与えられたグラフ上であるノード(頂点)から別のノードへ到達可能かどうかを判定するプログラムを紹介します。グラフの探索には、キューを用いた幅優先探索(BFS)を活用します。 アルゴリズムの流れ 始点 s から終点 d への到達可能性を判定する関数 isReach() は、以下の手順で動作します。 すべての頂点を「未訪問」として初期化します。 現在のノード(始点)を訪問済みとしてマークし、キューに追加します。このキューは、各頂点の隣接頂点を順に処理するために使用されます。 キューから頂点を取り出し(デキュー)、その隣接頂点をすべて取得します。 隣接頂点がまだ訪問されて

  2. 二分探索を用いた2つのソート済み配列の中央値の求め方(C++実装)

    2つのソート済み配列をマージせずに中央値を求めるには、二分探索を応用した分割統治法が効率的です。時間計算量は O(log(min(m,n))) で実現できます。ここでは、同じサイズの2つの配列に対する基本的なアプローチを C++ で実装し、アルゴリズムの流れとコード例を解説します。 アルゴリズムの概要 各部分配列の開始インデックスと終了インデックスを引数に median() 関数を呼び出す。 部分配列の長さを e1 - s1 + 1 で計算する。 長さが 1 または 2 の場合(基本ケース)、直接中央値を計算して返す。 両配列の中央値 m1, m2 を比較する。