左から右、上から下にソートされた2D配列の数値を検索するにはどうすればよいですか?


90

私は最近このインタビューの質問を与えられました、そして私はそれに対する良い解決策が何であるかについて知りたいです。

配列のすべての数値が左から右、上から下へと昇順である2次元配列が与えられたとしましょう。

ターゲット番号が配列内にあるかどうかを検索して判別する最良の方法は何ですか?

さて、私の最初の傾向は、データがソートされているため、バイナリ検索を利用することです。O(log N)時間で数値が単一行にあるかどうかを判断できます。しかし、それは私を捨てる2つの方向です。

私がうまくいくかもしれないと思ったもう一つの解決策は、真ん中のどこかから始めることです。真ん中の値が私の目標よりも小さい場合、それは真ん中からマトリックスの左の正方形の部分にあると確信できます。次に、対角線上に移動して再度チェックし、ターゲットの数に磨きをかけるまで、ターゲットが入る可能性のある正方形のサイズを小さくします。

この問題を解決する上で何か良いアイデアはありますか?

配列の例:

左から右、上から下に並べ替えました。

1  2  4  5  6  
2  3  5  7  8  
4  6  8  9  10  
5  8  9  10 11  

簡単な質問:それはあなたが同じ値を持つ隣人を持つことができるということかもしれません:[[1 1][1 1]]
Matthieu M.10年

回答:


115

ここに簡単なアプローチがあります:

  1. 左下隅から始めます。
  2. ターゲットがその値よりも小さい場合は、目標を上回っている必要があるため、1つ上に移動します。
  3. それ以外の場合は、ターゲットがその列に存在できないことがわかっているため、右に移動します
  4. 後藤2。

以下のためNxMのアレイ、これはで実行されますO(N+M)。もっとうまくやるのは難しいと思います。:)


編集:たくさんの良い議論。上記の一般的なケースについて話していました。明らかに、NまたはM 小さい場合は、バイナリ検索アプローチを使用して、対数時間に近い何かでこれを行うことができます。

興味がある人のために、いくつかの詳細を以下に示します。

歴史

この単純なアルゴリズムは、サドルバック検索と呼ばれます。しばらく使用されており、の場合は最適N == Mです。いくつかの参照:

ただし、の場合N < M、直感は、バイナリ検索は次のことよりも優れた機能を備えているべきだと示唆していますO(N+M)。たとえば、N == 1、純粋なバイナリ検索は線形時間ではなく対数で実行されます。

最悪の場合の限界

Richard Birdは、2006年の論文でバイナリ検索がサドルバックアルゴリズムを改善できるというこの直観を調べました。

かなり珍しい会話技法を使用して、BirdはN <= M、この問題にはの下限があることを示していΩ(N * log(M/N))ます。この境界は、線形のパフォーマンスN == Mと対数のパフォーマンスを提供するので意味がありN == 1ます。

長方形配列のアルゴリズム

行ごとのバイナリ検索を使用する1つのアプローチは次のようになります。

  1. 長方形の配列から始めN < Mます。N行と列であるとしましょうM
  2. 中央の行でバイナリ検索を実行しvalueます。見つかったら完了です。
  3. そうでなければ、我々は数字の隣接ペアを見つけたsg、どこs < value < g
  4. 上と左の数字の長方形sはより小さいためvalue、削除できます。
  5. 下と右の長方形gはより大きいのでvalue、削除できます。
  6. 残りの2つの長方形のそれぞれについて、手順(2)に進みます。

最悪の場合の複雑さに関して、このアルゴリズムはlog(M)、考えられるソリューションの半分を排除するように機能し、2つの小さな問題に対して再帰的に2回呼び出されます。log(M)すべての行でその作業の小さいバージョンを繰り返す必要がありますが、行の数が列の数と比較して少ない場合、対数時間でこれらの列をすべて削除できることは価値があるようになります。

これはアルゴリズムにの複雑さを与え、T(N,M) = log(M) + 2 * T(M/2, N/2)BirdはそうO(N * log(M/N))です。

Craig Gidneyによって投稿された別のアプローチは、上記のアプローチと同様のアルゴリズムについて説明しています。これは、ステップサイズを使用して一度に行を調べますM/N。彼の分析によれば、これによりO(N * log(M/N))パフォーマンスも向上します。

性能比較

Big-O分析はすべてうまく機能していますが、これらのアプローチは実際にはどの程度うまく機能していますか?次の表は、「正方形」の配列を増やすための4つのアルゴリズムを示しています。

アルゴリズムのパフォーマンスと直角度

( "naive"アルゴリズムは単に配列のすべての要素を検索します。 "recursive"アルゴリズムは上で説明されています。 "hybrid"アルゴリズムはGidneyアルゴリズムの実装です。各アレイサイズについて、パフォーマンスは固定セット上で各アルゴリズムのタイミングを測定することによって測定されました1,000,000のランダムに生成された配列。)

いくつかの注目すべき点:

  • 予想どおり、「バイナリ検索」アルゴリズムは長方形配列で最高のパフォーマンスを提供し、サドルバックアルゴリズムは正方形配列で最高の動作をします。
  • サドルバックアルゴリズムは、1次元配列の「単純な」アルゴリズムよりもパフォーマンスが低下します。これは、おそらく各項目で複数の比較を行うためです。
  • 「バイナリ検索」アルゴリズムが正方配列に対して行うパフォーマンスへの影響は、バイナリ検索を繰り返し実行することによるオーバーヘッドが原因であると考えられます。

概要

二分探索の巧妙な使用はO(N * log(M/N)、長方形と正方形の両方の配列にパフォーマンスを提供できます。O(N + M)「サドル」アルゴリズムは、はるかに簡単であるが、性能劣化からアレイなど被るはますます矩形になります。


6
対角歩行にバイナリ検索を適用すると、O(logN)またはO(logM)のどちらか高い方になります。
Anurag

3
@Anurag-複雑さはうまくいっていないと思います。二分探索は、開始するのに適した場所を提供しますが、どちらか一方の次元を最後まで歩く必要があり、最悪の場合、一方の隅から開始して他方の隅で終了することもできます。
ジェフリーLホイットリッジ

1
N = 1かつM = 1000000の場合、iはO(N + M)よりも優れているため、別の解決策は、各行にバイナリ検索を適用してO(N * log(M))をもたらすことです。より小さい定数。
Luka Rahne、2011年

1
私はあなたの方法とバイナリ検索方法の両方を使用していくつかのテストを行い、結果をここに投稿しました。両方の方法で最悪の場合の条件を適切に生成できなかった場合を除いて、ジグザグ方法が最善のようです。
The111

1
参照をうまく使用してください!ただし、複雑さがM==N必要な場合、後者はゼロではありO(N)ませんO(N*log(N/N))。正しい「統一された」鋭い境界はO(N*(log(M/N)+1))、の場合N<=Mです。
hardmath 2014年

35

この問題は、かかるΘ(b lg(t))時間、b = min(w,h)およびにt=b/max(w,h)このブログ投稿で解決策について説明します。

下限

敵対者はΩ(b lg(t))、主対角に制限することで、アルゴリズムに強制的にクエリを実行させることができます。

主対角線を使用する敵

凡例:白いセルは小さいアイテム、灰色のセルは大きいアイテム、黄色のセルは小さいか等しいアイテム、オレンジのセルは大きいか等しいアイテムです。敵対者は、アルゴリズムが最後に照会する黄色またはオレンジ色のセルをソリューションに強制します。

bサイズの独立したソート済みリストがありtΩ(b lg(t))クエリを完全に排除する必要があることに注意してください。

アルゴリズム

  1. (一般性を失うことなくそれを仮定しますw >= h
  2. ターゲットアイテムをt有効な領域の右上隅の左側にあるセルと比較します
    • セルのアイテムが一致する場合、現在の位置を返します。
    • セルのアイテムがターゲットアイテムよりも小さい場合はt、バイナリ検索で行の残りのセルを削除します。これを行っている間に一致するアイテムが見つかった場合は、その位置で戻ります。
    • そうでない場合、セルのアイテムはターゲットアイテムより多くなり、t短い列が削除されます。
  3. 有効な領域が残っていない場合は、失敗を返します
  4. ステップ2に移動

アイテムを見つける:

アイテムを見つける

アイテムが存在しないと判断する:

アイテムが存在しないと判断

凡例:白いセルは小さなアイテム、灰色のセルは大きなアイテム、緑のセルは同等のアイテムです。

分析

b*t削除する短い列があります。b削除する長い行があります。長い行を削除するとO(lg(t))時間がかかります。t短いカラムを排除するとO(1)時間がかかります。

最悪の場合、時間をかけてすべての列とすべての行を削除する必要がありますO(lg(t)*b + b*t*1/t) = O(b lg(t))

lg1より大きい結果(つまりlg(x) = log_2(max(2,x)))へのクランプを想定していることに注意してください。これがw=h、の意味でt=1、の期待される範囲が得られる理由ですO(b lg(1)) = O(b) = O(w+h)

コード

public static Tuple<int, int> TryFindItemInSortedMatrix<T>(this IReadOnlyList<IReadOnlyList<T>> grid, T item, IComparer<T> comparer = null) {
    if (grid == null) throw new ArgumentNullException("grid");
    comparer = comparer ?? Comparer<T>.Default;

    // check size
    var width = grid.Count;
    if (width == 0) return null;
    var height = grid[0].Count;
    if (height < width) {
        var result = grid.LazyTranspose().TryFindItemInSortedMatrix(item, comparer);
        if (result == null) return null;
        return Tuple.Create(result.Item2, result.Item1);
    }

    // search
    var minCol = 0;
    var maxRow = height - 1;
    var t = height / width;
    while (minCol < width && maxRow >= 0) {
        // query the item in the minimum column, t above the maximum row
        var luckyRow = Math.Max(maxRow - t, 0);
        var cmpItemVsLucky = comparer.Compare(item, grid[minCol][luckyRow]);
        if (cmpItemVsLucky == 0) return Tuple.Create(minCol, luckyRow);

        // did we eliminate t rows from the bottom?
        if (cmpItemVsLucky < 0) {
            maxRow = luckyRow - 1;
            continue;
        }

        // we eliminated most of the current minimum column
        // spend lg(t) time eliminating rest of column
        var minRowInCol = luckyRow + 1;
        var maxRowInCol = maxRow;
        while (minRowInCol <= maxRowInCol) {
            var mid = minRowInCol + (maxRowInCol - minRowInCol + 1) / 2;
            var cmpItemVsMid = comparer.Compare(item, grid[minCol][mid]);
            if (cmpItemVsMid == 0) return Tuple.Create(minCol, mid);
            if (cmpItemVsMid > 0) {
                minRowInCol = mid + 1;
            } else {
                maxRowInCol = mid - 1;
                maxRow = mid - 1;
            }
        }

        minCol += 1;
    }

    return null;
}

1
興味深く、おそらく部分的に私の頭の上に。私は、この「敵対的な」スタイルの複雑さ分析に精通していません。敵は実際に検索時に配列を動的に変更しているのですか、それとも最悪の場合の検索で遭遇した不運に付けられた名前なのですか?
The111

2
@ The111不運は、誰かがこれまでに見たものに違反しない悪いパスを選択することと同等なので、これらの定義はどちらも同じように機能します。実際に、計算の複雑さに関してこの手法を具体的に説明するリンクを見つけるのに苦労しています...これはもっと有名なアイデアだと思いました。
Craig Gidney 2013

log(1)= 0なので、複雑さの推定値はではO(b*(lg(t)+1))なくとして指定する必要がありO(b*lg(t))ます。素敵な記事、特に。「最悪のケース」の限界を示す際に「敵の技術」に注意を喚起したことに対して。
hardmath 14年

@hardmath私は答えでそれを述べます。私はそれを少し明確にしました。
クレイグギドニー2014年

17

この問題には、あなたが提案したものと同様の分割統治戦略を使用しますが、詳細は少し異なります。

これは、行列の部分範囲の再帰的な検索になります。

各ステップで、範囲の中央にある要素を選択します。見つかった値が求めているものであれば、それで終わりです。

それ以外の場合、見つかった値が求めている値よりも小さい場合は、現在の位置の左上にある象限にないことがわかります。したがって、2つのサブ範囲を再帰的に検索します。現在の位置より下のすべて(排他的)と現在の位置またはその上にある右側のすべて(排他的)です。

それ以外の場合(検出された値は、求めている値よりも大きい)、現在の位置の右下の象限にないことがわかります。したがって、2つのサブ範囲を再帰的に検索します。現在の位置の左側にあるすべて(排他的)と、現在の位置より上にある現在の列または右側の列にあるすべて(排他的)です。

そして、バーダビン、あなたはそれを見つけました。

各再帰呼び出しは現在のサブ範囲のみを処理し、(たとえば)現在の位置より上のすべての行は処理しないことに注意してください。現在のサブレンジにあるものだけです。

ここにあなたのためのいくつかの疑似コードがあります:

bool numberSearch(int[][] arr, int value, int minX, int maxX, int minY, int maxY)

if (minX == maxX and minY == maxY and arr[minX,minY] != value)
    return false
if (arr[minX,minY] > value) return false;  // Early exits if the value can't be in 
if (arr[maxX,maxY] < value) return false;  // this subrange at all.
int nextX = (minX + maxX) / 2
int nextY = (minY + maxY) / 2
if (arr[nextX,nextY] == value)
{
    print nextX,nextY
    return true
}
else if (arr[nextX,nextY] < value)
{
    if (numberSearch(arr, value, minX, maxX, nextY + 1, maxY))
        return true
    return numberSearch(arr, value, nextX + 1, maxX, minY, nextY)
}
else
{
    if (numberSearch(arr, value, minX, nextX - 1, minY, maxY))
        return true
    reutrn numberSearch(arr, value, nextX, maxX, minY, nextY)
}

+1:これはO(log(N))戦略であり、したがって、取得しようとしているのと同じくらい良い順序です。
Rex Kerr、

3
@Rex Kerr-O(log(N))のように見えます。これは通常のバイナリ検索と同じですが、各レベルで2つの再帰呼び出しが発生する可能性があることに注意してください。これは、単純な対数よりもはるかに悪いことを意味します。すべての行またはすべての列を検索する必要がある可能性があるため、最悪のケースはO(M + N)よりも良いとは思いません。しかし、このアルゴリズムは多くの値で最悪のケースを打つことができると思います。そして、最良の部分は、並列化可能であることです。これは、ハードウェアが最近向かっているところだからです。
ジェフリーLホイットレッジ2010年

1
@JLW:O(log(N))ですが、実際にはO(log_(4/3)(N ^ 2))またはそのようなものです。以下のSvanteの回答を参照してください。あなたの答えは実際には同じです(あなたが思ったように再帰を意味する場合)。
Rex Kerr、

1
@Svante-サブ配列は重複しません。最初のオプションでは、共通のy要素はありません。2番目のオプションでは、共通のx要素はありません。
ジェフリーLホイットレッジ

1
これが対数かどうかはわかりません。近似回帰関係T(0)= 1、T(A)= T(A / 2)+ T(A / 4)+ 1を使用して複雑さを計算しました。ここで、Aは検索領域であり、T( A)= O(Fib(lg(A)))、これはおよそO(A ^ 0.7)であり、O(n + m)であるO(A ^ 0.5)よりも悪いです。多分私は愚かな間違いをしたかもしれませんが、アルゴリズムが無益な枝を下って行くのに多くの時間を浪費しているようです。
Craig Gidney

6

これまでの主な2つの答えは、間違いなくO(log N)「ジグザグ法」とO(N+M)二分探索法のようです。2つの方法をいくつかのさまざまな設定と比較して、いくつかのテストを行うと思いました。詳細は次のとおりです。

配列はすべてのテストでN x Nの正方形で、Nは125から8000まで変化します(私のJVMヒープが処理できる最大のもの)。配列のサイズごとに、配列内のランダムな場所を選択して、1つのを配置しました2。次に、3可能な限りどこでも(2の右と下に)配置し、残りの配列を1。以前のコメンターの中には、このタイプのセットアップが両方のアルゴリズムの最悪の場合の実行時間をもたらすと考えているようでした。アレイのサイズごとに、2(検索ターゲット)に対して100個の異なるランダムな場所を選び、テストを実行しました。各アルゴリズムの平均実行時間と最悪の場合の実行時間を記録しました。Javaで適切なms読み取りを取得するには速すぎたため、JavaのnanoTime()を信頼していないため、すべての時間に均一なバイアス係数を追加するために、各テストを1000回繰り返しました。結果は次のとおりです。

ここに画像の説明を入力してください

ZigZagは、すべてのテストで、平均と最悪の両方のケースの時間でバイナリをビートしましたが、それらはすべて互いにほぼ1桁以内です。

Javaコードは次のとおりです。

public class SearchSortedArray2D {

    static boolean findZigZag(int[][] a, int t) {
        int i = 0;
        int j = a.length - 1;
        while (i <= a.length - 1 && j >= 0) {
            if (a[i][j] == t) return true;
            else if (a[i][j] < t) i++;
            else j--;
        }
        return false;
    }

    static boolean findBinarySearch(int[][] a, int t) {
        return findBinarySearch(a, t, 0, 0, a.length - 1, a.length - 1);
    }

    static boolean findBinarySearch(int[][] a, int t,
            int r1, int c1, int r2, int c2) {
        if (r1 > r2 || c1 > c2) return false; 
        if (r1 == r2 && c1 == c2 && a[r1][c1] != t) return false;
        if (a[r1][c1] > t) return false;
        if (a[r2][c2] < t) return false;

        int rm = (r1 + r2) / 2;
        int cm = (c1 + c2) / 2;
        if (a[rm][cm] == t) return true;
        else if (a[rm][cm] > t) {
            boolean b1 = findBinarySearch(a, t, r1, c1, r2, cm - 1);
            boolean b2 = findBinarySearch(a, t, r1, cm, rm - 1, c2);
            return (b1 || b2);
        } else {
            boolean b1 = findBinarySearch(a, t, r1, cm + 1, rm, c2);
            boolean b2 = findBinarySearch(a, t, rm + 1, c1, r2, c2);
            return (b1 || b2);
        }
    }

    static void randomizeArray(int[][] a, int N) {
        int ri = (int) (Math.random() * N);
        int rj = (int) (Math.random() * N);
        a[ri][rj] = 2;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (i == ri && j == rj) continue;
                else if (i > ri || j > rj) a[i][j] = 3;
                else a[i][j] = 1;
            }
        }
    }

    public static void main(String[] args) {

        int N = 8000;
        int[][] a = new int[N][N];
        int randoms = 100;
        int repeats = 1000;

        long start, end, duration;
        long zigMin = Integer.MAX_VALUE, zigMax = Integer.MIN_VALUE;
        long binMin = Integer.MAX_VALUE, binMax = Integer.MIN_VALUE;
        long zigSum = 0, zigAvg;
        long binSum = 0, binAvg;

        for (int k = 0; k < randoms; k++) {
            randomizeArray(a, N);

            start = System.currentTimeMillis();
            for (int i = 0; i < repeats; i++) findZigZag(a, 2);
            end = System.currentTimeMillis();
            duration = end - start;
            zigSum += duration;
            zigMin = Math.min(zigMin, duration);
            zigMax = Math.max(zigMax, duration);

            start = System.currentTimeMillis();
            for (int i = 0; i < repeats; i++) findBinarySearch(a, 2);
            end = System.currentTimeMillis();
            duration = end - start;
            binSum += duration;
            binMin = Math.min(binMin, duration);
            binMax = Math.max(binMax, duration);
        }
        zigAvg = zigSum / randoms;
        binAvg = binSum / randoms;

        System.out.println(findZigZag(a, 2) ?
                "Found via zigzag method. " : "ERROR. ");
        //System.out.println("min search time: " + zigMin + "ms");
        System.out.println("max search time: " + zigMax + "ms");
        System.out.println("avg search time: " + zigAvg + "ms");

        System.out.println();

        System.out.println(findBinarySearch(a, 2) ?
                "Found via binary search method. " : "ERROR. ");
        //System.out.println("min search time: " + binMin + "ms");
        System.out.println("max search time: " + binMax + "ms");
        System.out.println("avg search time: " + binAvg + "ms");
    }
}

1
+1イェイ、データ。:) 2次元検索は、1次元の場合に近づくほど直感的に役立つようになるため、NxM配列でこれらの2つのアプローチがどのように機能するかを確認することも興味深いかもしれません。
ネイトコール2013

5

これは、問題の下限の短い証明です。

(要素の数ではなく、配列の次元に関して)線形時間よりも優れた方法はありません。以下の配列では、マークされた各要素は*5または6のいずれかになります(他の要素とは無関係)。したがって、ターゲット値が6(または5)の場合、アルゴリズムはそれらすべてを調べる必要があります。

1 2 3 4 *
2 3 4 * 7
3 4 * 7 8
4 * 7 8 9
* 7 8 9 10

もちろん、これはより大きな配列にも拡張されます。これは、この回答が最適であることを意味します。

更新:Jeffrey L Whitledgeによって指摘されたように、実行時間と入力データサイズの漸近的な下限(単一の変数として扱われる)としてのみ最適です。両方の配列次元で2変数関数として扱われる実行時間を改善できます。


あなたはその答えが最適であることを示していません。たとえば、5行目にターゲット値よりも高い値が含まれている、10倍で100万個の配列があるとします。その場合、提案されたアルゴリズムは、ターゲットに近づく前に999,995個の値をリニア検索します。私のような分岐アルゴリズムは、ターゲットに近づく前に18個の値のみを検索します。そして、それは他のすべてのケースで提案されたアルゴリズムよりも(漸近的に)悪くはありません。
ジェフリーLホイットリッジ

@ジェフリー:それは悲観的なケースの問題の下限です。適切な入力用に最適化できますが、線形よりも優れた入力が存在します。
ラファウDowgird

はい、線形よりもうまくできない入力が存在します。その場合、私のアルゴリズムはその線形検索を実行します。しかし、あなたが行うことができます他の入力がある方法を線形より良いが。したがって、提案されたソリューションは常に線形探索を行うため、最適ではありません。
ジェフリーLホイットリッジ

これは、アルゴリズムがBigOmega(n + m)ではなくBigOmega(min(n、m))の時間を取る必要があることを示しています。これが、1つの次元が大幅に小さい場合に、より良い結果が得られる理由です。たとえば、行が1つしかないことがわかっている場合は、問題を対数時間で解決できます。最適なアルゴリズムには時間がかかると思いますO(min(n + m、n lg m、m lg n))。
Craig Gidney

それに応じて回答を更新しました。
ラファウDowgird

4

私はここに答えがあると思います、それはあらゆる種類のソートされた行列に対して機能します

bool findNum(int arr[][ARR_MAX],int xmin, int xmax, int ymin,int ymax,int key)
{
    if (xmin > xmax || ymin > ymax || xmax < xmin || ymax < ymin) return false;
    if ((xmin == xmax) && (ymin == ymax) && (arr[xmin][ymin] != key)) return false;
    if (arr[xmin][ymin] > key || arr[xmax][ymax] < key) return false;
    if (arr[xmin][ymin] == key || arr[xmax][ymax] == key) return true;

    int xnew = (xmin + xmax)/2;
    int ynew = (ymin + ymax)/2;

    if (arr[xnew][ynew] == key) return true;
    if (arr[xnew][ynew] < key)
    {
        if (findNum(arr,xnew+1,xmax,ymin,ymax,key))
            return true;
        return (findNum(arr,xmin,xmax,ynew+1,ymax,key));
    } else {
        if (findNum(arr,xmin,xnew-1,ymin,ymax,key))
            return true;
        return (findNum(arr,xmin,xmax,ymin,ynew-1,key));
    }
}

1

興味深い質問です。このアイデアを検討してください-すべての数値がターゲットよりも大きい境界と、すべての数値がターゲットよりも小さい境界を作成します。2つの間に何かが残っている場合は、それがターゲットです。

あなたの例で3を探している場合は、4に到達するまで最初の行を読み取り、次に3より大きい最小の隣接番号(対角線を含む)を探します。

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11

今度は、3未満の数値についても同じようにします。

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11

さて、2つの境界の内側に何かありますか?はいの場合、それは3でなければなりません。いいえの場合、3はありません。実際には数が見つからないので、間接のように、そこにある必要があると推測します。これには、3をすべてカウントするという追加のボーナスがあります。

私はいくつかの例でこれを試してみましたが、うまくいくようです。


コメントなしの反対票?最悪の場合のパフォーマンスでは対角線のチェックが必要なので、これはO(N ^ 1/2)だと思います。少なくとも、この方法が機能しない反例を見せてください!
Grembo

+1:すばらしい解決策...創造的であり、すべての解決策を見つけることができるのは良いことです。
Tony Delroy、2011年

1

配列の対角線を通る二分探索が最良のオプションです。要素が対角線の要素以下かどうかを確認できます。


0

A.ターゲット番号がオンになっている可能性がある行でバイナリ検索を実行します。

B.グラフにする:未訪問の最小の隣接ノードを常に使用して数を探し、大きすぎる数が見つかった場合はバックトラックする


0

バイナリ検索が最善の方法です、imo。1/2 xから始めて、1/2 yで半分にカットします。IE 5x5の正方形は、x == 2 / y == 3のようなものです。ターゲット値の方向で、1つの値を切り下げ、1つの値をより良いゾーンに切り上げました。

わかりやすくするために、次の反復ではx == 1 / y == 2 OR x == 3 / y == 5のようになります。


0

まず、正方形を使用していると仮定しましょう。

1 2 3
2 3 4
3 4 5

1.正方形を検索する

私は対角線上で二分探索を使用します。目標は、厳密に目標数よりも低くない小さい数を見つけることです。

4たとえば、私が探しているとすると、最終的にはに移動5(2,2)ます。

その後、私があればと確信しています4テーブル内にある、それは位置のいずれかである(x,2)(2,x)xの中で[0,2]。まあ、それは2つのバイナリ検索だけです。

複雑さは難しくありません:(O(log(N))長さの範囲での3つのバイナリ検索N

2.長方形の検索、素朴なアプローチ

もちろん、それが(長方形で)異なる場合は少し複雑にNなりMます。この退化したケースを考えてください:

1  2  3  4  5  6  7  8
2  3  4  5  6  7  8  9
10 11 12 13 14 15 16 17

そして、私が探しているとしましょう9...対角アプローチはまだ良いですが、対角の定義が変わります。ここに私の対角線があり[1, (5 or 6), 17]ます。私が拾ったとしましょう[1,5,17]、そして9それが表にある場合、それはサブパートにあることを知っています:

            5  6  7  8
            6  7  8  9
10 11 12 13 14 15 16

これにより、2つの長方形が得られます。

5 6 7 8    10 11 12 13 14 15 16
6 7 8 9

だから私たちは再帰することができます!おそらく要素の少ないものから始めます(ただし、この場合は私たちを殺します)。

いずれかの次元が未満の場合3、対角法を適用できず、バイナリ検索を使用する必要があることを指摘しておきます。ここでそれは意味します:

  • でバイナリ検索を適用しますが10 11 12 13 14 15 16見つかりません
  • でバイナリ検索を適用しますが5 6 7 8見つかりません
  • でバイナリ検索を適用しますが6 7 8 9見つかりません

良いパフォーマンスを得るには、一般的な形状に応じて、いくつかのケースを区別したい場合があるので、注意が必要です。

3.長方形の検索、残忍なアプローチ

正方形を扱った方がはるかに簡単です...だから、物事を正方形にしましょう。

1  2  3  4  5  6  7  8
2  3  4  5  6  7  8  9
10 11 12 13 14 15 16 17
17 .  .  .  .  .  .  17
.                    .
.                    .
.                    .
17 .  .  .  .  .  .  17

これで正方形ができました。

もちろん、実際にはそれらの行を作成するのではなく、単にそれらをエミュレートすることができます。

def get(x,y):
  if x < N and y < M: return table[x][y]
  else: return table[N-1][M-1]            # the max

したがって、メモリを消費せずに正方形のように動作します(おそらくキャッシュによっては速度が犠牲になりますが...まあ:p)


0

編集:

その質問を誤解した。コメントが指摘するように、これはより制限されたケースでのみ機能します。

行優先順でデータを格納するCのような言語では、それを単にサイズn * mの1D配列として扱い、バイナリ検索を使用します。


はい、なぜ必要以上に複雑にするのですか?
erikkallen

配列は並べ替えられていないため、ビン検索を適用できません
Miollnyr

1
これは、各行の最後の要素が次の行の最初の要素よりも高い場合にのみ機能します。これは、問題が提案するよりもはるかに厳しい要件です。
ジェフリーLホイットレッジ

ありがとう、私は私の答えを編集しました。特に配列の例など、十分に注意深く読みませんでした。
ヒューブラケット

0

再帰的なDivide&Conquer Solutionがあります。1つのステップの基本的な考え方は次のとおりです。左上(LU)が最小で、右下(RB)が最大の番号であることを知っているため、指定されたNo(N)はN> = LUおよびN <=でなければなりません。 RB

IF N == LUおよびN == RB :::: Element FoundおよびAbortが位置/インデックスを返す場合N> = LUおよびN <= RB = FALSEの場合、Noは存在せず、異常終了します。N> = LUおよびN <= RB = TRUEの場合、2D配列を2D配列の4つの等しい部分にそれぞれ論理的に分割します。次に、同じアルゴリズムのステップを4つのサブ配列すべてに適用します。

私のアルゴは正しいです友達のPCに実装しました。複雑さ:各4つの比較は、要素の総数を最悪の場合の4分の1に推定するために使用できます。したがって、私の複雑さは1 + 4 x lg(n)+ 4になりますが、これは実際にOで機能すると予想されます(n)

Complexityの計算でどこかがおかしいと思います。もしそうなら修正してください。


0

最適なソリューションは、左上隅から開始することです。特定の要素の値> =の値を持つ要素に到達するまで、右下に斜めに下に移動します。要素の値が指定された要素の値と等しい場合、trueとして返されます。

それ以外の場合は、ここから2つの方法で進めることができます。

戦略1:

  1. 列を上に移動し、最後に到達するまで特定の要素を検索します。見つかった場合、trueとして返されます
  2. 行の左に移動し、最後に到達するまで指定された要素を検索します。見つかった場合は、trueとして返されます
  3. falseとして見つかりました

戦略2:iが行インデックスを示し、jが停止した対角要素の列インデックスを示すとします。(ここでは、i = j、BTWです)。k = 1とします。

  • ik> = 0になるまで以下の手順を繰り返します
    1. a [ik] [j]が指定された要素と等しいかどうかを検索します。はいの場合、trueとして返されます。
    2. a [i] [jk]が指定された要素と等しいかどうかを検索します。はいの場合、trueとして返されます。
    3. 増分k

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11


0
public boolean searchSortedMatrix(int arr[][] , int key , int minX , int maxX , int minY , int maxY){

    // base case for recursion
    if(minX > maxX || minY > maxY)
        return false ;
    // early fails
    // array not properly intialized
    if(arr==null || arr.length==0)
        return false ;
    // arr[0][0]> key return false
    if(arr[minX][minY]>key)
        return false ;
    // arr[maxX][maxY]<key return false
    if(arr[maxX][maxY]<key)
        return false ;
    //int temp1 = minX ;
    //int temp2 = minY ;
    int midX = (minX+maxX)/2 ;
    //if(temp1==midX){midX+=1 ;}
    int midY = (minY+maxY)/2 ;
    //if(temp2==midY){midY+=1 ;}


    // arr[midX][midY] = key ? then value found
    if(arr[midX][midY] == key)
        return true ;
    // alas ! i have to keep looking

    // arr[midX][midY] < key ? search right quad and bottom matrix ;
    if(arr[midX][midY] < key){
        if( searchSortedMatrix(arr ,key , minX,maxX , midY+1 , maxY))
            return true ;
        // search bottom half of matrix
        if( searchSortedMatrix(arr ,key , midX+1,maxX , minY , maxY))
            return true ;
    }
    // arr[midX][midY] > key ? search left quad matrix ;
    else {
         return(searchSortedMatrix(arr , key , minX,midX-1,minY,midY-1));
    }
    return false ;

}

0

すべてのキャラクターをに保存することをお勧めし2D listます。次に、リストに存在する場合、必要な要素のインデックスを見つけます。

存在しない場合は適切なメッセージを印刷し、そうでない場合は行と列を次のように印刷します。

row = (index/total_columns) そして column = (index%total_columns -1)

これにより、リスト内のバイナリ検索時間のみが発生します。

修正を提案してください。:)


0

もしO(Mログ(N))溶液のMxNアレイのOKです-

template <size_t n>
struct MN * get(int a[][n], int k, int M, int N){
  struct MN *result = new MN;
  result->m = -1;
  result->n = -1;

  /* Do a binary search on each row since rows (and columns too) are sorted. */
  for(int i = 0; i < M; i++){
    int lo = 0; int hi = N - 1;
    while(lo <= hi){
      int mid = lo + (hi-lo)/2;
      if(k < a[i][mid]) hi = mid - 1;
      else if (k > a[i][mid]) lo = mid + 1;
      else{
        result->m = i;
        result->n = mid;
        return result;
      }
    }
  }
  return result;
}

動作するC ++デモ。

これでうまくいかない場合やバグがある場合はお知らせください。


0

私はこの質問をインタビューで10年間のほとんどの部分で尋ねてきましたが、最適なアルゴリズムを考え出すことができたのは1人だけだったと思います。

私の解決策は常にされています:

  1. 中央の対角線であるを検索します。これは、にあるアイテムを含み、右下に実行される対角線(rows.count/2, columns.count/2)です。

  2. ターゲット番号が見つかった場合、trueを返します。

  3. それ以外の場合は、ターゲットよりも小さく、ターゲットよりも大きく、1つ右および1つ下の2つの数値(uおよびv)が見つかります。uvvu

  4. の右側uと上部、vおよび下部uと左側のサブマトリックスを再帰的に検索しvます。

これは、ネイトによってここで与えられたアルゴリズムに対する厳密な改善であると私は信じていますで対角線を検索すると、検索スペースの半分以上の削減が可能になるため(行列が正方形に近い場合)、行または列を検索すると常に除去が行われるためです。ちょうど半分の。

これが(恐らくSwifttyではないでしょう)Swiftのコードです:

import Cocoa

class Solution {
    func searchMatrix(_ matrix: [[Int]], _ target: Int) -> Bool {
        if (matrix.isEmpty || matrix[0].isEmpty) {
            return false
        }

        return _searchMatrix(matrix, 0..<matrix.count, 0..<matrix[0].count, target)
    }

    func _searchMatrix(_ matrix: [[Int]], _ rows: Range<Int>, _ columns: Range<Int>, _ target: Int) -> Bool {
        if (rows.count == 0 || columns.count == 0) {
            return false
        }
        if (rows.count == 1) {
            return _binarySearch(matrix, rows.lowerBound, columns, target, true)
        }
        if (columns.count == 1) {
            return _binarySearch(matrix, columns.lowerBound, rows, target, false)
        }

        var lowerInflection = (-1, -1)
        var upperInflection = (Int.max, Int.max)
        var currentRows = rows
        var currentColumns = columns
        while (currentRows.count > 0 && currentColumns.count > 0 && upperInflection.0 > lowerInflection.0+1) {
            let rowMidpoint = (currentRows.upperBound + currentRows.lowerBound) / 2
            let columnMidpoint = (currentColumns.upperBound + currentColumns.lowerBound) / 2
            let value = matrix[rowMidpoint][columnMidpoint]
            if (value == target) {
                return true
            }

            if (value > target) {
                upperInflection = (rowMidpoint, columnMidpoint)
                currentRows = currentRows.lowerBound..<rowMidpoint
                currentColumns = currentColumns.lowerBound..<columnMidpoint
            } else {
                lowerInflection = (rowMidpoint, columnMidpoint)
                currentRows = rowMidpoint+1..<currentRows.upperBound
                currentColumns = columnMidpoint+1..<currentColumns.upperBound
            }
        }
        if (lowerInflection.0 == -1) {
            lowerInflection = (upperInflection.0-1, upperInflection.1-1)
        } else if (upperInflection.0 == Int.max) {
            upperInflection = (lowerInflection.0+1, lowerInflection.1+1)
        }

        return _searchMatrix(matrix, rows.lowerBound..<lowerInflection.0+1, upperInflection.1..<columns.upperBound, target) || _searchMatrix(matrix, upperInflection.0..<rows.upperBound, columns.lowerBound..<lowerInflection.1+1, target)
    }

    func _binarySearch(_ matrix: [[Int]], _ rowOrColumn: Int, _ range: Range<Int>, _ target: Int, _ searchRow : Bool) -> Bool {
        if (range.isEmpty) {
            return false
        }

        let midpoint = (range.upperBound + range.lowerBound) / 2
        let value = (searchRow ? matrix[rowOrColumn][midpoint] : matrix[midpoint][rowOrColumn])
        if (value == target) {
            return true
        }

        if (value > target) {
            return _binarySearch(matrix, rowOrColumn, range.lowerBound..<midpoint, target, searchRow)
        } else {
            return _binarySearch(matrix, rowOrColumn, midpoint+1..<range.upperBound, target, searchRow)
        }
    }
}

-1

次のように正方行列があるとします。

[abc]
[def]
[ijk]

a <c、d <f、i <kであることはわかっています。わからないのは、d <cまたはd> cなどです。1次元のみの保証があります。

最後の要素(c、f、k)を見ると、一種のフィルターを実行できます。N<cですか?search():next()。したがって、行に対してn回の反復が行われ、各行は、バイナリ検索の場合はO(log(n))を、除外された場合はO(1)のいずれかを取ります。

N = jの例を挙げましょう。

1)行1を確認します。j<c?(いいえ、次に進みます)

2)行2を確認します。j<f?(はい、ビン検索は何もしません)

3)行3を確認します。j<k?(はい、ビン検索はそれを見つけます)

N = qで再試行します。

1)行1を確認します。q<c?(いいえ、次に進みます)

2)行2を確認します。q<f?(いいえ、次に進みます)

3)行3を確認します。q<k?(いいえ、次に進みます)

おそらくもっと良い解決策がありますが、これは簡単に説明できます.. :)


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.