Wie suche ich nach einer Zahl in einem 2D-Array, das von links nach rechts und von oben nach unten sortiert ist?


90

Vor kurzem wurde mir diese Interviewfrage gestellt und ich bin gespannt, was für eine gute Lösung das wäre.

Angenommen, ich erhalte ein 2D-Array, in dem alle Zahlen im Array von links nach rechts und von oben nach unten in aufsteigender Reihenfolge aufgeführt sind.

Was ist der beste Weg, um zu suchen und festzustellen, ob sich eine Zielnummer im Array befindet?

Meine erste Neigung besteht nun darin, eine binäre Suche zu verwenden, da meine Daten sortiert sind. Ich kann in O (log N) feststellen, ob sich eine Zahl in einer einzelnen Zeile befindet. Es sind jedoch die 2 Richtungen, die mich abschrecken.

Eine andere Lösung, von der ich dachte, dass sie funktionieren könnte, besteht darin, irgendwo in der Mitte zu beginnen. Wenn der Mittelwert kleiner als mein Ziel ist, kann ich sicher sein, dass er sich im linken quadratischen Teil der Matrix von der Mitte befindet. Ich bewege mich dann diagonal und überprüfe erneut, wobei ich die Größe des Quadrats reduziere, in dem sich das Ziel möglicherweise befinden könnte, bis ich die Zielnummer verfeinert habe.

Hat jemand gute Ideen zur Lösung dieses Problems?

Beispielarray:

Von links nach rechts, von oben nach unten sortiert.

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

Einfache Frage: Kann es sein, dass Sie einen Nachbarn mit demselben Wert haben können : [[1 1][1 1]]?
Matthieu M.

Antworten:


115

Hier ist ein einfacher Ansatz:

  1. Beginnen Sie in der unteren linken Ecke.
  2. Wenn das Ziel weniger ist als dieser Wert, muss es über uns, so eine nach oben .
  3. Andernfalls wissen wir, dass sich das Ziel nicht in dieser Spalte befinden kann. Bewegen Sie sich also nach rechts .
  4. Gehe zu 2.

Für ein NxMArray wird dies ausgeführt O(N+M). Ich denke, es wäre schwierig, es besser zu machen. :) :)


Edit: Viele gute Diskussionen. Ich habe über den allgemeinen Fall oben gesprochen; Wenn Sie klein sind Noder Msind, können Sie einen binären Suchansatz verwenden, um dies in einer Zeit zu tun, die sich der logarithmischen Zeit nähert.

Hier sind einige Details für diejenigen, die neugierig sind:

Geschichte

Dieser einfache Algorithmus wird als Saddleback-Suche bezeichnet . Es gibt es schon eine Weile und es ist optimal, wenn N == M. Einige Referenzen:

Wenn jedoch die N < MIntuition vorschlägt, dass die binäre Suche besser funktionieren sollte als O(N+M): Zum Beispiel, wenn N == 1eine reine binäre Suche eher in logarithmischer als in linearer Zeit ausgeführt wird.

Worst-Case gebunden

Richard Bird untersuchte diese Intuition, dass die binäre Suche den Saddleback-Algorithmus verbessern könnte, in einem Artikel aus dem Jahr 2006:

Mit einer eher ungewöhnlichen Konversationstechnik zeigt uns Bird, dass N <= Mdieses Problem eine Untergrenze von hat Ω(N * log(M/N)). Diese Grenze ist sinnvoll, da sie uns eine lineare Leistung wann N == Mund eine logarithmische Leistung wann gibt N == 1.

Algorithmen für rechteckige Arrays

Ein Ansatz, der eine zeilenweise binäre Suche verwendet, sieht folgendermaßen aus:

  1. Beginnen Sie mit einem rechteckigen Array, wo N < M. Angenommen, es Nhandelt sich um Zeilen und MSpalten.
  2. Führen Sie in der mittleren Zeile eine binäre Suche nach durch value. Wenn wir es finden, sind wir fertig.
  3. Ansonsten haben wir ein benachbartes Zahlenpaar gefunden sund g, wo s < value < g.
  4. Das Rechteck der Zahlen über und links von sist kleiner als value, sodass wir es entfernen können.
  5. Das Rechteck unter und rechts von gist größer als value, sodass wir es entfernen können.
  6. Fahren Sie mit Schritt (2) für jedes der beiden verbleibenden Rechtecke fort.

In Bezug auf die Komplexität im schlimmsten Fall log(M)eliminiert dieser Algorithmus die Hälfte der möglichen Lösungen und ruft sich dann bei zwei kleineren Problemen zweimal rekursiv auf. Wir müssen eine kleinere Version dieser log(M)Arbeit für jede Zeile wiederholen , aber wenn die Anzahl der Zeilen im Vergleich zur Anzahl der Spalten gering ist, lohnt es sich, alle diese Spalten in logarithmischer Zeit zu entfernen .

Dies gibt dem Algorithmus eine Komplexität T(N,M) = log(M) + 2 * T(M/2, N/2), die Bird zeigt O(N * log(M/N)).

Ein anderer von Craig Gidney veröffentlichter Ansatz beschreibt einen Algorithmus, der dem obigen Ansatz ähnlich ist: Er untersucht jeweils eine Zeile mit einer Schrittgröße von M/N. Seine Analyse zeigt, dass dies auch zu O(N * log(M/N))Leistung führt.

Leistungsvergleich

Die Big-O-Analyse ist gut und schön, aber wie gut funktionieren diese Ansätze in der Praxis? In der folgenden Tabelle werden vier Algorithmen für zunehmend "quadratische" Arrays untersucht:

Algorithmusleistung gegen Rechtwinkligkeit

(Der "naive" Algorithmus durchsucht einfach jedes Element des Arrays. Der "rekursive" Algorithmus ist oben beschrieben. Der "hybride" Algorithmus ist eine Implementierung des Gidney-Algorithmus . Für jede Arraygröße wurde die Leistung gemessen, indem jeder Algorithmus über einen festen Satz gesteuert wurde von 1.000.000 zufällig generierten Arrays.)

Einige bemerkenswerte Punkte:

  • Wie erwartet bieten die "binären Such" -Algorithmen die beste Leistung für rechteckige Arrays, und der Saddleback-Algorithmus funktioniert am besten für quadratische Arrays.
  • Der Saddleback-Algorithmus ist schlechter als der "naive" Algorithmus für 1-d-Arrays, vermutlich weil er für jedes Element mehrere Vergleiche durchführt.
  • Der Leistungseinbruch, den die "binären Such" -Algorithmen bei quadratischen Arrays erzielen, ist vermutlich auf den Aufwand zurückzuführen, der durch das Ausführen wiederholter binärer Suchvorgänge entsteht.

Zusammenfassung

Die clevere Verwendung der binären Suche kann sowohl O(N * log(M/N)für rechteckige als auch für quadratische Arrays Leistung bringen. Der O(N + M)"Saddleback" -Algorithmus ist viel einfacher, leidet jedoch unter Leistungseinbußen, da Arrays zunehmend rechteckiger werden.


6
Wenden Sie die binäre Suche auf den diagonalen Gang an und Sie haben O (logN) oder O (logM), je nachdem, welcher Wert höher ist.
Anurag

3
@ Anurag - Ich denke nicht, dass die Komplexität so gut funktioniert. Eine binäre Suche gibt Ihnen einen guten Ausgangspunkt, aber Sie müssen die eine oder andere Dimension den ganzen Weg gehen, und im schlimmsten Fall können Sie immer noch in einer Ecke beginnen und in der anderen enden.
Jeffrey L Whitledge

1
Wenn N = 1 und M = 1000000, kann ich es besser machen als O (N + M). Eine andere Lösung besteht darin, in jeder Zeile eine binäre Suche anzuwenden, die O (N * log (M)) bringt, wobei N <M ist, falls dies ergibt kleinere Konstante.
Luka Rahne

1
Ich habe einige Tests sowohl mit Ihrer Methode als auch mit der binären Suchmethode durchgeführt und die Ergebnisse HIER veröffentlicht . Die Zick-Zack-Methode scheint die beste zu sein, es sei denn, ich habe die Worst-Case-Bedingungen für beide Methoden nicht ordnungsgemäß generiert.
The111

1
Gute Verwendung von Referenzen! Wenn M==Nwir jedoch O(N)Komplexität wollen , nicht, O(N*log(N/N))da letztere Null ist. Eine korrekte "einheitliche" scharfe Grenze ist O(N*(log(M/N)+1))wann N<=M.
Hardmath

35

Dieses Problem braucht Θ(b lg(t))Zeit, wo b = min(w,h)und t=b/max(w,h). Ich diskutiere die Lösung in diesem Blog-Beitrag .

Untergrenze

Ein Gegner kann einen Algorithmus zwingen, Ω(b lg(t))Abfragen durchzuführen, indem er sich auf die Hauptdiagonale beschränkt:

Gegner mit Hauptdiagonale

Legende: Weiße Zellen sind kleinere Elemente, graue Zellen sind größere Elemente, gelbe Zellen sind kleinere oder gleiche Elemente und orange Zellen sind größere oder gleiche Elemente. Der Gegner erzwingt, dass die Lösung die gelbe oder orangefarbene Zelle ist, die der Algorithmus zuletzt abfragt.

Beachten Sie, dass es bunabhängige sortierte Größenlisten gibt t, bei denen Ω(b lg(t))Abfragen vollständig entfernt werden müssen.

Algorithmus

  1. (Nehmen Sie ohne Verlust der Allgemeinheit an, dass w >= h)
  2. Vergleichen Sie das Zielelement mit der Zelle tlinks von der oberen rechten Ecke des gültigen Bereichs
    • Wenn das Element der Zelle übereinstimmt, geben Sie die aktuelle Position zurück.
    • Wenn das Element der Zelle kleiner als das Zielelement ist, entfernen Sie die verbleibenden tZellen in der Zeile mit einer binären Suche. Wenn dabei ein passender Artikel gefunden wird, kehren Sie mit seiner Position zurück.
    • Andernfalls ist das Element der Zelle mehr als das Zielelement, wodurch tkurze Spalten eliminiert werden .
  3. Wenn kein gültiger Bereich mehr vorhanden ist, geben Sie den Fehler zurück
  4. Weiter zu Schritt 2

Einen Gegenstand finden:

Einen Gegenstand finden

Bestimmen eines Elements existiert nicht:

Das Bestimmen eines Elements existiert nicht

Legende: Weiße Zellen sind kleinere Elemente, graue Zellen sind größere Elemente und die grüne Zelle ist ein gleiches Element.

Analyse

Es sind b*tkurze Spalten zu entfernen. Es sind blange Reihen zu beseitigen. Das Eliminieren einer langen Reihe kostet O(lg(t))Zeit. Das Eliminieren tkurzer Spalten kostet O(1)Zeit.

Im schlimmsten Fall müssen wir jede Spalte und jede Zeile entfernen, was einige Zeit in Anspruch nimmt O(lg(t)*b + b*t*1/t) = O(b lg(t)).

Beachten Sie, dass ich lgKlammern für ein Ergebnis über 1 (dh lg(x) = log_2(max(2,x))) annehme . Das ist der Grund w=h, warum t=1wir, wenn wir das tun, die erwartete Grenze bekommen O(b lg(1)) = O(b) = O(w+h).

Code

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
Interessant und möglicherweise teilweise über meinem Kopf. Ich bin mit diesem "gegnerischen" Stil der Komplexitätsanalyse nicht vertraut. Ändert der Gegner das Array während der Suche tatsächlich dynamisch, oder ist er nur ein Name für das Pech, dem Sie bei einer Suche im schlimmsten Fall begegnen?
The111

2
@ The111 Pech ist gleichbedeutend damit, dass jemand einen schlechten Weg wählt, der nicht gegen die bisher gesehenen Dinge verstößt, sodass beide Definitionen gleich funktionieren. Ich habe tatsächlich Probleme, Links zu finden, die die Technik speziell in Bezug auf die Komplexität der Berechnungen erklären ... Ich dachte, dies sei eine viel bekanntere Idee.
Craig Gidney

Da log (1) = 0 ist, sollte die Komplexitätsschätzung als O(b*(lg(t)+1))statt angegeben werden O(b*lg(t)). Nettes Schreiben, insb. um auf die "gegnerische Technik" aufmerksam zu machen, indem sie eine "Worst-Case" -Bindung zeigt.
Hardmath

@hardmath Ich erwähne das in der Antwort. Ich habe es ein bisschen geklärt.
Craig Gidney

17

Ich würde die Divide-and-Conquer-Strategie für dieses Problem verwenden, ähnlich wie Sie es vorgeschlagen haben, aber die Details sind etwas anders.

Dies ist eine rekursive Suche nach Unterbereichen der Matrix.

Wählen Sie bei jedem Schritt ein Element in der Mitte des Bereichs aus. Wenn der gefundene Wert das ist, was Sie suchen, sind Sie fertig.

Wenn der gefundene Wert geringer ist als der gesuchte Wert, wissen Sie, dass er sich nicht im Quadranten über und links von Ihrer aktuellen Position befindet. Suchen Sie also rekursiv die beiden Unterbereiche: alles (ausschließlich) unter der aktuellen Position und alles (ausschließlich) rechts, das sich an oder über der aktuellen Position befindet.

Andernfalls (der gefundene Wert ist größer als der gesuchte Wert) wissen Sie, dass er sich nicht im Quadranten unterhalb und rechts von Ihrer aktuellen Position befindet. Suchen Sie also rekursiv die beiden Unterbereiche: alles (ausschließlich) links von der aktuellen Position und alles (ausschließlich) über der aktuellen Position in der aktuellen Spalte oder eine Spalte rechts.

Und ba-da-bing, du hast es gefunden.

Beachten Sie, dass jeder rekursive Aufruf nur den aktuellen Unterbereich behandelt, nicht (zum Beispiel) ALLE Zeilen über der aktuellen Position. Nur die im aktuellen Unterbereich.

Hier ist ein Pseudocode für Sie:

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: Dies ist eine O-Strategie (log (N)) und daher eine so gute Reihenfolge, wie man sie bekommen wird.
Rex Kerr

3
@Rex Kerr - Es sieht aus wie O (log (N)), da dies eine normale binäre Suche ist. Beachten Sie jedoch, dass es auf jeder Ebene möglicherweise zwei rekursive Aufrufe gibt. Dies bedeutet, dass es viel schlimmer ist als normales Logarithmus. Ich glaube nicht, dass der schlimmste Fall besser ist als O (M + N), da möglicherweise jede Zeile oder jede Spalte durchsucht werden muss. Ich würde jedoch vermuten, dass dieser Algorithmus den schlechtesten Fall für viele Werte übertreffen könnte. Und das Beste daran ist, dass es paralellierbar ist, da dort in letzter Zeit die Hardware eingesetzt wird.
Jeffrey L Whitledge

1
@JLW: Es ist O (log (N)) - aber es ist tatsächlich O (log_ (4/3) (N ^ 2)) oder so ähnlich. Siehe Svantes Antwort unten. Ihre Antwort ist tatsächlich dieselbe (wenn Sie so rekursiv gemeint haben, wie ich es glaube).
Rex Kerr

1
@Svante - Die Subarrays überlappen sich nicht. Bei der ersten Option haben sie kein gemeinsames y-Element. Bei der zweiten Option haben sie kein gemeinsames x-Element.
Jeffrey L Whitledge

1
Ich bin mir nicht sicher, ob dies logarithmisch ist. Ich berechnete die Komplexität unter Verwendung der ungefähren Wiederholungsrelation T (0) = 1, T (A) = T (A / 2) + T (A / 4) + 1, wobei A der Suchbereich ist, und endete mit T ( A) = O (Fib (lg (A))), was ungefähr O (A ^ 0,7) und schlechter als O (n + m) ist, was O (A ^ 0,5) ist. Vielleicht habe ich einen dummen Fehler gemacht, aber es sieht so aus, als würde der Algorithmus viel Zeit damit verschwenden, fruchtlose Zweige hinunterzugehen.
Craig Gidney

6

Die beiden bisherigen Hauptantworten scheinen die wohl O(log N)"ZigZag-Methode" und die O(N+M)binäre Suchmethode zu sein. Ich dachte, ich würde einige Tests durchführen, um die beiden Methoden mit verschiedenen Setups zu vergleichen. Hier sind die Details:

Das Array ist in jedem Test N x N Quadrat, wobei N zwischen 125 und 8000 variiert (der größte, den mein JVM-Heap verarbeiten kann). Für jede Arraygröße habe ich eine zufällige Stelle im Array ausgewählt, um eine einzelne zu platzieren 2. Ich habe dann ein 3überall mögliches (rechts und unterhalb der 2) platziert und dann den Rest des Arrays mit gefüllt1. Einige der früheren Kommentatoren schienen zu glauben, dass diese Art der Einrichtung für beide Algorithmen eine Worst-Case-Laufzeit ergeben würde. Für jede Arraygröße habe ich 100 verschiedene zufällige Positionen für die 2 (Suchziel) ausgewählt und den Test durchgeführt. Ich habe für jeden Algorithmus die durchschnittliche Laufzeit und die Worst-Case-Laufzeit aufgezeichnet. Da es zu schnell ging, um gute MS-Werte in Java zu erhalten, und weil ich Javas nanoTime () nicht vertraue, habe ich jeden Test 1000 Mal wiederholt, um immer einen einheitlichen Bias-Faktor hinzuzufügen. Hier sind die Ergebnisse:

Geben Sie hier die Bildbeschreibung ein

ZigZag schlug Binär in jedem Test sowohl für die Durchschnitts- als auch für die Worst-Case-Zeiten, sie liegen jedoch alle mehr oder weniger in einer Größenordnung voneinander.

Hier ist der Java-Code:

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 Ja, Daten. :) Es könnte auch interessant sein zu sehen, wie sich diese beiden Ansätze auf NxM-Arrays entwickeln, da die binäre Suche intuitiv nützlicher zu werden scheint, je mehr wir uns einem eindimensionalen Fall nähern.
Nate Kohl

5

Dies ist ein kurzer Beweis für die Untergrenze des Problems.

Sie können es nicht besser machen als die lineare Zeit (in Bezug auf die Array-Dimensionen, nicht die Anzahl der Elemente). Im folgenden Array kann jedes der als *5 oder 6 gekennzeichneten Elemente (unabhängig von anderen) sein. Wenn Ihr Zielwert also 6 (oder 5) ist, muss der Algorithmus alle untersuchen.

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

Dies erweitert sich natürlich auch auf größere Arrays. Dies bedeutet, dass diese Antwort optimal ist.

Update: Wie von Jeffrey L Whitledge hervorgehoben, ist es nur als asymptotische Untergrenze der Laufzeit gegenüber der Größe der Eingabedaten (als einzelne Variable behandelt) optimal. Die Laufzeit, die in beiden Array-Dimensionen als Funktion mit zwei Variablen behandelt wird, kann verbessert werden.


Sie haben nicht gezeigt, dass diese Antwort optimal ist. Stellen Sie sich zum Beispiel ein Array mit einem Durchmesser von zehn und einer Million vor, in dem die fünfte Zeile Werte enthält, die alle höher als der Zielwert sind. In diesem Fall führt der vorgeschlagene Algorithmus eine Linier-Suche nach 999.995 Werten durch, bevor er sich dem Ziel nähert. Ein Bifurkationsalgorithmus wie meiner durchsucht nur 18 Werte, bevor er sich dem Ziel nähert. Und es ist (asymtotisch) in allen anderen Fällen nicht schlechter als der vorgeschlagene Algorithmus.
Jeffrey L Whitledge

@ Jeffrey: Es ist eine Untergrenze für das Problem für den pessimistischen Fall. Sie können für gute Eingaben optimieren, aber es gibt Eingaben, bei denen Sie nicht besser als linear arbeiten können.
Rafał Dowgird

Ja, es gibt Eingaben, bei denen Sie nicht besser als linear arbeiten können. In diesem Fall führt mein Algorithmus diese lineare Suche durch. Es gibt aber auch andere Eingaben, bei denen Sie viel besser als linear arbeiten können. Daher ist die vorgeschlagene Lösung nicht optimal, da immer eine lineare Suche durchgeführt wird.
Jeffrey L Whitledge

Dies zeigt, dass der Algorithmus BigOmega (min (n, m)) Zeit benötigen muss, nicht BigOmega (n + m). Deshalb können Sie es viel besser machen, wenn eine Dimension deutlich kleiner ist. Wenn Sie beispielsweise wissen, dass nur eine Zeile vorhanden ist, können Sie das Problem in logarithmischer Zeit lösen. Ich denke, ein optimaler Algorithmus braucht Zeit O (min (n + m, n lg m, m lg n)).
Craig Gidney

Die Antwort wurde entsprechend aktualisiert.
Rafał Dowgird

4

Ich denke, hier ist die Antwort und sie funktioniert für jede Art von sortierter Matrix

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

Interessante Frage. Betrachten Sie diese Idee - erstellen Sie eine Grenze, an der alle Zahlen größer als Ihr Ziel sind, und eine andere, an der alle Zahlen kleiner als Ihr Ziel sind. Wenn noch etwas zwischen den beiden übrig ist, ist das Ihr Ziel.

Wenn ich in Ihrem Beispiel nach 3 suche, lese ich in der ersten Zeile, bis ich 4 drücke, und suche dann nach der kleinsten benachbarten Zahl (einschließlich Diagonalen) größer als 3:

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

Jetzt mache ich dasselbe für Zahlen unter 3:

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

Jetzt frage ich, liegt etwas innerhalb der beiden Grenzen? Wenn ja, muss es 3 sein. Wenn nein, dann gibt es keine 3. Art von indirekt, da ich die Nummer nicht wirklich finde, schließe ich nur, dass sie dort sein muss. Dies hat den zusätzlichen Bonus, ALLE 3 zu zählen.

Ich habe dies an einigen Beispielen versucht und es scheint in Ordnung zu funktionieren.


Eine Abstimmung ohne Kommentar? Ich denke, dies ist O (N ^ 1/2), da die Leistung im ungünstigsten Fall eine Überprüfung der Diagonale erfordert. Zeigen Sie mir zumindest ein Gegenbeispiel, bei dem diese Methode nicht funktioniert!
Grembo

+1: schöne Lösung ... kreativ und gut, dass es alle Lösungen findet.
Tony Delroy

1

Die binäre Suche durch die Diagonale des Arrays ist die beste Option. Wir können herausfinden, ob das Element kleiner oder gleich den Elementen in der Diagonale ist.


0

A. Führen Sie eine binäre Suche in den Zeilen durch, in denen sich möglicherweise die Zielnummer befindet.

B. Machen Sie es zu einem Diagramm: Suchen Sie nach der Nummer, indem Sie immer den kleinsten nicht besuchten Nachbarknoten nehmen und zurückverfolgen, wenn eine zu große Nummer gefunden wird


0

Binäre Suche wäre der beste Ansatz, imo. Ab 1/2 x schneidet 1/2 y es in zwei Hälften. IE ein 5x5 Quadrat wäre so etwas wie x == 2 / y == 3. Ich habe einen Wert nach unten und einen Wert nach oben gerundet, um eine bessere Zone in Richtung des Zielwerts zu erreichen.

Aus Gründen der Klarheit würde die nächste Iteration so etwas wie x == 1 / y == 2 ODER x == 3 / y == 5 ergeben


0

Nehmen wir zunächst an, wir verwenden ein Quadrat.

1 2 3
2 3 4
3 4 5

1. Ein Quadrat suchen

Ich würde eine binäre Suche auf der Diagonale verwenden. Das Ziel ist es, die kleinere Zahl zu lokalisieren, die nicht streng niedriger als die Zielzahl ist.

Angenommen, ich suche zum 4Beispiel, dann würde ich am Ende 5bei suchen (2,2).

Dann bin ich mir sicher, dass wenn 4es in der Tabelle ist, es an einer Position entweder (x,2)oder (2,x)mit xin ist [0,2]. Nun, das sind nur 2 binäre Suchen.

Die Komplexität ist nicht entmutigend: O(log(N))(3 binäre Suchen nach Längenbereichen N)

2. Suche nach einem rechteckigen, naiven Ansatz

Natürlich wird es etwas komplizierter, wenn Nund Munterscheiden Sie sich (mit einem Rechteck), betrachten Sie diesen entarteten Fall:

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

Nehmen wir an, ich suche 9... Der diagonale Ansatz ist immer noch gut, aber die Definition von diagonalen Änderungen. Hier ist meine Diagonale [1, (5 or 6), 17]. Nehmen wir an, ich habe abgeholt [1,5,17], dann weiß ich, dass wenn 9es in der Tabelle ist, es entweder im Unterabschnitt ist:

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

Dies gibt uns 2 Rechtecke:

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

So können wir zurückgreifen! wahrscheinlich beginnend mit dem mit weniger Elementen (obwohl es uns in diesem Fall umbringt).

Ich sollte darauf hinweisen, dass 3wir , wenn eine der Dimensionen kleiner als ist , die diagonalen Methoden nicht anwenden können und eine binäre Suche verwenden müssen. Hier würde es bedeuten:

  • Wenden Sie die binäre Suche auf an 10 11 12 13 14 15 16, nicht gefunden
  • Wenden Sie die binäre Suche auf an 5 6 7 8, nicht gefunden
  • Wenden Sie die binäre Suche auf an 6 7 8 9, nicht gefunden

Es ist schwierig, denn um eine gute Leistung zu erzielen, sollten Sie je nach allgemeiner Form zwischen mehreren Fällen unterscheiden.

3. Suche nach einem rechteckigen, brutalen Ansatz

Es wäre viel einfacher, wenn wir uns mit einem Quadrat befassen würden ... also lasst uns einfach die Dinge zusammenfassen.

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

Wir haben jetzt ein Quadrat.

Natürlich werden wir diese Zeilen wahrscheinlich NICHT erstellen, wir könnten sie einfach emulieren.

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

es verhält sich also wie ein Quadrat, ohne mehr Speicher zu belegen (auf Kosten der Geschwindigkeit, wahrscheinlich abhängig vom Cache ... na ja: p)


0

BEARBEITEN:

Ich habe die Frage falsch verstanden. Wie die Kommentare zeigen, funktioniert dies nur im eingeschränkteren Fall.

In einer Sprache wie C, in der Daten in Zeilenreihenfolge gespeichert werden, behandeln Sie sie einfach als 1D-Array der Größe n * m und verwenden Sie eine binäre Suche.


Ja, warum sollte es komplexer sein, als es sein muss?
Erikkallen

Array ist nicht sortiert, daher kann keine Bin-Suche darauf angewendet werden
Miollnyr

1
Dies funktioniert nur, wenn das letzte Element jeder Zeile höher ist als das erste Element in der nächsten Zeile. Dies ist eine viel restriktivere Anforderung als das Problem vorschlägt.
Jeffrey L Whitledge

Danke, ich habe meine Antwort bearbeitet. Ich habe nicht sorgfältig genug gelesen, insbesondere das Beispielarray.
Hugh Brackett

0

Ich habe eine rekursive Divide & Conquer-Lösung. Grundidee für einen Schritt ist: Wir wissen, dass das linke obere (LU) am kleinsten und das rechte untere (RB) die größte Nr. Ist, daher muss das gegebene Nein (N): N> = LU und N <= sein RB

WENN N == LU und N == RB :::: Element gefunden und abgebrochen, wobei die Position / der Index zurückgegeben wird Wenn N> = LU und N <= RB = FALSE, ist Nein nicht vorhanden und wird abgebrochen. Wenn N> = LU und N <= RB = TRUE, teilen Sie das 2D-Array auf logische Weise in 4 gleiche Teile des 2D-Arrays. Wenden Sie dann denselben Algo-Schritt auf alle vier Sub-Arrays an.

Mein Algo ist korrekt Ich habe es auf dem PC meines Freundes implementiert. Komplexität: Jede 4 Vergleiche kann verwendet werden, um die Gesamtzahl der Elemente im schlimmsten Fall auf ein Viertel abzuleiten. Meine Komplexität beträgt also 1 + 4 x lg (n) + 4. Aber ich habe wirklich erwartet, dass dies auf O funktioniert (n)

Ich denke, irgendwo in meiner Berechnung der Komplexität stimmt etwas nicht. Bitte korrigieren Sie dies, wenn ja.


0

Die optimale Lösung besteht darin, an der oberen linken Ecke zu beginnen, die nur einen minimalen Wert hat. Bewegen Sie sich diagonal nach rechts unten, bis Sie auf ein Element treffen, dessen Wert> = Wert des angegebenen Elements ist. Wenn der Wert des Elements dem des angegebenen Elements entspricht, wird return als true gefunden.

Ansonsten können wir von hier aus auf zwei Arten vorgehen.

Strategie 1:

  1. Bewegen Sie sich in der Spalte nach oben und suchen Sie nach dem angegebenen Element, bis wir das Ende erreichen. Wenn gefunden, wird return als true zurückgegeben
  2. Bewegen Sie sich in der Reihe nach links und suchen Sie nach dem angegebenen Element, bis wir das Ende erreichen. Wenn gefunden, wird return als true zurückgegeben
  3. Rückgabe als falsch gefunden

Strategie 2: Ich bezeichne den Zeilenindex und j den Spaltenindex des diagonalen Elements, bei dem wir angehalten haben. (Hier haben wir i = j, BTW). Sei k = 1.

  • Wiederholen Sie die folgenden Schritte, bis ik> = 0 ist
    1. Suchen Sie, ob a [ik] [j] dem angegebenen Element entspricht. Wenn ja, wird als wahr zurückgegeben.
    2. Suchen Sie, ob a [i] [jk] dem angegebenen Element entspricht. Wenn ja, wird als wahr zurückgegeben.
    3. Inkrement 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

Ich schlage vor, alle Zeichen in einem zu speichern 2D list. Suchen Sie dann den Index des erforderlichen Elements, falls es in der Liste vorhanden ist.

Wenn nicht vorhanden, drucken Sie die entsprechende Nachricht. Andernfalls drucken Sie Zeile und Spalte wie folgt:

row = (index/total_columns) und column = (index%total_columns -1)

Dies führt nur zur binären Suchzeit in einer Liste.

Bitte schlagen Sie Korrekturen vor. :) :)


0

Wenn O (M log (N)) - Lösung für ein MxN-Array in Ordnung ist -

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;
}

Funktionierende C ++ - Demo.

Bitte lassen Sie mich wissen, ob dies nicht funktionieren würde oder ob es einen Fehler gibt.


0

Ich habe diese Frage fast ein Jahrzehnt lang in Interviews gestellt und ich denke, es gab nur eine Person, die in der Lage war, einen optimalen Algorithmus zu entwickeln.

Meine Lösung war schon immer:

  1. Binäre Suche in der mittleren Diagonale, dh der Diagonale, die nach unten und rechts verläuft und das Element bei enthält (rows.count/2, columns.count/2).

  2. Wenn die Zielnummer gefunden wird, geben Sie true zurück.

  3. Andernfalls wurden zwei Zahlen ( uund v) gefunden, udie kleiner als das Ziel, vgrößer als das Ziel und veine rechts und eine niedriger als das Ziel sind u.

  4. Durchsuchen Sie rekursiv die Untermatrix rechts uund oben vund die Untermatrix unten uund links von v.

Ich glaube, dies ist eine strikte Verbesserung gegenüber dem von Nate hier angegebenen Algorithmus , da das Durchsuchen der Diagonale häufig eine Reduzierung des Suchraums um mehr als die Hälfte ermöglicht (wenn die Matrix nahe am Quadrat liegt), während das Durchsuchen einer Zeile oder Spalte immer zu einer Eliminierung führt von genau der Hälfte.

Hier ist der Code in (wahrscheinlich nicht besonders schnell) 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

Gegeben ist eine quadratische Matrix wie folgt:

[abc]
[def]
[ijk]

Wir wissen, dass a <c, d <f, i <k. Was wir nicht wissen, ist, ob dc oder dc usw. Wir haben Garantien nur in 1-Dimension.

Wenn wir uns die Endelemente (c, f, k) ansehen, können wir eine Art Filter erstellen: Ist N <c? search (): next (). Wir haben also n Iterationen über die Zeilen, wobei jede Zeile entweder O (log (n)) für die binäre Suche oder O (1) nimmt, wenn sie herausgefiltert wird.

Lassen Sie mich ein BEISPIEL geben, in dem N = j,

1) Überprüfen Sie Zeile 1. j <c? (nein, geh weiter)

2) Überprüfen Sie Zeile 2. j <f? (Ja, Bin Suche bekommt nichts)

3) Überprüfen Sie Zeile 3. j <k? (Ja, bin search findet es)

Versuchen Sie es erneut mit N = q,

1) Überprüfen Sie Zeile 1. q <c? (nein, geh weiter)

2) Überprüfen Sie Zeile 2. q <f? (nein, geh weiter)

3) Überprüfen Sie Zeile 3. q <k? (nein, geh weiter)

Es gibt wahrscheinlich eine bessere Lösung, aber das ist leicht zu erklären .. :)


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.