Per aiutare a chiarire il comportamento di Array#sort
e il suo comparatore, considera questo ingenuo tipo di inserzione insegnato all'inizio dei corsi di programmazione:
const sort = arr => {
for (let i = 1; i < arr.length; i++) {
for (let j = i; j && arr[j-1] > arr[j]; j--) {
[arr[j], arr[j-1]] = [arr[j-1], arr[j]];
}
}
};
const array = [3, 0, 4, 5, 2, 2, 2, 1, 2, 2, 0];
sort(array);
console.log("" + array);
Ignorando la scelta di inserzione come algoritmo, fuoco sulla hardcoded comparatore: arr[j-1] > arr[j]
. Questo ha due problemi rilevanti per la discussione:
- L'
>
operatore viene invocato su coppie di elementi dell'array ma molte cose che potresti voler ordinare come gli oggetti non rispondono >
in modo ragionevole (lo stesso sarebbe vero se usassimo-
).
- Anche se stai lavorando con i numeri, spesso vuoi un arrangiamento diverso da quello crescente che è stato incorporato qui.
Possiamo risolvere questi problemi aggiungendo un comparefn
argomento con cui hai familiarità:
const sort = (arr, comparefn) => {
for (let i = 1; i < arr.length; i++) {
for (let j = i; j && comparefn(arr[j-1], arr[j]) > 0; j--) {
[arr[j], arr[j-1]] = [arr[j-1], arr[j]];
}
}
};
const array = [3, 0, 4, 5, 2, 2, 2, 1, 2, 2, 0];
sort(array, (a, b) => a - b);
console.log("" + array);
sort(array, (a, b) => b - a);
console.log("" + array);
const objArray = [{id: "c"}, {id: "a"}, {id: "d"}, {id: "b"}];
sort(objArray, (a, b) => a.id.localeCompare(b.id));
console.log(JSON.stringify(objArray, null, 2));
Ora l'ingenua routine di ordinamento è generalizzata. Puoi vedere esattamente quando viene richiamato questo callback, rispondendo alla tua prima serie di dubbi:
La funzione di callback per l'ordinamento dell'array viene chiamata molte volte nel corso dell'ordinamento? In tal caso, mi piacerebbe sapere quali due numeri vengono passati ogni volta nella funzione
L'esecuzione del codice seguente mostra che, sì, la funzione viene chiamata molte volte e puoi usare console.log
per vedere quali numeri sono stati passati:
const sort = (arr, comparefn) => {
for (let i = 1; i < arr.length; i++) {
for (let j = i; j && comparefn(arr[j-1], arr[j]) > 0; j--) {
[arr[j], arr[j-1]] = [arr[j-1], arr[j]];
}
}
};
console.log("on our version:");
const array = [3, 0, 4, 5];
sort(array, (a, b) => console.log(a, b) || (a - b));
console.log("" + array);
console.log("on the builtin:");
console.log("" +
[3, 0, 4, 5].sort((a, b) => console.log(a, b) || (a - b))
);
Tu chiedi:
Come vengono quindi ordinate le due serie di numeri l'una rispetto all'altra?
Per essere precisi con la terminologia, a
e b
non sono insiemi di numeri: sono oggetti nell'array (nel tuo esempio, sono numeri).
La verità è che non importa come vengono ordinati perché dipende dall'implementazione. Se avessi utilizzato un algoritmo di ordinamento diverso dall'ordinamento per inserzione, il comparatore verrebbe probabilmente invocato su coppie di numeri diverse, ma alla fine della chiamata di ordinamento, l'invariante che conta per il programmatore JS è che la matrice dei risultati è ordinata in base al comparatore, assumendo che il comparatore restituisca valori che aderiscono al contratto che hai dichiarato (<0 quando a < b
, 0 quando a === b
e> 0 quando a > b
).
Nello stesso senso in cui ho la libertà di cambiare l'implementazione del mio tipo fintanto che non infrango la mia specifica, le implementazioni di ECMAScript sono libere di scegliere l'implementazione dell'ordinamento entro i confini della specifica del linguaggio , quindi Array#sort
probabilmente produrrà diverse chiamate di confronto su motori diversi. Non si scriverebbe codice in cui la logica si basa su una particolare sequenza di confronti (né il comparatore dovrebbe produrre effetti collaterali in primo luogo).
Ad esempio, il motore V8 (al momento della scrittura) richiama Timsort quando l'array è più grande di un certo numero di elementi precalcolati e utilizza un ordinamento per inserimento binario per piccoli blocchi di array. Tuttavia, utilizzava quicksort che è instabile e probabilmente darebbe una diversa sequenza di argomenti e chiamate al comparatore.
Poiché diverse implementazioni di ordinamento utilizzano il valore di ritorno della funzione di confronto in modo diverso, ciò può portare a un comportamento sorprendente quando il comparatore non aderisce al contratto. Vedi questo thread per un esempio.