Para ayudar a aclarar el comportamiento de Array#sort
y su comparador, considere este tipo de inserción ingenuo que se enseña en los cursos de programación para principiantes:
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 elección de ordenación por inserción como algoritmo, se centran en la hardcoded comparador: arr[j-1] > arr[j]
. Esto tiene dos problemas relevantes para la discusión:
- El
>
operador se invoca en pares de elementos de matriz, pero muchas cosas que podría querer ordenar, como los objetos, no responden de >
una manera razonable (lo mismo sería cierto si usáramos -
).
- Incluso si está trabajando con números, a menudo desea algún otro arreglo que no sea el ascendente que se ha incorporado aquí.
Podemos solucionar estos problemas agregando un comparefn
argumento con el que esté familiarizado:
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));
Ahora la rutina de clasificación ingenua está generalizada. Puede ver exactamente cuándo se invoca esta devolución de llamada, respondiendo a su primer conjunto de inquietudes:
¿Se llama muchas veces a la función de devolución de llamada de clasificación de matriz durante el transcurso de la clasificación? Si es así, me gustaría saber qué dos números se pasan a la función cada vez
Ejecutar el siguiente código muestra que, sí, la función se llama muchas veces y puede usarla console.log
para ver qué números se pasaron:
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))
);
Usted pregunta:
¿Cómo se ordenan los dos conjuntos de números entre sí?
Para ser precisos con la terminología, a
y b
no son conjuntos de números, son objetos en la matriz (en su ejemplo, son números).
La verdad es que no importa cómo se clasifiquen porque depende de la implementación. Si hubiera usado un algoritmo de ordenación diferente al de ordenación por inserción, el comparador probablemente se invocaría en diferentes pares de números, pero al final de la llamada de ordenación, el invariante que le importa al programador de JS es que la matriz de resultados se ordena de acuerdo con el comparador, asumiendo que el comparador devuelve valores que se adhieren al contrato que estableció (<0 cuando a < b
, 0 cuando a === b
y> 0 cuando a > b
).
En el mismo sentido en que tengo la libertad de cambiar la implementación de mi clasificación siempre que no infrinja mi especificación, las implementaciones de ECMAScript son libres de elegir la implementación de clasificación dentro de los límites de la especificación del lenguaje , por Array#sort
lo que probablemente producirá diferentes llamadas de comparador en diferentes motores. Uno no escribiría código donde la lógica se basa en alguna secuencia particular de comparaciones (ni el comparador debería producir efectos secundarios en primer lugar).
Por ejemplo, el motor V8 (en el momento de escribir este artículo) invoca Timsort cuando la matriz es mayor que un número precalculado de elementos y usa una ordenación de inserción binaria para fragmentos de matriz pequeños. Sin embargo, solía usar quicksort que es inestable y probablemente daría una secuencia diferente de argumentos y llamadas al comparador.
Dado que las diferentes implementaciones de ordenación utilizan el valor de retorno de la función de comparación de manera diferente, esto puede llevar a un comportamiento sorprendente cuando el comparador no se adhiere al contrato. Vea este hilo para ver un ejemplo.