Wir lösen eine Aufgabe aus einem Google-Interview in JavaScript: 4 verschiedene AnsÀtze

Wir lösen eine Aufgabe aus einem Google-Interview in JavaScript: 4 verschiedene AnsÀtze

Als ich mich mit der Leistungsbewertung von Algorithmen beschĂ€ftigte, stieß ich auf dieses Video mit einem Mock-Interview von Google. Es bietet nicht nur einen Einblick in die Interviews großer Technologiekonzerne, sondern zeigt auch, wie algorithmische Probleme auf möglichst effiziente Weise gelöst werden.

Dieser Artikel ist eine Art Begleitmaterial zum Video. Ich gebe Kommentare zu allen gezeigten Lösungen ab und prĂ€sentiere auch meine eigene Version der Lösung in JavaScript. Außerdem werden die Besonderheiten jedes Algorithmus diskutiert.

Wir erinnern daran: alle Leser von „Habr“ erhalten einen Rabatt von 10.000 Rubel bei der Anmeldung zu einem beliebigen Kurs von Skillbox mit dem Aktionscode „Habr“.

Skillbox empfiehlt: Praktischer Kurs „Mobile Entwickler PRO“.

Problemstellung

Uns wird ein sortiertes Array und ein bestimmter Wert gegeben. Dann sollen wir eine Funktion erstellen, die true oder false zurĂŒckgibt, je nachdem, ob die Summe von zwei Zahlen aus dem Array dem vorgegebenen Wert entspricht.

Anders ausgedrĂŒckt: Gibt es zwei ganze Zahlen x und y im Array, deren Summe dem angegebenen Wert entspricht?

Beispiel A

Wenn uns das Array [1, 2, 4, 9] und der Wert 8 gegeben wurden, gibt die Funktion false zurĂŒck, da keine zwei Zahlen aus dem Array zusammen 8 ergeben können.

Beispiel B

Wenn es jedoch das Array [1, 2, 4, 4] und den Wert 8 gibt, sollte die Funktion true zurĂŒckgeben, weil 4 + 4 = 8.

Lösung 1. Brute Force

ZeitkomplexitĂ€t: O(NÂČ).
RaumkomplexitÀt: O(1).

Die offensichtlichste Bedeutung ist die Verwendung eines Paares von verschachtelten Schleifen.

const findSum = (arr, val) => {
  for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length; j++) {
      if (i !== j && arr[i] + arr[j] === val) {
        return true;
      };
    };
  };
  return false;
};

Diese Lösung kann nicht als effizient bezeichnet werden, da sie jede mögliche Summe von zwei Elementen im Array ĂŒberprĂŒft und jedes Indexpaar zweimal vergleicht. (Zum Beispiel, wenn i = 1 und j = 2 — das ist tatsĂ€chlich dasselbe wie der Vergleich von i = 2 und j = 1, aber diese Lösung versucht beide Varianten).

Da unsere Lösung ein Paar verschachtelter Schleifen verwendet, ist sie quadratisch mit einer zeitlichen KomplexitĂ€t von O(NÂČ).


Lösung 2. BinÀre Suche

ZeitkomplexitÀt: O(Nlog(N)).
RaumkomplexitÀt: O(1)
.

Da die Arrays sortiert sind, können wir nach einer Lösung mit binĂ€rer Suche suchen. Dies ist der effizienteste Algorithmus fĂŒr sortierte Arrays. An sich hat die binĂ€re Suche eine Laufzeit von O(log(N)). Dennoch muss ein for-Loop verwendet werden, um jedes Element mit allen anderen Werten zu ĂŒberprĂŒfen.

So könnte die Lösung aussehen. Um alles klar zu machen, verwenden wir eine separate Funktion zur Steuerung der binĂ€ren Suche. Außerdem gibt es die Funktion removeIndex(), die eine Version des Arrays ohne den angegebenen Index zurĂŒckgibt.

const findSum = (arr, val) => {
  for (let i = 0; i  {
  return arr.slice(0, i).concat(arr.slice(i + 1, arr.length));
};
 
const binarySearch = (arr, val) => {
  let start = 0;
  let end = arr.length - 1;
  let pivot = Math.floor(arr.length / 2);
  while (start < end) {
    if (val  arr[pivot]) {
      start = pivot + 1;
    };
    pivot = Math.floor((start + end) / 2);
    if (arr[pivot] === val) {
      return true;
    }
  };
  return false;
};

Der Algorithmus beginnt mit dem Index [0]. Dann erstellt er eine Version des Arrays, in der der erste Index ausgeschlossen ist, und verwendet die binĂ€re Suche, um zu ĂŒberprĂŒfen, ob eines der verbleibenden Werte dem Array hinzugefĂŒgt werden kann, um die gewĂŒnschte Summe zu erreichen. Diese Aktion wird einmal fĂŒr jedes Element im Array durchgefĂŒhrt.

Der for-Zyklus hat eine lineare zeitliche KomplexitĂ€t von O(N), aber innerhalb des for-Zyklus fĂŒhren wir eine binĂ€re Suche durch, was zu einer GesamtzeitkomplexitĂ€t von O(N log(N)) fĂŒhrt. Diese Lösung ist besser als die vorherige, aber es gibt noch Verbesserungsmöglichkeiten.


Lösung 3. Lineare Zeit

ZeitkomplexitÀt: O(N).
RaumkomplexitÀt: O(1).

Jetzt werden wir das Problem lösen, wobei wir daran denken, dass das Array sortiert ist. Die Lösung besteht darin, zwei Zahlen zu nehmen: eine am Anfang und eine am Ende. Wenn das Ergebnis von dem Geforderten abweicht, Àndern wir den Anfangs- und Endpunkt.

Am Ende werden wir entweder den gewĂŒnschten Wert treffen und true zurĂŒckgeben, oder der Anfangs- und Endpunkt werden sich treffen und false zurĂŒckgegeben.

const findSum = (arr, val) => {
  let start = 0;
  let end = arr.length - 1;
  while (start  val) {
      end -= 1;
    } else if (sum < val) {
      start += 1;
    } else {
      return true;
    };
  };
  return false;
};


Jetzt ist alles in Ordnung, die Lösung scheint optimal zu sein. Aber wer garantiert, dass das Array sortiert war?

Was dann?

Auf den ersten Blick könnten wir zuerst einfach das Array sortieren und dann die obige Lösung verwenden. Aber wie wĂŒrde sich das auf die AusfĂŒhrungszeit auswirken?

Der beste Algorithmus ist die schnelle Sortierung mit einer zeitlichen KomplexitÀt von O (N log (N)). Wenn wir ihn in unserer optimalen Lösung verwenden, wird sich die Leistung von O (N) auf O (N log (N)) Àndern. Ist es möglich, eine lineare Lösung mit einem unsortierten Array zu finden?

Lösung 4

ZeitkomplexitÀt: O(N).
RÀumliche KomplexitÀt: O(N).

Ja, eine lineare Lösung existiert. Dazu muss ein neues Array erstellt werden, das eine Liste der Übereinstimmungen enthĂ€lt, die wir suchen. Der Kompromiss besteht hier im höheren Speicherverbrauch: Dies ist die einzige Lösung im Artikel mit einer rĂ€umlichen KomplexitĂ€t, die O (1) ĂŒbersteigt.

Wenn der erste Wert dieses Arrays 1 ist und der gesuchte Wert 8 ist, können wir den Wert 7 in das Array „Suchwerte“ hinzufĂŒgen.

Dann können wir, indem wir jedes Element des Arrays verarbeiten, das Array „Suchwerte“ ĂŒberprĂŒfen und sehen, ob eines davon unserem Wert entspricht. Wenn ja, geben wir true zurĂŒck.

const findSum = (arr, val) => {
  let searchValues = [val - arr[0]];
  for (let i = 1; i < arr.length; i++) {
    let searchVal = val - arr[i];
    if (searchValues.includes(arr[i])) {
      return true;
    } else {
      searchValues.push(searchVal);
    }
  };
  return false;
};

Die Grundlage der Lösung ist eine for-Schleife, die, wie oben gesehen, eine lineare zeitliche KomplexitÀt von O (N) hat.

Der zweite iterative Teil unserer Funktion ist Array.prototype.includes(), eine JavaScript-Methode, die true oder false zurĂŒckgibt, abhĂ€ngig davon, ob das Array einen bestimmten Wert enthĂ€lt.

Um die zeitliche KomplexitÀt von Array.prototype.includes() zu ermitteln, können wir den von MDN bereitgestellten Polyfill (geschrieben in JavaScript) betrachten oder die Methode im Quellcode einer JavaScript-Engine wie Google V8 (C++) verwenden.

// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
  Object.defineProperty(Array.prototype, 'includes', {
    value: function(valueToFind, fromIndex) {
 
      if (this == null) {
        throw new TypeError('"this" is null or not defined');
      }
 
      // 1. Let O be ? ToObject(this value).
      var o = Object(this);
 
      // 2. Let len be ? ToLength(? Get(O, "length")).
      var len = o.length >>> 0;
 
      // 3. If len is 0, return false.
      if (len === 0) {
        return false;
      }
 
      // 4. Let n be ? ToInteger(fromIndex).
      //    (If fromIndex is undefined, this step produces the value 0.)
      var n = fromIndex | 0;
 
      // 5. If n ≄ 0, then
      //  a. Let k be n.
      // 6. Else n < 0,
      //  a. Let k be len + n.
      //  b. If k < 0, let k be 0.
      var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
 
      function sameValueZero(x, y) {
        return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
      }
 
      // 7. Repeat, while k < len
      while (k < len) {
        // a. Let elementK be the result of ? Get(O, ! ToString(k)).
        // b. If SameValueZero(valueToFind, elementK) is true, return true.
        if (sameValueZero(o[k], valueToFind)) {
          return true;
        }
        // c. Increase k by 1.
        k++;
      }
 
      // 8. Return false
      return false;
    }
  });
}

Der iterative Teil von Array.prototype.includes() ist eine while-Schleife in Schritt 7, die (fast) die gesamte LĂ€nge des gegebenen Arrays durchlĂ€uft. Das bedeutet, dass seine zeitliche KomplexitĂ€t ebenfalls linear ist. Da sie immer einen Schritt hinter unserem Hauptarray zurĂŒckliegt, betrĂ€gt die zeitliche KomplexitĂ€t O(N + (N - 1)). Durch die Verwendung der Big-O-Notation vereinfacht sich dies zu O(N) – da N den grĂ¶ĂŸten Einfluss bei wachsender EingangsgrĂ¶ĂŸe hat.

Was die SpeicherkomplexitĂ€t betrifft, so ist ein zusĂ€tzliches Array erforderlich, dessen LĂ€nge das ursprĂŒngliche Array widerspiegelt (minus eins, ja, aber das kann ignoriert werden), was zu einer rĂ€umlichen KomplexitĂ€t von O(N) fĂŒhrt. Ein erhöhter Speicherbedarf gewĂ€hrleistet dabei die maximale Effizienz des Algorithmus.


Ich hoffe, der Artikel ist fĂŒr Sie als ErgĂ€nzung zum Video-Interview hilfreich. Er zeigt, dass eine einfache Aufgabe auf verschiedene Arten mit unterschiedlichen Ressourcen (Zeit, Speicher) gelöst werden kann.

Skillbox empfiehlt:

Quelle: habr.com

Erwerben Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster