Sortowanie w JavaScript: czy zwracanie wartości logicznej nie powinno wystarczyć do funkcji porównawczej?
Zawsze z powodzeniem sortowałem moje tablice w ten sposób (kiedy nie chciałem standardowego porządkowania leksykograficznego):
var arr = […] // some numbers or so
arr.sort(function(a, b) {
return a > b;
});
Ktoś mi powiedział, że to było złe i że będę musiał to zrobić. Czy to prawda, a jeśli tak, to dlaczego? Przetestowałem moją funkcję porównawczą i działa! Poza tym, dlaczego moje rozwiązanie byłoby tak powszechne , gdy jest złe? 2 answers
TL;DR
Nie, Nie zrobiłeś tego. I nie zauważyłem tego. Szybki kontrprzykład:Zawsze z powodzeniem sortowałem moje tablice w ten sposób
> [1,1,0,2].sort(function(a, b){ return a>b })
Array [0, 1, 2, 1]
// in Opera 12. Results may vary between sorting algorithm implementations
Dlaczego?
Ponieważ funkcja porównawcza zwraca false
(lub 0
, równoważnie) nawet wtedy, gdy b
jest większa niż a
. Ale 0
implikuje, że dwa elementy są uważane za równe-i algorytm sortowania uważa, że.
In-Depth Wyjaśnienie
Funkcje porównawcze w JavaScript
Jak działają funkcje porównawcze?
The Array::sort
metoda może przyjmować opcjonalną, niestandardową funkcję porównawczą jako swój argument. Funkcja ta przyjmuje dwa argumenty (powszechnie określane jako a
i b
), które powinna porównać i ma zwracać liczbę
-
> 0
Gdya
jest uważane za większe niżb
i należy je posortować po it -
== 0
gdya
jest uważane za równeb
i nie ma znaczenia, który będzie pierwszy -
< 0
gdya
jest uważany za mniejszy niżb
i powinien być sortowany przed
Jeśli nie zwróci liczby, wynik zostanie oddany do liczby (co jest przydatne dla booleanów). Zwracana liczba nie musi być dokładnie -1
lub 0
lub 1
(choć zazwyczaj tak jest).
Konsekwentne porządkowanie
Aby być konsekwentnym, funkcja porównania musiałaby spełnić równanie
comp(a, b) == -1 * comp(b, a)
// or, if values other than -1, 0 and 1 are considered:
comp(a, b) * comp(b, a) <= 0
Jeśli ten wymóg jest złamany, sortowanie będzie zachowywać się niezdefiniowany.
Powołując się na ES5. 1 spec na sort
(to samo w ES6 spec):
jeśli
comparefn
jest [ ... ] nie jest spójną funkcją porównawczą dla elementów tej tablicy, zachowanie sort jest zdefiniowane w implementacji.funkcja {[27] } jest spójną funkcją porównawczą dla zbioru of values
S
if all of the requirements below are specified for all valuesa
,b
, ic
(ewentualnie ta sama wartość) w zbiorzeS
: zapisa <CF b
oznaczacomparefn(a,b) < 0
;a =CF b
oznaczacomparefn(a,b) = 0
(jednego ze znaków); oraza >CF b
oznaczacomparefn(a,b) > 0
.wywołanie
comparefn(a,b)
zawsze zwraca tę samą wartośćv
, gdy podano określoną parę wartościa
ib
jako dwa argumenty. PonadtoType(v)
jest liczbą, av
nie jestNaN
. Zauważ, że oznacza to, że dokładnie jeden za <CF b
,a =CF b
, i {[38] } będzie prawdziwe dla danej parya
ib
.
- wywołanie
comparefn(a,b)
nie modyfikuje tego obiektu.a =CF a
(refleksyjność)- If
a =CF b
, thenb =CF a
(symetria)- If
a =CF b
andb =CF c
, thena =CF c
(przechodniość z=CF
)- If
a <CF b
andb <CF c
, thena <CF c
(przechodniość<CF
)- If
a >CF b
andb >CF c
, thena >CF c
(przechodniość>CF
)uwaga: powyższe warunki są konieczne i wystarczające do zapewnienia, że
comparefn
dzieli zbiórS
na klasy równoważności i że te klasy równoważności są całkowicie uporządkowane.
Co to znaczy? Co mnie to obchodzi?
Algorytm sortowania musi porównywać elementy tablicy z każdym inne. Aby wykonać dobrą i wydajną pracę, nie musi porównywać każdego przedmiotu z każdym innym, ale musi być w stanie rozumować o ich zamówieniu. Aby to dobrze działało, istnieje kilka zasad, których musi przestrzegać funkcja porównywania niestandardowego. Trywialne jest to, że element a
jest równy sobie (compare(a, a) == 0
) - to pierwszy element z powyższej listy (refleksywność). Tak, to trochę matematyczne, ale dobrze się opłaca.
Najważniejszą z nich jest przechodniość. Mówi, że gdy algorytm porównał dwie wartości a
i b
, a także b
z c
i odkrył, stosując funkcję porównawczą, która np. a = b
i b < c
, to może oczekiwać, że a < c
również się trzyma. Wydaje się to tylko logiczne i jest wymagane dla dobrze zdefiniowanego, spójnego uporządkowania.
Ale twoja funkcja porównawcza zawodzi . Spójrzmy na ten przykład:
function compare(a, b) { return Number(a > b); }
compare(0, 2) == 0 // ah, 2 and 0 are equal
compare(1, 0) == 1 // ah, 1 is larger than 0
// let's conclude: 1 is also larger than 2
Ooops. I dlatego algorytm sortowania może zawieść (w specyfikacji jest to be "zachowanie zależne od implementacji " -tj. nieprzewidywalne wyniki), gdy jest wywoływane z funkcją porównawczą, która nie jest spójna.
Dlaczego złe rozwiązanie jest tak powszechne?
Ponieważ w wielu innych językach istnieją algorytmy sortowania, które nie oczekują trójstronnego porównania , a jedynie boolean mniejszy od operatora. C++ std::sort
jest tego dobrym przykładem. Zostanie po prostu zastosowany dwa razy z wymienionymi argumentami jeśli zachodzi potrzeba ustalenia równości. Co prawda, może to być bardziej wydajne i mniej podatne na błędy, ale wymaga więcej wywołań do funkcji porównawczej, jeśli operator nie może być inlinowany.
Kontrwywiad
Przetestowałem swoją funkcję porównawczą i działa!
Tylko przez szczęście, jeśli spróbujesz jakiegoś przypadkowego przykładu. Lub dlatego, że twój zestaw testów jest wadliwy - niepoprawny i / lub niekompletny.
Oto mały skrypt, który znalazłem powyższy minimalny kontrprzykład:
function perms(n, i, arr, cb) {
// calls callback with all possible arrays of length n
if (i >= n) return cb(arr);
for (var j=0; j<n; j++) {
arr[i] = j;
perms(n, i+1, arr, cb);
}
}
for (var i=2; ; i++) // infinite loop
perms(i, 0, [], function(a) {
if ( a.slice().sort(function(a,b){ return a>b }).toString()
!= a.slice().sort(function(a,b){ return a-b }).toString() )
// you can also console.log() all of them, but remove the loop!
throw a.toString();
});
Jaka funkcja porównawcza jest poprawna?
Nie używaj żadnej funkcji porównawczej, jeśli chcesz sortować leksykograficznie. W razie potrzeby pozycje w tablicy będą stringified.
Ogólna funkcja porównawcza, która działa jak operatory relacyjne, może być zaimplementowana jako
function(a, b) {
if (a > b) return 1;
if (a < b) return -1;
/* else */ return 0;
}
Za pomocą kilku sztuczek można to minifigurować do odpowiednika function(a,b){return +(a>b)||-(a<b)}
.
Dla liczb , można po prostu zwrócić ich różnica, która przestrzega wszystkich powyższych praw:
function(a, b) {
return a - b; // but make sure only numbers are passed (to avoid NaN)
}
Jeśli chcesz sortować w odwrotnej kolejności, po prostu weź odpowiedni i zamień a
na b
.
Jeśli chcesz sortować typy złożone (obiekty itp.), zamień każdy a
i każdy b
na dostęp do danych właściwości, wywołanie metody lub cokolwiek chcesz sortować według.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:15
The sort
funkcja oczekuje funkcji, która oczekuje dwóch argumentów a
i b
i zwraca:
- liczba ujemna jeśli a przychodzi przed b
- liczba dodatnia Jeśli a przychodzi po b
- Zero, jeśli względny porządek a i b nie ma znaczenia
W celu sortowania liczb w porządku rosnącym return a - b
wygeneruje prawidłowe wartości zwracane; na przykład:
a b ret
1 2 -1
3 2 1
2 2 0
Z drugiej strony return a > b
produkuje następujące wartości zwracane:
a b ret implied
1 2 false 0
3 2 true 1
2 2 false 0
W powyższym przykładzie funkcja sortowania mówi, że 1 i 2 są tym samym (i umieszczenie 1 Przed 2 lub 2 przed 1 nie ma znaczenia). Spowoduje to niepoprawny wynik ,na przykład (w Chrome 49):
console.log([5, 8, 7, 1, 2, 3, 4, 6, 9, 10, 11, 12, 13].sort(function(a, b) {
return a > b;
}));
// [4, 5, 3, 1, 2, 6, 7, 8, 9, 10, 11, 12, 13]
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-10-15 17:39:06