Problēmas atrisināŔana no Google intervijas JavaScript: 4 dažādi veidi

Problēmas atrisināŔana no Google intervijas JavaScript: 4 dažādi veidi

Kad es pētÄ«ju algoritmu veiktspēju, es saskāros ar to Å is ir Google izspēles intervijas video.. Tas ne tikai sniedz priekÅ”statu par to, kā notiek intervijas lielajās tehnoloÄ£iju korporācijās, bet arÄ« ļauj saprast, kā pēc iespējas efektÄ«vāk tiek risinātas algoritmiskās problēmas.

Šis raksts ir sava veida video pavadījums. Tajā es sniedzu komentārus par visiem parādītajiem risinājumiem, kā arī savu risinājuma versiju JavaScript. Tiek apspriestas arī katra algoritma nianses.

Atgādinām: visiem "Habr" lasītājiem - atlaide 10 000 rubļu, reģistrējoties jebkurā Skillbox kursā, izmantojot "Habr" reklāmas kodu.

Skillbox iesaka: Praktiskais kurss "Mobile Developer PRO".

Problēmas paziņojums

Mums tiek dots sakārtots masīvs un noteikta vērtība. Pēc tam tiek lūgts izveidot funkciju, kas atgriež patiesu vai nepatiesu atkarībā no tā, vai jebkuru divu skaitļu summa masīvā var būt vienāda ar doto vērtību.

Citiem vārdiem sakot, vai masīvā ir divi veseli skaitļi, x un y, kas, saskaitot kopā, ir vienādi ar norādīto vērtību?

Piemērs A

Ja mums tiek dots masīvs [1, 2, 4, 9] un vērtība ir 8, funkcija atgriezīs nepatiesu, jo neviens masīvs nevar pievienot 8.

B piemērs

Bet, ja tas ir masīvs [1, 2, 4, 4] un vērtība ir 8, funkcijai ir jāatgriež vērtība True, jo 4 + 4 = 8.

1. risinājums: brutāls spēks

Laika sarežģītÄ«ba: O(NĀ²).
Telpas sarežģītība: O(1).

Acīmredzamākā nozīme ir izmantot ligzdotu cilpu pāri.

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

Å is risinājums nav efektÄ«vs, jo pārbauda katru iespējamo divu elementu summu masÄ«vā un arÄ« salÄ«dzina katru indeksu pāri divas reizes. (Piemēram, ja i = 1 un j = 2 patiesÄ«bā ir tas pats, kas salÄ«dzināt ar i = 2 un j = 1, bet Å”ajā risinājumā mēs izmēģinām abas iespējas).

Tā kā mÅ«su risinājumā tiek izmantots pāris ligzdotas cilpas, tas ir kvadrātveida ar O(NĀ²) laika sarežģītÄ«bu.


2. risinājums: binārā meklÄ“Å”ana

Laika sarežģītība: O(Nlog(N)).
Telpas sarežģītība: O(1)
.

Tā kā masÄ«vi ir sakārtoti, mēs varam meklēt risinājumu, izmantojot bināro meklÄ“Å”anu. Å is ir visefektÄ«vākais algoritms sakārtotiem masÄ«viem. Binārās meklÄ“Å”anas darbÄ«bas laiks ir O(log(N)). Tomēr jums joprojām ir jāizmanto for cilpa, lai pārbaudÄ«tu katru elementu pret visām pārējām vērtÄ«bām.

LÅ«k, kā varētu izskatÄ«ties risinājums. Lai lietas bÅ«tu skaidras, mēs izmantojam atseviŔķu funkciju, lai kontrolētu bināro meklÄ“Å”anu. Un arÄ« funkcija removeIndex(), kas atgriež masÄ«va versiju mÄ«nus doto indeksu.

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

Algoritms sākas ar indeksu [0]. Pēc tam tiek izveidota masÄ«va versija, izņemot pirmo indeksu, un tiek izmantota binārā meklÄ“Å”ana, lai noskaidrotu, vai masÄ«vam var pievienot kādu no atlikuÅ”ajām vērtÄ«bām, lai iegÅ«tu vēlamo summu. Å Ä« darbÄ«ba tiek veikta vienu reizi katram masÄ«va elementam.

PaÅ”ai for cilpai bÅ«s lineārā laika sarežģītÄ«ba O(N), bet for cilpas iekÅ”pusē mēs veicam bināro meklÄ“Å”anu, kas nodroÅ”ina kopējo laika sarežģītÄ«bu O(Nlog(N)). Å is risinājums ir labāks par iepriekŔējo, taču vēl ir ko uzlabot.


3. risinājums: lineārais laiks

Laika sarežģītība: O(N).
Telpas sarežģītība: O(1).

Tagad mēs atrisināsim problēmu, atceroties, ka masÄ«vs ir sakārtots. Risinājums ir ņemt divus skaitļus: vienu sākumā un vienu beigās. Ja rezultāts atŔķiras no nepiecieÅ”amā, mainiet sākuma un beigu punktu.

Galu galā mēs vai nu sastapsim vēlamo vērtību un atgriezīsimies ar patiesu vērtību, vai arī sākuma un beigu punkti saplūdīs un atgriezīsies false.

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


Tagad viss ir kārtÄ«bā, risinājums Ŕķiet optimāls. Bet kurÅ” var garantēt, ka masÄ«vs tika pasÅ«tÄ«ts?

Ko tad?

No pirmā acu uzmetiena mēs varējām vienkārÅ”i pasÅ«tÄ«t masÄ«vu un pēc tam izmantot iepriekÅ” minēto risinājumu. Bet kā tas ietekmēs izpildes laiku?

Labākais algoritms ir ātrā kārtoÅ”ana ar laika sarežģītÄ«bu O (Nlog (N)). Ja mēs to izmantosim mÅ«su optimālajā risinājumā, tas mainÄ«s tā veiktspēju no O(N) uz O(Nlog(N)). Vai ir iespējams atrast lineāru risinājumu ar nesakārtotu masÄ«vu?

4 risinājums

Laika sarežģītība: O(N).
Telpas sarežģītība: O(N).

Jā, ir lineārs risinājums; lai to izdarÄ«tu, mums ir jāizveido jauns masÄ«vs, kurā ir ietverts meklēto atbilstÄ«bas saraksts. Kompromiss Å”eit ir lielāks atmiņas lietojums: tas ir vienÄ«gais risinājums dokumentā, kura telpas sarežģītÄ«ba ir lielāka par O(1).

Ja dotā masÄ«va pirmā vērtÄ«ba ir 1 un meklÄ“Å”anas vērtÄ«ba ir 8, mēs varam pievienot vērtÄ«bu 7 masÄ«vam "meklÄ“Å”anas vērtÄ«bas".

Pēc tam, apstrādājot katru masÄ«va elementu, mēs varam pārbaudÄ«t ā€œmeklÄ“Å”anas vērtÄ«buā€ masÄ«vu un noskaidrot, vai kāds no tiem ir vienāds ar mÅ«su vērtÄ«bu. Ja jā, atgrieziet patieso.

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

Risinājuma pamatā ir for cilpa, kurai, kā redzējām iepriekÅ”, ir O(N) lineārā laika sarežģītÄ«ba.

Otrā mūsu funkcijas iteratīvā daļa ir Array.prototype.include(), JavaScript metode, kas atgriezīs patiesu vai nepatiesu atkarībā no tā, vai masīvā ir norādītā vērtība.

Lai noskaidrotu Array.prototype.includes() laika sarežģītÄ«bu, mēs varam aplÅ«kot MDN nodroÅ”ināto polifill (un rakstÄ«ts JavaScript valodā) vai izmantot metodi JavaScript dzinēja avota kodā, piemēram, Google V8 (C++).

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

Å eit Array.prototype.include() iteratÄ«vā daļa ir while cilpa 7. darbÄ«bā, kas (gandrÄ«z) Ŕķērso visu dotā masÄ«va garumu. Tas nozÄ«mē, ka arÄ« tā laika sarežģītÄ«ba ir lineāra. Tā kā tas vienmēr ir vienu soli aiz mÅ«su galvenā masÄ«va, laika sarežģītÄ«ba ir O (N + (N - 1)). Izmantojot lielo O apzÄ«mējumu, mēs to vienkārÅ”ojam lÄ«dz O(N), jo tieÅ”i N ir vislielākā ietekme, palielinot ievades lielumu.

AttiecÄ«bā uz telpisko sarežģītÄ«bu ir nepiecieÅ”ams papildu masÄ«vs, kura garums atspoguļo sākotnējo masÄ«vu (mÄ«nus viens, jā, bet to var ignorēt), kā rezultātā rodas O(N) telpiskā sarežģītÄ«ba. Palielināts atmiņas lietojums nodroÅ”ina maksimālu algoritma efektivitāti.


Ceru, ka raksts jums noderēs kā papildinājums video intervijai. Tas parāda, ka vienkārÅ”u problēmu var atrisināt vairākos dažādos veidos, izmantojot dažādus resursu (laika, atmiņas) apjomu.

Skillbox iesaka:

Avots: www.habr.com

Pievieno komentāru