Сортировка чисел в порядке убывания, но с `0` в начале

36

У меня есть проблема в JavaScript, которую я пытаюсь решить уже некоторое время.

Рассмотрим этот массив:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

Я должен вывести этот результат:

arr = [0, 0, 0, 0, 0, 5, 4, 3, 2, 1]

Я следую этой строке логики, чтобы расположить нули впереди, регулируя значение индекса:

arr.sort((x, y) => {
    if (x !== 0) {
        return 1;
    }

    if (x === 0) {
        return -1;
    }

    return y - x;
});

Но я застрял в этом результате:

arr = [0, 0, 0, 0, 0, 1, 2, 3, 4, 5]

У кого-нибудь есть какие-нибудь советы, как это решить?

lianbwl
источник
6
Гарантируется, что нет отрицательных чисел?
Майкл - Где Клэй Ширки
2
Разве это не исправляет, если последняя строка переключена на return x - y;?
Mooing Duck
2
Будет ли эффективно в JavaScript подсчитывать и удалять нули, сортировать оставшиеся элементы нормально? (без специального компаратора, так что, надеюсь, механизм JS может использовать встроенную функцию сортировки чисел). Затем добавьте правильное количество нулей к результату. Если имеется много нулей, удаление их перед сортировкой создает меньшую проблему. Или поменять нули на начало массива за один проход, а затем отсортировать хвост массива?
Питер Кордес
2
Это вообще когда-нибудь доходило return y - x;? Даже в javascript я не могу думать ни о чем, что было бы ни то, ===0ни другое !==0.
Джордж Т
2
JSPerf для ответов: jsperf.com/stackoverflow-question-58933996-v2
Салман A

Ответы:

35

Вы можете отсортировать по дельте bи a(для сортировки по убыванию) и принять Number.MAX_VALUEдля ложных значений, таких как ноль.

Эта:

Number.MAX_VALUE - Number.MAX_VALUE

равно нулю.

let array = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

array.sort((a, b) => (b || Number.MAX_VALUE) - (a || Number.MAX_VALUE));

console.log(...array);

Нина Шольц
источник
13
Ваша функция сравнения вернется, NaNесли оба aи bравны нулю. Это может быть нежелательным поведением.
Ден04
20
Результаты Array.prototype.sortопределяются реализацией, если компаратор когда-либо возвращается NaN, поэтому этот компаратор - плохая идея. Он пытается быть умным и ошибается.
user2357112 поддерживает Monica
3
Что делать, если элемент внутри массива бесконечность? Функция сравнения возвращает 0 при сравнении числа Бесконечности с 0.
Марко
4
Спасибо вам всем. Я беру наибольшее число, которое может быть вычтено само собой, и надеюсь, что это число не используется в массиве op.
Нина Шольц
4
Абсолютно не читается. Круто, но нечитаемо.
Салман А
24

Как сказано в MDN Docs:

Если a и b - два сравниваемых элемента, то:

Если compareFunction(a, b)возвращается меньше чем 0, сортируйте aпо индексу ниже чем b(то есть a стоит первым).

Если compareFunction(a, b)возвращается 0, оставьте a и b неизменными по отношению друг к другу, но отсортированные по всем различным элементам. Примечание: стандарт ECMAscript не гарантирует такого поведения, поэтому не все браузеры (например, версии Mozilla, датируемые как минимум 2003 годом) соблюдают это.

Если compareFunction(a, b) возвращается больше 0, сортируйте bпо индексу ниже, чем (т.е. bидет первым).

compareFunction(a, b)всегда должен возвращать одно и то же значение, если ему дана определенная пара элементов aи bдва аргумента. Если возвращаются противоречивые результаты, порядок сортировки не определен.

Итак, функция сравнения имеет следующий вид:

function compare(a, b) {
  if (a is less than b by some ordering criterion) {
    return -1;
  }
  if (a is greater than b by the ordering criterion) {
    return 1;
  }
  // a must be equal to b
  return 0;
}

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

arr.sort((x, y) => {
    if (x > 0 && y > 0) {
        return y - x;
    }
    return x - y;
});

console.log(arr);

стартап
источник
3
+1 за то, что дает единственное решение с функцией сравнения, которая фактически соответствует (как определено в стандарте ) для всех входных данных, включая отрицательные числа.
Илмари
3
@IlmariKaronen: К сожалению, сравнение соответствует отрицательным числам, но это неправильно сравнение отрицательных чисел. Негативы сортируются до 0 с помощью этого компаратора и сортируются в порядке возрастания.
user2357112 поддерживает Monica
2
@ user2357112supportsMonica Тем не менее, OP не указал желаемое поведение для отрицательных чисел, и с этим прототипом тривиально настроить поведение отрицательных чисел в соответствии с любым требованием OP. Что хорошо в этом ответе, так это то, что он идиоматичен и использует стандартную функцию сравнения для выполнения этой работы.
J ...
13

Если вы заботитесь об эффективности, возможно, быстрее всего сначала отфильтровать нули . Вы не хотите sortтратить время, даже глядя на них, не говоря уже о добавлении дополнительной работы к вашему обратному вызову сравнения для обработки этого особого случая.

Особенно, если вы ожидаете значительное количество нулей, один проход по данным для их фильтрации должен быть намного лучше, чем выполнять более крупную O (N log N) сортировку, которая будет смотреть на каждый ноль несколько раз.

Вы можете эффективно добавлять правильное количество нулей после того, как вы закончите.

Также легко прочитать полученный код. Я использовал TypedArray, потому что он эффективен и упрощает сортировку чисел . Но вы можете использовать эту технику с обычным массивом, используя стандартную идиому (a,b)=>a-bдля .sort.

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

let nonzero_arr = Int32Array.from(arr.filter(n => n != 0));
let zcount = arr.length - nonzero_arr.length;
nonzero_arr.sort();      // numeric TypedArray sorts numerically, not alphabetically

// Reverse the sorted part before copying into the final array.
nonzero_arr.reverse();

 // efficient-ish TypedArray for main result
let revsorted = new Int32Array(arr.length);   // zero-filled full size
revsorted.set(nonzero_arr, zcount);           // copy after the right number of zeros

console.log(Array.from(revsorted));      // prints strangely for TypedArray, with invented "0", "1" keys

/*
   // regular Array result
let sorted = [...Array(zcount).fill(0), ...nonzero_arr]  // IDK if this is efficient
console.log(sorted);
*/

Я не знаю, если TypedArray, .sort()а затем .reverseбыстрее, чем с помощью пользовательской функции сравнения для сортировки в порядке убывания. Или, если мы можем копировать и обратно на лету с итератором.


Также стоит подумать: используйте только один TypedArray полной длины .

Вместо того, чтобы использовать .filter, зациклите его и поменяйте местами нули в начале массива. Это займет один проход над вашими данными.

Затем используйте .subarray()для получения нового представления TypedArray ненулевых элементов того же базового ArrayBuffer. Сортировка, в результате которой вы получите полный массив с нулевым началом и отсортированным хвостом, причем сортировка всегда будет рассматривать только ненулевые элементы.

Я не видел функции разделения в методах Array или TypedArray, но я почти не знаю JavaScript. При хорошем JIT цикл не должен быть намного хуже, чем встроенный метод. (Особенно, когда этот метод включает в себя функцию обратного вызова .filter, и если он не используется reallocпод капотом для сжатия, он должен выяснить, сколько памяти выделить, прежде чем он фактически отфильтровывается).

Я использовал обычный массив .filter() перед преобразованием в TypedArray. Если ваши входные данные уже являются TypedArray, у вас нет этой проблемы, и эта стратегия становится еще более привлекательной.

Питер Кордес
источник
Возможно, это может быть проще и / или более идиоматично, или, по крайней мере, более компактно. IDK, если движкам JS удается устранить / оптимизировать большую часть копирования или инициализации нуля, или если это поможет использовать циклы вместо методов TypedArray, например, для копирования в обратном направлении вместо reverse + .set. Я не удосужился проверить скорость; если кто-то захочет это сделать, мне будет интересно.
Питер Кордес
Согласно тестированию @ Salman, этот ответ выглядит как дерьмо для маленьких массивов (из ~ 1000 элементов, 10% из которых равны 0). Предположительно все создание новых массивов вредит. Или, может быть, небрежное смешивание TypedArray и обычного? Разделение на месте внутри TypedArray на сортировку с нулевым и ненулевым + подмассивом может быть намного лучше, как предлагается во 2-м разделе моего ответа.
Питер Кордес
8

Просто измените условие вашей функции сравнения следующим образом:

let arr = [-1, 0, 1, 0, 2, -2, 0, 3, -3, 0, 4, -4, 0, 5, -5];
arr.sort((a, b) => {
   if(a && b) return b-a;
   if(!a && !b) return 0;
   return !a ? -1 : 1;
});

console.log(arr);

Харунур Рашид
источник
6
Это не является последовательным компаратором, если любой из входных данных может быть отрицательным; согласно вашему компаратору, 0 сортирует до 1, сортирует до -1, а сортирует до 0.
Ильмари
Обновлено с отрицательными входами
Харунур Рашид
@SalmanA, если оба равны нулю, условие !aвсе еще выполняется. Это вернется-1
Харунур Рашид
Я не думаю, что это большое дело, a и b оба равны нулю @SalmanA
Харунур Рашид
1
Чтобы быть точным, я отредактировал ответ. Просто добавил условие дляa=b=0
Харунур Рашид
6

Не играю в гольф-код здесь:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5, -1];
arr.sort(function(a, b) {
  if (a === 0 && b !== 0) {
    // a is zero b is nonzero, a goes first
    return -1;
  } else if (a !== 0 && b === 0) {
    // a is nonzero b is zero, b goes first
    return 1;
  } else {
    // both are zero or both are nonzero, sort descending
    return b - a;
  }
});
console.log(arr.toString());

Салман А
источник
5

Не пишите свою собственную числовую сортировку, если она уже существует. То, что вы хотите сделать, это именно то, что вы говорите в названии; сортировать числа в порядке убывания, кроме нулей в начале.

const zeroSort = arr => [...arr.filter(n => n == 0),
                         ...new Float64Array(arr.filter(n => n != 0)).sort().reverse()];

console.log(zeroSort([0, 1, 0, 2, 0, 3, 0, 4, 0, 500]));

Не пишите код, который вам не нужен; Вы можете ошибаться.

Выберите TypedArray в зависимости от того, какой тип чисел вы хотите обработать. Float64 является хорошим значением по умолчанию, поскольку он обрабатывает все обычные числа JS.

JollyJoker
источник
@IlmariKaronen Лучше?
JollyJoker
Вам не нужно фильтровать дважды; как показывает мой ответ, вы можете отфильтровать один раз и вычесть длины, чтобы получить число для Array(n).fill(0).
Питер Кордес
@PeterCordes Хотя это можно было бы назвать более простым, я думаю, что код становится достаточно длинным, чтобы его было
труднее
Да, для эффективности потребуется 1 дополнительная строка для переменной tmp. В моем ответе используется несколько дополнительных строк, потому что я привык к C и C ++ и языку ассемблера, а наличие большего количества отдельных строк облегчает трассировку asm, сгенерированного компилятором, обратно в оператор, отвечающий за него при оптимизации / профилировании. Кроме того, я обычно не делаю JS, поэтому я не чувствовал необходимости втиснуть все это в пару строк. Но вы могли бы сделать let f64arr = new Float64Array(arr.filter(n => n != 0)), то [ ...Array(arr.length - f64arr.length).fill(0),... так что это добавляет 1 дополнительную строку и упрощает последнюю строку.
Питер Кордес
4

Вы можете сделать это так:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

let result = arr.sort((a,b) => {
  if(a == 0 || b == 0)
    return a-b;
  return b-a;
})
console.log(result)

или вы можете сделать это:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

let result = arr.sort().sort((a,b) => {
  if(a > 0 && b > 0)
    return b-a
  return 0
})

console.log(result)

NuOne
источник
4
Ваше первое решение имеет ту же проблему согласованности с отрицательными данными, что и ответ Харунура Рашида. Второй является последовательным, но лечит все отрицательные числа , как равный нулю, в результате чего их взаимное порядок сортировки не определен.
Илмари
-2

const myArray = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];
const splitArray = myArray.reduce((output, item) => {
     if(!item) output[0].push(item);
    else output[1].push(item);
    return output;
}, [[], []]);
const result = [...splitArray[0], ...splitArray[1].reverse()]
console.log(result);

Максимум
источник