Lösen einer Aufgabe aus einem Google-Interview mit JavaScript: 4 verschiedene Möglichkeiten



Als ich die Leistung von Algorithmen studierte, stieß ich auf dieses Video aus dem Scheininterview von Google . Es gibt nicht nur eine Vorstellung davon, wie Interviews in großen Technologieunternehmen durchgefĂŒhrt werden, sondern ermöglicht Ihnen auch zu verstehen, wie algorithmische Probleme am effizientesten gelöst werden.

Dieser Artikel ist eine Art Begleitung zum Video. Darin gebe ich Kommentare zu allen gezeigten Lösungen sowie zu meiner eigenen Version der Lösung in JavaScript. Die Nuancen jedes Algorithmus werden ebenfalls diskutiert.

Wir erinnern Sie daran: FĂŒr alle Leser von „Habr“ - ein Rabatt von 10.000 Rubel bei der Anmeldung fĂŒr einen Skillbox-Kurs mit dem Promo-Code „Habr“.

Skillbox empfiehlt: Praktikum "Mobile Developer PRO" .

ErklÀrung des Problems


Wir erhalten ein geordnetes Array und einen bestimmten Wert. Dann bitten sie darum, eine Funktion zu erstellen, die true oder false zurĂŒckgibt, je nachdem, ob die Summe von zwei beliebigen Zahlen aus dem Array dem angegebenen Wert entsprechen kann.

Mit anderen Worten, gibt es zwei Ganzzahlen x und y im Array, die beim HinzufĂŒgen dem angegebenen Wert entsprechen?

Beispiel A.

Wenn wir ein Array [1, 2, 4, 9] und einen Wert von 8 erhalten haben, gibt die Funktion false zurĂŒck, da keine zwei Zahlen aus dem Array insgesamt 8 ergeben können.

Beispiel B.

Wenn es sich jedoch um ein Array [1, 2, 4, 4] handelt und der Wert 8 ist, sollte die Funktion true zurĂŒckgeben, da 4 + 4 = 8 ist.

Lösung 1. Bruteforce

Zeitliche Schwierigkeit: O (NÂČ).
RÀumliche KomplexitÀt: O (1).

Die naheliegendste Bedeutung ist die Verwendung eines Paares verschachtelter 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 effektiv bezeichnet werden, da sie jede mögliche Summe von zwei Elementen im Array ĂŒberprĂŒft und jedes Indexpaar zweimal vergleicht. (Wenn zum Beispiel i = 1 und j = 2 ist, entspricht dies tatsĂ€chlich dem Vergleich mit i = 2 und j = 1, aber in dieser Lösung versuchen wir beide Optionen).

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


Lösung 2. BinÀre (binÀre) Suche

Zeitliche Schwierigkeit: O (Nlog (N)).
RÀumliche KomplexitÀt: O (1) .

Da Arrays geordnet sind, können wir mithilfe der binĂ€ren Suche nach einer Lösung suchen. Dies ist der effizienteste Algorithmus fĂŒr geordnete Arrays. Die binĂ€re Suche selbst hat eine O (log (N)) Laufzeit. Sie mĂŒssen jedoch weiterhin eine for-Schleife verwenden, um jedes Element mit allen anderen Werten zu vergleichen.

So könnte die Lösung aussehen. Um alles klar zu machen, verwenden wir eine separate Funktion, um die binĂ€re Suche zu steuern. Sowie die Funktion removeIndex (), die die Version des Arrays abzĂŒglich des angegebenen Index zurĂŒckgibt.

 const findSum = (arr, val) => { for (let i = 0; i < arr.length; i++){ if (binarySearch(removeIndex(arr, i), val - arr[i])) { return true; } }; return false; }; const removeIndex = (arr, 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]) { end = pivot - 1; } else if (val > arr[pivot]) { start = pivot + 1; }; pivot = Math.floor((start + end) / 2); if (arr[pivot] === val) { return true; } }; return false; }; 

Der Algorithmus beginnt bei Index [0]. Anschließend wird eine Version des Arrays mit Ausnahme des ersten Index erstellt und mithilfe einer binĂ€ren Suche ĂŒberprĂŒft, ob der verbleibende Wert zum Array hinzugefĂŒgt werden kann, um die gewĂŒnschte Menge zu erhalten. Diese Aktion wird einmal fĂŒr jedes Element im Array ausgefĂŒhrt.

Die for-Schleife selbst hat eine lineare ZeitkomplexitĂ€t von O (N), aber innerhalb der for-Schleife fĂŒhren wir eine binĂ€re Suche durch, die die GesamtzeitkomplexitĂ€t von O (Nlog (N)) angibt. Diese Lösung ist besser als die vorherige, aber es gibt noch etwas zu verbessern.


Lösung 3. Lineare Zeit

Zeitliche Schwierigkeit: O (N).
RÀumliche KomplexitÀt: O (1).

Jetzt werden wir das Problem lösen und uns daran erinnern, dass das Array sortiert ist. Die Lösung besteht darin, zwei Zahlen zu verwenden: eine am Anfang und eine am Ende. Wenn das Ergebnis vom gewĂŒnschten abweicht, Ă€ndern wir den Start- und Endpunkt.

Am Ende erfĂŒllen wir entweder den gewĂŒnschten Wert und geben true zurĂŒck, oder die Start- und Endpunkte konvergieren und geben false zurĂŒck.

 const findSum = (arr, val) => { let start = 0; let end = arr.length - 1; while (start < end) { let sum = arr[start] + arr[end]; if (sum > 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 bestellt wurde?

Was dann?


Auf den ersten Blick könnten wir zuerst das Array sortieren und dann die obige Lösung verwenden. Aber wie wirkt sich das auf die Laufzeit aus?

Der beste Algorithmus ist die schnelle Sortierung mit der ZeitkomplexitÀt O (Nlog (N)). Wenn wir es in unserer optimalen Lösung verwenden, Àndert es seine Leistung von O (N) in O (Nlog (N)). Ist es möglich, eine lineare Lösung mit einem ungeordneten Array zu finden?

Entscheidung 4

Zeitliche Schwierigkeit: O (N).
RÀumliche KomplexitÀt: O (N).

Ja, es gibt eine lineare Lösung. Dazu mĂŒssen Sie ein neues Array erstellen, das eine Liste der Übereinstimmungen enthĂ€lt, nach denen wir suchen. Der Kompromiss hierbei ist eine aktivere Nutzung des Speichers: Dies ist die einzige Lösung in dem Artikel mit einer rĂ€umlichen KomplexitĂ€t von mehr als O (1).

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

Wenn wir dann jedes Element des Arrays verarbeiten, können wir das Array der „Suchwerte“ ĂŒberprĂŒfen und feststellen, ob einer von ihnen unserem Wert entspricht. Wenn ja, geben Sie 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 Basis der Lösung ist die for-Schleife, die, wie wir oben gesehen haben, eine lineare ZeitkomplexitÀt O (N) aufweist.

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

Um die zeitliche KomplexitĂ€t von Array.prototype.includes () herauszufinden, können wir uns die von MDN bereitgestellte (und in JavaScript geschriebene) PolyfĂŒllung ansehen oder eine Methode im Quellcode einer JavaScript-Engine wie Google V8 (C ++) verwenden.

 // https://tc39.imtqy.com/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 () die while-Schleife in Schritt 7, die (fast) die gesamte LĂ€nge des angegebenen Arrays durchlĂ€uft. Dies bedeutet, dass seine zeitliche KomplexitĂ€t ebenfalls linear ist. Nun, da es immer einen Schritt hinter unserem Hauptarray liegt, ist die zeitliche KomplexitĂ€t O (N + (N - 1)). Mit der Big O-Notation vereinfachen wir sie auf O (N) - da N mit zunehmender EingabegrĂ¶ĂŸe den grĂ¶ĂŸten Einfluss hat.

FĂŒr die rĂ€umliche KomplexitĂ€t wird ein zusĂ€tzliches Array benötigt, dessen LĂ€nge das ursprĂŒngliche Array widerspiegelt (minus eins, ja, aber dies kann ignoriert werden), was zur rĂ€umlichen KomplexitĂ€t von O (N) fĂŒhrt. Eine erhöhte Speichernutzung sorgt fĂŒr maximale Effizienz des Algorithmus.


Ich hoffe, dieser Artikel ist fĂŒr Sie als Anhang zu einem Videointerview hilfreich. Es zeigt, dass ein einfaches Problem auf verschiedene Weise mit unterschiedlichen Ressourcen (Zeit, Speicher) gelöst werden kann.

Skillbox empfiehlt:

Source: https://habr.com/ru/post/de443886/


All Articles