Wykonujemy proste ćwiczenie z dwiema tablicami zawierającymi liczby oraz ich kwadraty, w dowolnej kolejności. Poznajemy trzy sposoby na sprawdzenie, czy te tablice są prawidłowe tj. jedna pasuje do drugiej.
Jeden sposób jest prosty, ale wymaga sortowania, drugi bardziej zawiły i wymaga znajomości metod tablicowych, zaś trzeci to popularne podejście „frequency counter”, którego znajomość opłaci nam się w przyszłości.
W zasadzie trudno o opisowy tytuł dla tego ćwiczenia. Piszemy funkcję, która pobiera dwie tablice. Chodzi o to, że w jednej tablicy znajdują się, w dowolnej kolejności, pewne liczby. W drugiej tablicy znajdują się, w dowolnej kolejności, kwadraty tych liczb.
Poniższy przykład ilustruje co chcemy osiągnąć:
function compareArrays(arr1, arr2) {
return;
}
console.log(compareArrays([1,2,3], [1,4,9]));
//ma zwracać true
W takim przypadku mamy dostać wartość prawdziwą. Pozwalamy jednak użytkownikowi, aby te wartości były w dowolnej kolejności. Poniższy przykład również ma zwrócić true:
console.log(compareArrays([3,2,1], [4,9,1]));
//ma zwracać true
Mało tego – pozwalamy na dowolną ilość liczb. Poniższy przykład również ma zwrócić true:
console.log(compareArrays([3,3,2,1], [4,9,1,9]));
//ma zwracać true
Mamy liczbę 3 dwa razy w pierwszej tablicy, zaś w drugiej liczbę 9 (jej kwadrat) też dwa razy. To jest istota tego ćwiczenia.
Podejście pierwsze – posortuj
Bardzo łatwe podejście do problemu, polegające na posortowaniu obu tablic.
function compareArrays(arr1, arr2) {
arr1.sort((a,b)=>a-b);
arr2.sort((a,b)=>a-b);
console.log(arr1);
console.log(arr2);
}
compareArrays([3,3,2,1], [4,9,1,9]);
//[1, 2, 3, 3]
//[1, 4, 9, 9]
Jak widać problem mamy już prawie rozwiązany. Teraz musimy przejść po tablicach, po ich indeksach, i sprawdzić, czy element o danym indeksie w jednej tablicy to kwadrat elementu o tym samym indeksie w drugim.
Do przechodzenia po indeksach tablic mamy pętle for…in (łatwo zapamiętać – in jak INdeks…):
function compareArrays(arr1, arr2) {
arr1.sort((a,b)=>a-b);
arr2.sort((a,b)=>a-b);
for(let idx in arr1) {
if(arr2[idx] !== arr1[idx] ** 2)
return false;
return true;
}
}
console.log(compareArrays([3,3,2,1], [4,9,1,9]));
//true
Czyli tak – przechodzimy po indeksach arr1 (albo arr2, bez różnicy) i sprawdzamy czy w arr2 pod danym indeksem jest element z arr1 pod takim samym indeksem do kwadratu.
Jeżeli nie ma – false. Jeżeli pętla się skończy – true.
Brakuje tylko sprawdzenia, czy tablice są jednakowej długości. Bo jeżeli są różne, to już na początku wiemy, że trzeba zwrócić false:
function compareArrays(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
arr1.sort((a,b)=>a-b);
arr2.sort((a,b)=>a-b);
for(let idx in arr1) {
if(arr2[idx] !== arr1[idx] ** 2)
return false;
return true;
}
}
console.log(compareArrays([1,2,3], [1,4,9]));
//true
console.log(compareArrays([1,2], [1,4,9]));
//false
Gotowe. Możemy to sobie na spokojnie przeanalizować albo przejść do bardziej zaawansowanych przykładów.
Podejście naiwne z pomocą metod tablicowych
To podejście jest z jednej strony „naiwne” (proste), z drugiej wymaga od nas pewnej znajomości metod tablicowych.
Na samym początku odrzucamy tablice o różnej długości:
function compareArrays(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
}
Teraz przechodzimy po tablicy 1 w pętli for. Kod poniżej to tylko demonstracja, bo robimy to krok po kroku:
function compareArrays(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
for(let i = 0; i < arr1.length; i++) {
let foundIdx = arr2.indexOf(arr1[i] ** 2);
}
}
Czyli bierzemy element o danym indeksie z arr1, podnosimy go (wartość pod tym indeksem) do kwadratu i za pomocą funkcji indexOf szukamy tego indeksu w tablicy arr2.
Metoda indexOf może zwrócić -1, jeżeli w drugiej tablicy nie znajdzie elementu (z tablicy 1 podniesionego do kwadratu):
function compareArrays(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
for(let i = 0; i < arr1.length; i++) {
let foundIdx = arr2.indexOf(arr1[i] ** 2);
if(foundIdx === -1)
return false;
}
}
Wtedy wiemy, że skoro w tablicy 2 nie ma elementu z tablicy 1 podniesionego do kwadratu, to mamy false.
Teraz tylko usuwamy element o danym indeksie z tablicy 2, jak już został „wykorzystany”:
function compareArrays(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
for(let i = 0; i < arr1.length; i++) {
let foundIdx = arr2.indexOf(arr1[i] ** 2);
if(foundIdx === -1)
return false;
arr2.splice(foundIdx, 1);
}
return true;
}
console.log(compareArrays([1,2,3], [1,4,9]));
//true
console.log(compareArrays([1,2,4], [1,4,9]));
//false
Możemy to sobie przeanalizować na spokojnie. Oto nasz algorytm:
- Jeżeli różna długość tablic na początku – fałsz
- Przechodzimy w pętli for po indeksach tablicy 1
- Wartość elementu z tablicy 1 podnosimy do kwadratu i sprawdzamy pod jakim indeksem znajduje się w tablicy 2
- Jeżeli się nie znajduje (indexOf zwraca -1) to zwracamy fałsz
- Po upewnieniu się, że element o wartości podniesionej do kwadratu się w drugiej tablicy znajduje i zapisaniu jego indeksu usuwamy ten element z drugiej tablicy, już jest „wykorzystany”
- Na koniec – zwracamy prawdę.
Uwaga – to podejście jest dużo gorsze, niż podejście pierwsze. Nie dość, że wymaga dobrej znajomości wbudowanych, czasami osobliwych funkcji tablicowych (jak splice) i jest mało czytelne, to jeszcze jest mało wydajne.
Operacje zmiany w tablicach są kosztowne. W przykładzie pierwszym dokonujemy dwóch takich zmian (posortowanie tablicy 1 i 2). W przykładzie drugim dokonujemy zmiany tablicy (usunięcia elementu arr2) wewnątrz pętli, co z punktu widzenia złożoności algorytmu jest jeszcze gorsze.
Frequency counter – sposób najlepszy
Chyba najlepsze podejście. Zacznijmy od tego, że chcemy napisać funkcję, która podlicza ilość wystąpień elementów w naszych tablicach:
function compareArrays(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
let counter1 = {};
let counter2 = {};
for(let val of arr1)
counter1[val] = (counter1[val] || 0) + 1;
for(let val of arr2)
counter2[val] = (counter2[val] || 0) + 1;
console.log(counter1);
console.log(counter2);
}
compareArrays([1,2,3], [1,4,9]);
//{1: 1, 2: 1, 3: 1}
//{1: 1, 4: 1, 9: 1}
Tworzymy dwa obiekty, będące licznikami ile razy dana wartość występuje w obu tablicach.
Teraz musimy:
- Przejść po kluczach licznika pierwszego
- Sprawdzić czy w liczniku drugim jest klucz, który jest podniesieniem do kwadratu tego z pierwszego, np. klucz w pierwszym 2, sprawdź czy drugi licznik ma klucz 4
- Sprawdzić, czy klucz z licznika pierwszego i jego odpowiednik podniesiony do kwadratu mają taką samą wartość (czyli 2:1 w pierwszym liczniku odpowiada 4:1 w drugim).
Wygląda to tak:
function compareArrays(arr1, arr2) {
if(arr1.length !== arr2.length)
return false;
let counter1 = {};
let counter2 = {};
for(let val of arr1)
counter1[val] = (counter1[val] || 0) + 1;
for(let val of arr2)
counter2[val] = (counter2[val] || 0) + 1;
for(let key in counter1) {
if(!(key ** 2 in counter2))
return false;
if(counter2[key**2] !== counter1[key])
return false;
}
return true;
}
console.log(compareArrays([1,2,3], [1,4,9]));
//true
Możemy to sobie na spokojnie przeanalizować. Nie jest to tak prosty algorytm jak przykład 1, ale zdecydowanie łatwiejszy (moim zdaniem) niż przykład 2.
W dodatku optymalny. Nie modyfikujemy tablic, pętle są, ale nie są zagnieżdżone (w przypadku złożoności algorytmów pętla po pętli to jak jedna pętla, zaś pętla w pętli – cała złożoność podniesiona do kwadratu).
Myślę, że tyle sposobów wystarczy.