Skuteczny sposób wstawiania liczby do posortowanej tablicy liczb?


143

Mam posortowaną tablicę JavaScript i chcę wstawić jeszcze jeden element do tablicy, tak aby wynikowa tablica pozostała posortowana. Z pewnością mógłbym zaimplementować prostą funkcję wstawiania w stylu quicksort:

var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
  array.splice(locationOf(element, array) + 1, 0, element);
  return array;
}

function locationOf(element, array, start, end) {
  start = start || 0;
  end = end || array.length;
  var pivot = parseInt(start + (end - start) / 2, 10);
  if (end-start <= 1 || array[pivot] === element) return pivot;
  if (array[pivot] < element) {
    return locationOf(element, array, pivot, end);
  } else {
    return locationOf(element, array, start, pivot);
  }
}

console.log(insert(element, array));

[OSTRZEŻENIE] ten kod ma błąd, gdy próba wstawienia na początek tablicy, np. insert(2, [3, 7 ,9]) Daje niepoprawne [3, 2, 7, 9].

Jednak zauważyłem, że implementacje funkcji Array.sort mogą potencjalnie zrobić to dla mnie i natywnie:

var array = [1,2,3,4,5,6,7,8,9];
var element = 3.5;
function insert(element, array) {
  array.push(element);
  array.sort(function(a, b) {
    return a - b;
  });
  return array;
}

console.log(insert(element, array));

Czy jest dobry powód, aby wybrać pierwszą implementację zamiast drugiej?

Edycja : Zauważ, że w ogólnym przypadku wstawienie O (log (n)) (jak zaimplementowano w pierwszym przykładzie) będzie szybsze niż ogólny algorytm sortowania; jednakże niekoniecznie ma to miejsce w szczególności w przypadku JavaScript. Zauważ, że:

  • Najlepszym przypadkiem dla kilku algorytmów wstawiania jest O (n), które nadal znacznie różni się od O (log (n)), ale nie tak źle jak O (n log (n)), jak wspomniano poniżej. Sprowadziłoby się to do konkretnego użytego algorytmu sortowania (patrz implementacja Javascript Array.sort? )
  • Metoda sortowania w JavaScript jest funkcją natywną, więc potencjalnie przynosząca ogromne korzyści - O (log (n)) z ogromnym współczynnikiem może nadal być znacznie gorsza niż O (n) dla zbiorów danych o rozsądnych rozmiarach.

używanie splice w drugiej implementacji jest trochę marnotrawstwem. Dlaczego nie użyć push?
Breton

Słuszna uwaga, właśnie skopiowałem to z pierwszego.
Elliot Kroo

4
Wszystko, co zawiera splice()(np. Twój pierwszy przykład), jest już O (n). Nawet jeśli wewnętrznie nie tworzy nowej kopii całej tablicy, potencjalnie musi przesuwać wszystkie n elementów wstecz o 1 pozycję, jeśli element ma być wstawiony na pozycji 0. Może to szybko, ponieważ jest to funkcja natywna, a stała to niski, ale mimo to jest O (n).
j_random_hacker

6
również, dla przyszłego odniesienia dla osób używających tego kodu, kod zawiera błąd podczas próby wstawienia na początek tablicy. Spójrz w dół, aby znaleźć poprawiony kod.
Pinokio,

3
Nie parseIntużywaj Math.floorzamiast tego użyj . Math.floorjest znacznie szybszy niż parseInt: jsperf.com/test-parseint-and-math-floor
Hubert Schölnast

Odpowiedzi:


58

Podobnie jak pojedynczy punkt danych, dla wykopów przetestowałem to, wstawiając 1000 losowych elementów do tablicy 100000 wstępnie posortowanych liczb przy użyciu dwóch metod w przeglądarce Chrome w systemie Windows 7:

First Method:
~54 milliseconds
Second Method:
~57 seconds

Tak więc, przynajmniej w tej konfiguracji, metoda natywna tego nie rekompensuje. Dzieje się tak nawet w przypadku małych zestawów danych, wstawiania 100 elementów do tablicy 1000:

First Method:
1 milliseconds
Second Method:
34 milliseconds

1
arrays.sort brzmi całkiem okropnie
njzk2

2
Wygląda na to, że array.splice musi robić coś naprawdę sprytnego, aby wstawić pojedynczy element w ciągu 54 mikrosekund.
gnasher729

@ gnasher729 - Nie sądzę, aby tablice JavaScript były w rzeczywistości takie same, jak fizycznie ciągłe tablice, takie jak w C. Myślę, że silniki JS mogą je zaimplementować jako mapę skrótów / słownik umożliwiający szybkie wstawianie.
Ian

1
Kiedy używasz funkcji komparatora z Array.prototype.sort, tracisz zalety C ++, ponieważ funkcja JS jest tak często nazywana.
aleclarson

Jak wypada porównanie pierwszej metody teraz, gdy Chrome używa TimSort ? Z Wikipedii TimSort : „W najlepszym przypadku, kiedy dane wejściowe są już posortowane, [TimSort] działa w czasie liniowym”.
najpiękniejszy

47

Proste ( Demo ):

function sortedIndex(array, value) {
    var low = 0,
        high = array.length;

    while (low < high) {
        var mid = (low + high) >>> 1;
        if (array[mid] < value) low = mid + 1;
        else high = mid;
    }
    return low;
}

4
Miły dotyk. Nigdy nie słyszałem o używaniu operatorów bitowych do znalezienia średniej wartości dwóch liczb. Normalnie pomnożyłbym po prostu przez 0,5. Czy robi to w ten sposób znaczący wzrost wydajności?
Jackson

2
@Jackson x >>> 1to binarne przesunięcie w prawo o 1 pozycję, co w rzeczywistości jest po prostu dzieleniem przez 2. np. Dla 11: 1011-> 101wyniki do 5.
Qwerty

3
@Qwerty @Web_Designer Będąc już na tym utworze, czy możesz wyjaśnić różnicę między >>> 1i ( widzianymi tu i tam ) >> 1?
yckart

4
>>>jest przesunięciem w prawo bez znaku, podczas gdy >>rozszerza znak - wszystko sprowadza się do reprezentacji w pamięci liczb ujemnych, gdzie wysoki bit jest ustawiany jako ujemny. Więc jeśli przesuniesz się w 0b1000prawo o 1 miejsce >>, otrzymujesz 0b1100, jeśli zamiast tego użyjesz >>>, otrzymasz 0b0100. O ile w przypadku podanym w odpowiedzi nie ma to większego znaczenia (liczba przesunięta nie jest ani większa od 32-bitowej dodatniej wartości całkowitej ze znakiem, ani ujemna), ważne jest, aby w tych dwóch przypadkach użyć właściwego musisz wybrać sprawę, którą chcesz obsłużyć).
asherkin

2
@asherkin - to nie jest w porządku: „jeśli przesuniesz się w 0b1000prawo o 1 miejsce >>, otrzymasz 0b1100”. Nie, rozumiesz 0b0100. Wynik różnych operatorów przesunięcia w prawo będzie taki sam dla wszystkich wartości z wyjątkiem liczb ujemnych i liczb większych niż 2 ^ 31 (tj. Liczb z 1 w pierwszym bicie).
gilly3

29

Bardzo dobre i niezwykłe pytanie z bardzo interesującą dyskusją! Używałem również tej Array.sort()funkcji po wypchnięciu pojedynczego elementu w tablicy z kilkoma tysiącami obiektów.

Musiałem rozszerzyć twoją locationOffunkcję dla swoich celów ze względu na złożone obiekty, a zatem potrzebę funkcji porównującej, takiej jak Array.sort():

function locationOf(element, array, comparer, start, end) {
    if (array.length === 0)
        return -1;

    start = start || 0;
    end = end || array.length;
    var pivot = (start + end) >> 1;  // should be faster than dividing by 2

    var c = comparer(element, array[pivot]);
    if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;

    switch (c) {
        case -1: return locationOf(element, array, comparer, start, pivot);
        case 0: return pivot;
        case 1: return locationOf(element, array, comparer, pivot, end);
    };
};

// sample for objects like {lastName: 'Miller', ...}
var patientCompare = function (a, b) {
    if (a.lastName < b.lastName) return -1;
    if (a.lastName > b.lastName) return 1;
    return 0;
};

7
Wydaje się warte odnotowania, dla przypomnienia, że ​​ta wersja DZIAŁA poprawnie przy próbie wstawienia na początek tablicy. (Warto o tym wspomnieć, ponieważ wersja w oryginalnym pytaniu ma błąd i nie działa poprawnie w tym przypadku.)
garyrob

3
Nie jestem pewien, czy moja implementacja była inna, ale musiałem zmienić trójskładnik na return c == -1 ? pivot : pivot + 1;, aby zwrócić poprawny indeks. W przeciwnym razie dla tablicy o długości 1 funkcja zwróci wartość -1 lub 0.
Niel

3
@James: Parametry start i end są używane tylko w wywołaniu rekurencyjnym i nie będą używane w wywołaniu początkowym. Ponieważ są to wartości indeksu dla tablicy, muszą być typu integer i przy wywołaniu rekurencyjnym jest to podawane niejawnie.
kwrl

1
@TheRedPea: nie, miałem na myśli, że >> 1powinno być szybsze (lub nie wolniejsze) niż/ 2
kwrl

1
Widzę potencjalny problem z wynikiem comparerfunkcji. W tym algorytmie jest porównywany, +-1ale może to być dowolna wartość <0/ >0. Zobacz funkcję porównania . Problematyczną częścią jest nie tylko switchstwierdzenie, ale także wiersz: if (end - start <= 1) return c == -1 ? pivot - 1 : pivot;gdzie cjest porównywany -1.
eXavier

19

W Twoim kodzie jest błąd. Powinien brzmieć:

function locationOf(element, array, start, end) {
  start = start || 0;
  end = end || array.length;
  var pivot = parseInt(start + (end - start) / 2, 10);
  if (array[pivot] === element) return pivot;
  if (end - start <= 1)
    return array[pivot] > element ? pivot - 1 : pivot;
  if (array[pivot] < element) {
    return locationOf(element, array, pivot, end);
  } else {
    return locationOf(element, array, start, pivot);
  }
}

Bez tej poprawki kod nigdy nie będzie mógł wstawić elementu na początku tablicy.


dlaczego wymawiasz int z 0? czyli co się zaczyna || 0 zrobić?
Pinokio,

3
@Pinocchio: start || 0 jest krótkim odpowiednikiem: if (! Start) start = 0; - Jednak wersja „dłuższa” jest skuteczniejsza, ponieważ nie przypisuje sobie zmiennej.
SuperNova

11

Wiem, że to stare pytanie, na które już jest odpowiedź, i istnieje wiele innych przyzwoitych odpowiedzi. Widzę kilka odpowiedzi, które sugerują, że możesz rozwiązać ten problem, wyszukując poprawny indeks wstawiania w O (log n) - możesz, ale nie możesz wstawić w tym czasie, ponieważ tablica musi być częściowo skopiowana, aby zrobić przestrzeń.

Konkluzja: Jeśli naprawdę potrzebujesz wstawiania i usuwania O (log n) do posortowanej tablicy, potrzebujesz innej struktury danych - nie tablicy. Powinieneś użyć B-Tree . Wzrost wydajności, który uzyskasz, korzystając z B-Tree dla dużego zestawu danych, przyćmiewa wszelkie oferowane tutaj ulepszenia.

Jeśli musisz użyć tablicy. Oferuję następujący kod, oparty na sortowaniu przez wstawianie, który działa wtedy i tylko wtedy, gdy tablica jest już posortowana. Jest to przydatne w przypadku, gdy musisz uciekać się po każdej wkładce:

function addAndSort(arr, val) {
    arr.push(val);
    for (i = arr.length - 1; i > 0 && arr[i] < arr[i-1]; i--) {
        var tmp = arr[i];
        arr[i] = arr[i-1];
        arr[i-1] = tmp;
    }
    return arr;
}

Powinien działać w O (n), co moim zdaniem jest najlepsze, co możesz zrobić. Byłoby ładniej, gdyby js obsługiwał wielokrotne przypisywanie. oto przykład do zabawy:

Aktualizacja:

to może być szybsze:

function addAndSort2(arr, val) {
    arr.push(val);
    i = arr.length - 1;
    item = arr[i];
    while (i > 0 && item < arr[i-1]) {
        arr[i] = arr[i-1];
        i -= 1;
    }
    arr[i] = item;
    return arr;
}

Zaktualizowany link JS Bin


W JavaScript sortowanie przez wstawianie, które proponujesz, będzie wolniejsze niż metoda wyszukiwania i łączenia binarnego, ponieważ splice ma szybką implementację.
trincot

chyba że javascript może w jakiś sposób złamać prawa złożoności czasowej, jestem sceptyczny. Czy masz działający przykład tego, jak metoda wyszukiwania i łączenia binarnego jest szybsza?
domoarigato

Cofam swój drugi komentarz ;-) Rzeczywiście, będzie rozmiar tablicy, powyżej którego rozwiązanie B-drzewa będzie lepsze od rozwiązania splatanego.
trincot

9

Twoja funkcja wstawiania zakłada, że ​​podana tablica jest posortowana, wyszukuje bezpośrednio lokalizację, w której można wstawić nowy element, zwykle po prostu patrząc na kilka elementów w tablicy.

Ogólna funkcja sortowania tablicy nie może przyjąć tych skrótów. Oczywiście musi przynajmniej sprawdzić wszystkie elementy w tablicy, aby zobaczyć, czy są już poprawnie uporządkowane. Już sam ten fakt sprawia, że ​​ogólne sortowanie jest wolniejsze niż funkcja wstawiania.

Ogólny algorytm sortowania ma zwykle średnią wartość O (n ⋅ log (n)) iw zależności od implementacji może to być w rzeczywistości najgorszy przypadek, jeśli tablica jest już posortowana, co prowadzi do złożoności O (n 2 ) . Zamiast tego bezpośrednie wyszukiwanie pozycji wstawienia ma tylko złożoność O (log (n)) , więc zawsze będzie znacznie szybsze.


Warto zauważyć, że wstawienie elementu do tablicy ma złożoność O (n), więc wynik końcowy powinien być mniej więcej taki sam.
NemPlayer

5

W przypadku niewielkiej liczby przedmiotów różnica jest dość trywialna. Jednakże, jeśli wstawiasz dużo elementów lub pracujesz z bardzo dużą tablicą, wywołanie funkcji .sort () po każdym wstawieniu spowoduje ogromne obciążenie.

Skończyło się na napisaniu całkiem zręcznej funkcji wyszukiwania / wstawiania plików binarnych właśnie w tym celu, więc pomyślałem, że się nią podzielę. Ponieważ whilezamiast rekurencji używa pętli, nie ma podsłuchiwania dodatkowych wywołań funkcji, więc myślę, że wydajność będzie nawet lepsza niż w przypadku każdej z pierwotnie opublikowanych metod. I domyślnie emuluje domyślny Array.sort()komparator, ale w razie potrzeby akceptuje niestandardową funkcję komparatora.

function insertSorted(arr, item, comparator) {
    if (comparator == null) {
        // emulate the default Array.sort() comparator
        comparator = function(a, b) {
            if (typeof a !== 'string') a = String(a);
            if (typeof b !== 'string') b = String(b);
            return (a > b ? 1 : (a < b ? -1 : 0));
        };
    }

    // get the index we need to insert the item at
    var min = 0;
    var max = arr.length;
    var index = Math.floor((min + max) / 2);
    while (max > min) {
        if (comparator(item, arr[index]) < 0) {
            max = index;
        } else {
            min = index + 1;
        }
        index = Math.floor((min + max) / 2);
    }

    // insert the item
    arr.splice(index, 0, item);
};

Jeśli jesteś otwarty na używanie innych bibliotek, lodash udostępnia funkcje sortIndex i sortLastIndex , których można użyć zamiast whilepętli. Dwie potencjalne wady to 1) wydajność nie jest tak dobra jak moja metoda (myślę, że nie jestem pewien, o ile jest gorsza) i 2) nie akceptuje niestandardowej funkcji komparatora, a jedynie metodę uzyskiwania wartości do porównania (zakładam, że używam domyślnego komparatora).


wezwaniem arr.splice()jest z pewnością złożoność O (n) czasowa.
domoarigato

4

Oto kilka przemyśleń: Po pierwsze, jeśli naprawdę martwisz się o środowisko wykonawcze swojego kodu, upewnij się, że wiesz, co się dzieje, gdy wywołujesz funkcje wbudowane! Nie wiem od góry w javascript, ale szybkie wygooglowanie funkcji splice zwróciło to , co wydaje się wskazywać, że tworzysz zupełnie nową tablicę przy każdym wywołaniu! Nie wiem, czy to rzeczywiście ma znaczenie, ale na pewno ma to związek z wydajnością. Widzę, że Breton już w komentarzach zwrócił na to uwagę, ale z pewnością ma to zastosowanie do dowolnej wybranej funkcji manipulowania tablicami.

W każdym razie, aby faktycznie rozwiązać problem.

Kiedy przeczytałem, że chcesz posortować, moją pierwszą myślą jest użycie sortowania przez wstawianie! . Jest to przydatne, ponieważ działa w czasie liniowym na posortowanych lub prawie posortowanych listach . Ponieważ twoje tablice będą miały tylko 1 nieprawidłowy element, liczy się to jako prawie posortowane (z wyjątkiem, no cóż, tablic o rozmiarze 2 lub 3 lub cokolwiek innego, ale w tym momencie daj spokój). Wdrożenie sortowania nie jest takie złe, ale jest to kłopot, z którym możesz nie chcieć sobie radzić, i znowu nie wiem nic o javascript i czy będzie to łatwe, trudne, czy co tam. Eliminuje to potrzebę korzystania z funkcji wyszukiwania i po prostu naciskasz (jak sugerował Breton).

Po drugie, twoja funkcja wyszukiwania „quicksort-esque” wydaje się być binarnym algorytmem wyszukiwania ! To bardzo fajny algorytm, intuicyjny i szybki, ale z jednym haczykiem: notorycznie trudno go poprawnie zaimplementować. Nie ośmielę się powiedzieć, czy twoje jest poprawne, czy nie (mam nadzieję, że tak! :)), ale bądź ostrożny, jeśli chcesz go użyć.

W każdym razie, podsumowanie: użycie "push" z sortowaniem przez wstawianie będzie działać w czasie liniowym (zakładając, że reszta tablicy jest posortowana) i pozwoli uniknąć bałaganu w wymaganiach algorytmu wyszukiwania binarnego. Nie wiem, czy to najlepszy sposób (podstawowa implementacja tablic, może szalona wbudowana funkcja robi to lepiej, kto wie), ale wydaje mi się to rozsądne. :) - Agor.


1
+1, ponieważ wszystko, co zawiera, splice()jest już O (n). Nawet jeśli nie utworzy wewnętrznie nowej kopii całej tablicy, potencjalnie musi przesunąć wszystkie n elementów z powrotem o 1 pozycję, jeśli element ma zostać wstawiony na pozycji 0.
j_random_hacker

Uważam, że sortowanie przez wstawianie jest również O (n) najlepszym przypadkiem i O (n ^ 2) najgorszym przypadkiem (chociaż przypadek użycia OP jest prawdopodobnie najlepszym przypadkiem).
domoarigato

Minus jeden za rozmowę z OP. Pierwszy akapit wydawał się zbędnym upomnieniem za to, że nie wiedziałeś, jak działa splot pod maską
Matt Zera

2

Oto porównanie czterech różnych algorytmów służących do osiągnięcia tego: https://jsperf.com/sorted-array-insert-comparison/1

Algorytmy

Naiwność jest zawsze okropna. Wydaje się, że w przypadku małych rozmiarów tablic pozostałe trzy nie różnią się zbytnio, ale w przypadku większych tablic ostatnie 2 przewyższają proste podejście liniowe.


Dlaczego nie przetestować struktur danych zaprojektowanych do szybkiego wstawiania i wyszukiwania? dawny. pominąć listy i BST. stackoverflow.com/a/59870937/3163618
qwr

Jak wypada porównanie Native, gdy Chrome używa TimSort ? Z Wikipedii TimSort : „W najlepszym przypadku, kiedy dane wejściowe są już posortowane, działa w czasie liniowym”.
najpiękniejszy

2

Oto wersja wykorzystująca lodash.

const _ = require('lodash');
sortedArr.splice(_.sortedIndex(sortedArr,valueToInsert) ,0,valueToInsert);

uwaga: sortIndex wykonuje wyszukiwanie binarne.


1

Najlepszą strukturą danych, o jakiej przychodzi mi do głowy, jest indeksowana lista pomijania, która zachowuje właściwości wstawiania połączonych list ze strukturą hierarchiczną, która umożliwia operacje w czasie rejestrowania. Przeciętnie wyszukiwanie, wstawianie i wyszukiwanie dostępu swobodnego można przeprowadzić w czasie O (log n).

Drzewo Kolejność statystyka pozwala dziennika czasu indeksowania z funkcji rankingu.

Jeśli nie potrzebujesz dostępu swobodnego, ale potrzebujesz wstawienia O (log n) i wyszukania kluczy, możesz porzucić strukturę tablicy i użyć dowolnego rodzaju drzewa wyszukiwania binarnego .

Żadna z odpowiedzi nie array.splice()jest w ogóle skuteczna, ponieważ jest to średni czas O (n). Jaka jest złożoność czasowa funkcji array.splice () w przeglądarce Google Chrome?


Jak to odpowiadaIs there a good reason to choose [splice into location found] over [push & sort]?
siwobrody

1
@greybeard To odpowiada tytuł. cynicznie żaden wybór nie jest skuteczny.
qwr

Żadna opcja nie mogłaby być skuteczna, gdyby obejmowała kopiowanie wielu elementów tablicy.
qwr

1

Oto moja funkcja, używa wyszukiwania binarnego, aby znaleźć element, a następnie odpowiednio wstawia:

function binaryInsert(val, arr){
    let mid, 
    len=arr.length,
    start=0,
    end=len-1;
    while(start <= end){
        mid = Math.floor((end + start)/2);
        if(val <= arr[mid]){
            if(val >= arr[mid-1]){
                arr.splice(mid,0,val);
                break;
            }
            end = mid-1;
        }else{
            if(val <= arr[mid+1]){
                arr.splice(mid+1,0,val);
                break;
            }
            start = mid+1;
        }
    }
    return arr;
}

console.log(binaryInsert(16, [
    5,   6,  14,  19, 23, 44,
   35,  51,  86,  68, 63, 71,
   87, 117
 ]));


0

Nie sortuj ponownie po każdym elemencie, to przesada ...

Jeśli jest tylko jeden element do wstawienia, możesz znaleźć lokalizację do wstawienia za pomocą wyszukiwania binarnego. Następnie użyj memcpy lub czegoś podobnego, aby zbiorczo skopiować pozostałe elementy, aby zrobić miejsce na wstawiony. Wyszukiwanie binarne to O (log n), a kopia to O (n), co daje łącznie O (n + log n). Korzystając z powyższych metod, wykonujesz ponowne sortowanie po każdym wstawieniu, czyli O (n log n).

Czy to ma znaczenie? Powiedzmy, że losowo wstawiasz k elementów, gdzie k = 1000. Posortowana lista zawiera 5000 elementów.

  • Binary search + Move = k*(n + log n) = 1000*(5000 + 12) = 5,000,012 = ~5 million ops
  • Re-sort on each = k*(n log n) = ~60 million ops

Jeśli k elementów do wstawienia pojawi się kiedykolwiek, musisz wykonać wyszukiwanie + przesuń. Jeśli jednak otrzymasz listę k elementów do wstawienia do posortowanej tablicy - z wyprzedzeniem - możesz zrobić jeszcze lepiej. Sortuj k elementów oddzielnie od już posortowanej tablicy n. Następnie wykonaj sortowanie ze skanowaniem, w którym przesuniesz w dół obie posortowane tablice jednocześnie, scalając jedną w drugą. - Jednostopniowe sortowanie scalające = k log k + n = 9965 + 5000 = ~ 15 000 operacji

Aktualizacja: jeśli chodzi o twoje pytanie.
First method = binary search+move = O(n + log n). Second method = re-sort = O(n log n)Dokładnie wyjaśnia, jakie czasy otrzymujesz.


tak, ale nie, to zależy od algorytmu sortowania. Używając sortowania bąbelkowego w odwrotnej kolejności, sortowanie, jeśli ostatni element nie jest posortowany, jest zawsze o (n)
njzk2

-1
function insertOrdered(array, elem) {
    let _array = array;
    let i = 0;
    while ( i < array.length && array[i] < elem ) {i ++};
    _array.splice(i, 0, elem);
    return _array;
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.