私は最近、このインタビューの質問を受けましたが、それに対する良い解決策は何だろうと思っています。
配列内のすべての数値が左から右、上から下に向かって昇順である2D配列が与えられているとします。
ターゲット番号が配列内にあるかどうかを検索して判別する最良の方法は何ですか?
今、私の最初の傾向は、データがソートされているため、バイナリ検索を利用することです。 O(log N)時間で数値が1行にあるかどうかを判断できます。しかし、それは私を落とす2つの方向です。
私がうまくいくと思う別の解決策は、途中から始めることです。中央の値がターゲットよりも小さい場合、中央からマトリックスの左の正方形部分にあることを確認できます。次に、斜めに移動して再度チェックし、ターゲット番号に磨きをかけるまで、ターゲットが入る可能性のある正方形のサイズを小さくします。
この問題を解決するためのアイデアはありますか?
配列の例:
左から右、上から下に並べ替えられます。
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11
簡単なアプローチを次に示します。
NxM
配列の場合、これはO(N+M)
で実行されます。もっと良くするのは難しいと思います。 :)
編集:たくさんの良い議論。上記の一般的なケースについて話していました。明らかに、N
またはM
が小さい場合は、バイナリ検索アプローチを使用して、対数時間に近いものでこれを行うことができます。
好奇心those盛な人のための詳細を以下に示します。
この単純なアルゴリズムは Saddleback Search と呼ばれます。これはしばらく前からあり、_N == M
_の場合に最適です。いくつかの参照:
ただし、_N < M
_の場合、直観では、バイナリ検索はO(N+M)
よりも優れている必要があることが示唆されています。たとえば、_N == 1
_の場合、純粋なバイナリ検索は線形ではなく対数で実行されます時間。
リチャードバードは、2006年の論文で、バイナリ検索がサドルバックアルゴリズムを改善できるというこの直感を検証しました。
かなり珍しい会話手法を使用して、Birdは_N <= M
_の場合、この問題にはΩ(N * log(M/N))
の下限があることを示しています。 _N == M
_の場合は線形のパフォーマンスを、_N == 1
_の場合は対数のパフォーマンスを提供するため、この範囲は理にかなっています。
行ごとのバイナリ検索を使用する1つのアプローチは次のようになります。
N < M
_の長方形配列から始めます。 N
が行で、M
が列だとしましょう。value
の中央の行でバイナリ検索を実行します。見つかったら完了です。s
とg
の隣接するペアが見つかりました。ここで_s < value < g
_です。s
の上と左にある数字の長方形はvalue
より小さいので、これを削除できます。g
の下と右側の長方形はvalue
よりも大きいため、削除できます。最悪の場合の複雑さという点では、このアルゴリズムはlog(M)
を実行して可能な解決策の半分を排除し、2つの小さな問題で再帰的に2回呼び出します。すべての行に対してlog(M)
workの小さいバージョンを繰り返す必要がありますが、列の数と比較して行の数が少ない場合は、対数時間でこれらの列をすべて削除し、価値があり始めます。
これは、アルゴリズムに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つのアルゴリズムを調べています。
(「単純な」アルゴリズムは、配列のすべての要素を単純に検索します。「再帰的な」アルゴリズムは上記のとおりです。「ハイブリッド」アルゴリズムは、 Gidneyのアルゴリズム の実装です。 1,000,000のランダムに生成された配列の固定セットで各アルゴリズムのタイミングを調整します。)
注目すべき点:
バイナリ検索を巧妙に使用すると、長方形配列と正方形配列の両方に対してO(N * log(M/N)
パフォーマンスを提供できます。 O(N + M)
"saddleback"アルゴリズムははるかに単純ですが、配列がますます長方形になるにつれてパフォーマンスが低下します。
この問題には、Θ(b lg(t))
およびb = min(w,h)
のt=b/max(w,h)
時間かかります。 このブログ投稿 で解決策を説明します。
下限
攻撃者は、アルゴリズムを強制的にΩ(b lg(t))
クエリを作成させ、自身を主対角線に制限することができます。
凡例:白いセルは小さいアイテム、灰色のセルは大きいアイテム、黄色のセルは小さいか等しいアイテム、オレンジのセルは大きいか等しいアイテムです。攻撃者は、アルゴリズムが最後に照会する黄色またはオレンジ色のセルをソリューションに強制します。
サイズb
のt
独立ソートリストがあり、Ω(b lg(t))
クエリを完全に排除する必要があることに注意してください。
アルゴリズム
w >= h
_と仮定します)t
とターゲットアイテムを比較しますt
セルを削除します。これを行っている間に一致するアイテムが見つかった場合、その位置で戻ります。t
短い列は削除されます。アイテムを見つける:
アイテムが存在しないと判断する:
凡例:白いセルは小さいアイテム、灰色のセルは大きいアイテム、緑色のセルは等しいアイテムです。
分析
削除する_b*t
_短い列があります。削除するb
長い行があります。長い行を削除するには、O(lg(t))
時間かかります。 t
短い列を削除すると、O(1)
時間かかります。
最悪の場合、O(lg(t)*b + b*t*1/t) = O(b lg(t))
の時間をかけてすべての列とすべての行を削除する必要があります。
lg
が1を超える結果にクランプすると仮定していることに注意してください(つまり、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;
}
_
この問題には、あなたが提案したものと同様の分割統治戦略を使用しますが、詳細は少し異なります。
これは、マトリックスの部分範囲での再帰検索になります。
各ステップで、範囲の中央にある要素を選択します。見つかった値が探しているものであれば、これで完了です。
そうではなく、見つかった値が探している値よりも小さい場合、現在の位置の左上の象限にないことがわかります。したがって、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)
}
これまでの主な2つの答えは、ほぼ間違いなくO(log N)
"ZigZag method"とO(N+M)
Binary Searchメソッドのようです。 2つの方法をいくつかのさまざまなセットアップと比較して、テストを行うと思いました。詳細は次のとおりです。
配列はすべてのテストでN x Nの正方形で、Nは125から8000(JVMヒープが処理できる最大の値)で変化します。配列サイズごとに、配列内のランダムな場所を選択して、単一の2
。次に、3
可能であればどこでも(2の右下)、配列の残りの部分を1
。初期のコメンターの一部は、このタイプのセットアップは両方のアルゴリズムの最悪の実行時間をもたらすと考えているようでした。配列サイズごとに、2(検索ターゲット)に対して100個の異なるランダムな場所を選択し、テストを実行しました。各アルゴリズムの平均実行時間と最悪の場合の実行時間を記録しました。 Javaで適切なmsの読み取り値を取得するには速すぎて、JavaのnanoTime()を信頼していないため、常に一定のバイアス係数を追加するために各テストを1000回繰り返しました。結果は次のとおりです。
ZigZagは、平均および最悪の両方のケースで、すべてのテストでバイナリに勝ちますが、それらはすべて、互いに多少の差があります。
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");
}
}
これは、問題の下限の短い証拠です。
線形時間(要素の数ではなく、配列の次元)を超えることはできません。以下の配列では、*
としてマークされた各要素は、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変数関数として扱われる実行時間を改善できます。
私はここが答えだと思います
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));
}
}
興味深い質問。この考えを考慮してください。すべての数値がターゲットよりも大きい境界と、すべての数値がターゲットよりも小さい境界を作成します。 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をすべてカウントするという追加のボーナスがあります。
いくつかの例でこれを試しましたが、うまくいくようです。
配列の対角線を通るバイナリ検索が最適なオプションです。要素が対角線の要素以下かどうかを調べることができます。
すべての文字を_2D list
_に保存することをお勧めします。リストに存在する場合、必要な要素のインデックスを見つけます。
存在しない場合は適切なメッセージを印刷し、そうでない場合は行と列を次のように印刷します。
row = (index/total_columns)
およびcolumn = (index%total_columns -1)
これにより、リスト内のバイナリ検索時間が発生します。
修正を提案してください。 :)
A.ターゲット番号がオンになっている可能性のある行でバイナリ検索を実行します。
B.グラフにする:常に最小の未訪問の隣接ノードを取得し、大きすぎる数が見つかった場合にバックトラックして、数を探します
バイナリ検索が最良のアプローチです、imo。 1/2 xから始まり、1/2 yで半分になります。 IE 5x5の正方形はx == 2/y == 3のようなものになります。ターゲット値の方向に合わせて、1つの値を切り下げ、1つの値を切り上げます。
わかりやすくするために、次の反復ではx == 1/y == 2 OR x == 3/y == 5
O(M log(N))解がMxN配列で問題ない場合-
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;
}
これでうまくいかない場合やバグがある場合はお知らせください。
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 ;
}
編集:
私は質問を誤解しました。コメントが指摘しているように、これはより制限された場合にのみ機能します。
行優先の順序でデータを格納するCのような言語では、単純にそれをサイズn * mの1D配列として扱い、バイナリ検索を使用します。
まず、正方形を使用していると仮定しましょう。
_1 2 3
2 3 4
3 4 5
_
1。正方形の検索
対角線上でバイナリ検索を使用します。目標は、ターゲット番号より厳密に低くない小さい番号を見つけることです。
たとえば、_4
_を探しているとすると、_5
_を_(2,2)
_に配置することになります。
次に、_4
_がテーブルにある場合、_(x,2)
_のx
とともに_(2,x)
_または_[0,2]
_のいずれかの位置にあることが保証されます。まあ、それは2回のバイナリ検索です。
複雑さは難しくありません:O(log(N))
(長さN
の範囲で3つのバイナリ検索)
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
_
したがって、再帰することができます!おそらく、要素の少ないものから始めます(ただし、この場合は私たちを殺します)。
ディメンションの1つが_3
_より小さい場合、対角法を適用できず、バイナリ検索を使用する必要があることを指摘する必要があります。ここでは次のことを意味します。
10 11 12 13 14 15 16
_にバイナリ検索を適用します5 6 7 8
_にバイナリ検索を適用します6 7 8 9
_にバイナリ検索を適用します良いパフォーマンスを得るには、一般的な形状に応じていくつかのケースを区別する必要があるため、注意が必要です。
。長方形の検索、残忍なアプローチ
正方形を扱うとはるかに簡単になります...それでは、正方形にしましょう。
_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)
再帰的な分割統治ソリューションがあります。 1ステップの基本的な考え方は次のとおりです。左上(LU)が最小で、右下(RB)が最大番号であることがわかっているため、指定されたNo(N)はN> = LUおよびN <=でなければなりませんRB
N == LUおよびN == RB :::: Elementが見つかり、位置/インデックスを返す中止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)
複雑さの計算のどこかで何かが間違っていると思います。
最適なソリューションは、左上隅から開始することです。これは、最小の値を持ちます。値が指定された要素の値以上の要素に到達するまで、斜め下に右に移動します。要素の値が指定された要素の値と等しい場合、found foundをtrueとして返します。
それ以外の場合は、ここから2つの方法で進めることができます。
戦略1:
戦略2:iで行インデックスを示し、jで停止した対角要素の列インデックスを示します。 (ここでは、i = j、BTWです)。 k = 1とします。
1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11