Чтобы помочь прояснить поведение Array#sort
и его компаратор, рассмотрим эту наивную сортировку вставки, которую преподают в начальных курсах программирования:
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);
Не обращая внимания на выбор вставки рода в качестве алгоритма, фокуса на закодированном компараторе: arr[j-1] > arr[j]
. Здесь есть две проблемы, относящиеся к обсуждению:
>
Оператор вызывается на пары элементов массива , но многие вещи , которые вы , возможно , захотите сортировать такие как объекты не отвечают >
в разумных пределах (то же самое было бы верно , если бы мы использовали -
).
- Даже если вы работаете с числами, часто вам нужно другое расположение, а не сортировка по возрастанию, которая здесь используется.
Мы можем решить эти проблемы, добавив comparefn
знакомый вам аргумент:
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));
Теперь наивная процедура сортировки обобщена. Вы можете точно увидеть, когда вызывается этот обратный вызов, отвечая на ваш первый набор проблем:
Вызывается ли функция обратного вызова сортировки массива много раз в ходе сортировки? Если да, то я хотел бы знать, какие два числа передаются в функцию каждый раз.
Выполнение приведенного ниже кода показывает, что да, функция вызывается много раз, и вы можете использовать ее, console.log
чтобы увидеть, какие числа были переданы:
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))
);
Ты спрашиваешь:
Как тогда эти два набора чисел сортируются по отношению друг к другу?
Чтобы быть точным , с терминологией, a
а b
не наборы чисел - they're объекты в массиве (в вашем примере, они номер).
По правде говоря, не имеет значения, как они отсортированы, потому что это зависит от реализации. Если бы я использовал другой алгоритм сортировки, чем сортировка вставкой, компаратор, вероятно, вызывался бы для разных пар чисел, но в конце вызова сортировки инвариант, который имеет значение для программиста JS, заключается в том, что массив результатов сортируется в соответствии с компаратор, предполагая, что компаратор возвращает значения, которые соответствуют указанному вами контракту (<0, когда a < b
, 0, когда a === b
и> 0, когдаa > b
).
В том же смысле, что у меня есть свобода изменять свою реализацию сортировки до тех пор, пока я не нарушаю свою спецификацию, реализации ECMAScript могут свободно выбирать реализацию сортировки в пределах спецификации языка , поэтому Array#sort
, вероятно, будут создаваться разные вызовы компаратора на разных двигателях. Нельзя писать код, в котором логика полагается на некоторую конкретную последовательность сравнений (и компаратор не должен вызывать побочные эффекты в первую очередь).
Например, механизм V8 (на момент написания) вызывает Timsort, когда массив больше, чем некоторое предварительно вычисленное количество элементов, и использует сортировку двоичной вставкой для небольших фрагментов массива. Однако раньше он использовал быструю сортировку, которая нестабильна и, вероятно, дает другую последовательность аргументов и вызовов компаратору.
Поскольку разные реализации сортировки по-разному используют возвращаемое значение функции компаратора, это может привести к неожиданному поведению, когда компаратор не соблюдает контракт. См. Эту ветку для примера.