Wir lösen eine Google-Interviewaufgabe in JavaScript: 4 verschiedene AnsÀtze

Wir lösen eine Google-Interviewaufgabe in JavaScript: 4 verschiedene AnsÀtze

Als ich mich mit der LeistungsfĂ€higkeit von Algorithmen beschĂ€ftigte, stieß ich auf dieses Video mit einem Mock-Interview von Google. Es vermittelt nicht nur einen Eindruck davon, wie Interviews in großen Technologiekonzernen ablaufen, sondern hilft auch zu verstehen, wie algorithmische Probleme effizient gelöst werden können.

Dieser Artikel ist eine Art Begleittext zu dem Video. Darin gebe ich Kommentare zu allen gezeigten Lösungen sowie meine eigene Version der Lösung in JavaScript. Außerdem werden die Feinheiten 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 „Mobiler Entwickler PRO“.

Aufgabenstellung

Wir erhalten ein sortiertes Array und einen bestimmten Wert. Dann werden wir gebeten, eine Funktion zu erstellen, die true oder false zurĂŒckgibt, abhĂ€ngig davon, ob die Summe zweier beliebiger Zahlen aus dem Array dem angegebenen Wert entsprechen kann.

Mit anderen Worten: Gibt es im Array zwei ganze Zahlen x und y, deren Summe dem angegebenen Wert entspricht?

Beispiel A

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

Beispiel B

Aber wenn es sich um das Array [1, 2, 4, 4] und den Wert 8 handelt, sollte die Funktion true zurĂŒckgeben, da 4 + 4 = 8.

Lösung 1. Brute Force

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

Der offensichtlichste Ansatz ist die Verwendung von zwei 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 betrachtet werden, da sie jede mögliche Summe zweier Elemente im Array ĂŒberprĂŒft und außerdem jedes Indexpaar zweimal vergleicht. (Zum Beispiel, wenn i = 1 und j = 2, ist das tatsĂ€chlich dasselbe wie i = 2 und j = 1, aber diese Lösung probiert beide Varianten aus).

Da unsere Lösung ein Paar von verschachtelten for-Schleifen verwendet, ist sie quadratisch und hat eine ZeitkomplexitĂ€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)). Es ist jedoch dennoch erforderlich, eine for-Schleife zu verwenden, um jedes Element mit allen anderen Werten zu ĂŒberprĂŒfen.

So könnte eine Lösung aussehen. Um alles klar zu machen, verwenden wir eine separate Funktion zur Kontrolle der binĂ€ren Suche. Außerdem 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, die den ersten Index ausschließt, und verwendet die binĂ€re Suche, um zu ĂŒberprĂŒfen, ob eines der verbleibenden Werte zu dem Array hinzugefĂŒgt werden kann, um die gewĂŒnschte Summe zu erreichen. Diese Aktion wird einmal fĂŒr jedes Element im Array ausgefĂŒhrt.

An sich hat die for-Schleife eine lineare ZeitkomplexitĂ€t von O (N), aber innerhalb der for-Schleife fĂŒhren wir eine binĂ€re Suche durch, was zu einer GesamtzeitkomplexitĂ€t von O (Nlog (N)) fĂŒhrt. Diese Lösung ist besser als die vorherige, aber es gibt noch Raum fĂŒr Verbesserungen.


Lösung 3. Lineare Zeit

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

Jetzt werden wir das Problem lösen, wĂ€hrend wir uns daran erinnern, dass das Array sortiert ist. Die Lösung besteht darin, zwei Zahlen zu nehmen: eine am Anfang und eine am Ende. Wenn das Ergebnis anders als gewĂŒnscht ist, Ă€ndern wir den Anfangs- und Endpunkt.

Letztendlich werden wir entweder den gewĂŒnschten Wert treffen und true zurĂŒckgeben, oder die Anfangs- und Endpunkte 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 gut, die Lösung scheint optimal zu sein. Aber wer garantiert, dass das Array sortiert war?

Was dann?

Auf den ersten Blick könnten wir das Array zunĂ€chst einfach sortieren und dann die obige Lösung verwenden. Aber wie wirkt sich das auf die AusfĂŒhrungszeit aus?

Der beste Algorithmus ist die schnelle Sortierung mit einer ZeitkomplexitÀt von O (Nlog (N)). Wenn wir ihn in unserer optimalen Lösung verwenden, verÀndert sich die Leistung von O (N) auf O (Nlog (N)). Kann man eine lineare Lösung mit einem unsortierten Array finden?

Lösung 4

ZeitkomplexitÀt: O(N).
PlatzkomplexitÀt: O(N).

Ja, es gibt eine lineare Lösung, dafĂŒr muss ein neues Array erstellt werden, das die Liste der Übereinstimmungen enthĂ€lt, nach denen wir suchen. Der Kompromiss hier ist eine aktivere Nutzung des Speichers: dies ist die einzige Lösung im Artikel mit einer SpeicherkomplexitĂ€t, die O (1) ĂŒbersteigt.

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

Dann können wir, indem wir jedes Element des Arrays bearbeiten, 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 wir oben gesehen haben, eine lineare zeitliche KomplexitÀt von O (N) hat.

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

Um die zeitliche KomplexitÀt von Array.prototype.includes () zu ermitteln, können wir den Polyfill betrachten, der von MDN bereitgestellt wird (und in JavaScript geschrieben ist), oder wir können die Methode im Quellcode der 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;
    }
  });
}

Hier ist der iterative Teil von Array.prototype.include () 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 liegt, ist die zeitliche KomplexitĂ€t O (N + (N - 1)). Durch die Verwendung der Big O Notation vereinfacht sich dies auf O (N) — denn N hat den grĂ¶ĂŸten Einfluss bei wachsender EingangsgrĂ¶ĂŸe.

Was die SpeicherkomplexitĂ€t betrifft, so ist ein zusĂ€tzliches Array erforderlich, dessen LĂ€nge dem ursprĂŒnglichen Array entspricht (minus eins, ja, aber das kann ignoriert werden), was zu einer SpeicherkomplexitĂ€t von O (N) fĂŒhrt. Der erhöhte Speicherverbrauch sorgt jedoch fĂŒr maximale Effizienz des Algorithmus.


Ich hoffe, dieser Artikel erweist sich als nĂŒtzlich fĂŒr Sie als ErgĂ€nzung zum Video-Interview. Er zeigt, dass eine einfache Aufgabe auf verschiedene Arten mit unterschiedlichen Mengen an genutzten Ressourcen (Zeit, Speicher) gelöst werden kann.

Skillbox empfiehlt:

Quelle: habr.com

60GB SSD 8Gb DDR4