Classificando números em ordem decrescente, mas com `0`s no início

36

Tenho um desafio em JavaScript que já estou tentando descobrir há um tempo.

Considere esta matriz:

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

Eu tenho que produzir este resultado:

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

Estou seguindo esta linha de lógica para posicionar os zeros na frente, ajustando o valor do índice:

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

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

    return y - x;
});

Mas estou preso neste resultado:

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

Alguém tem alguma dica sobre como resolver isso?

lianbwl
fonte
6
É garantido que não há números negativos?
Michael - Onde está Clay Shirky,
2
Não é corrigido se a última linha for alterada para return x - y;?
achou do
2
Seria eficiente no JavaScript contar e remover zeros, classificar os elementos restantes normalmente? (sem um comparador personalizado, espero que o mecanismo JS possa usar uma função interna de classificação de números). Anexe o número certo de zeros ao resultado. Se houver muitos zeros, removê-los antes da classificação cria um problema menor. Ou troque os zeros para o início da matriz com uma passagem e classifique a cauda da matriz?
Peter Cordes
2
Isso chega a acontecer return y - x;? Mesmo em javascript, não consigo pensar em nada que não fosse nem ===0nem !==0.
George T
2
JSPerf para respostas: jsperf.com/stackoverflow-question-58933996-v2
Salman A

Respostas:

35

Você pode classificar pelo delta de be a(para classificação decrescente) e pegar Number.MAX_VALUE, para valores falsos como zero.

Este:

Number.MAX_VALUE - Number.MAX_VALUE

é igual a zero.

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);

Nina Scholz
fonte
13
Sua função de comparação retornará NaNse ambos ae bforem zero. Esse pode ser um comportamento indesejado.
dan04
20
Os resultados de Array.prototype.sortsão definidos pela implementação se o comparador retornar NaN, portanto, esse comparador é uma má ideia. Ele tenta ser inteligente e erra.
user2357112 suporta Monica
3
E se um elemento dentro da matriz for Infinito? A função de comparação retorna 0 ao comparar um número Infinito a 0.
Marco
4
obrigado a todos. Eu pego o maior número que pode ser subtraído por si só, e espero que esse número não seja usado na matriz do op.
Nina Scholz
4
Absolutamente ilegível. Legal, mas ilegível.
Salman A
24

Como mdn docs diz:

Se aeb são dois elementos comparados, então:

Se compareFunction(a, b)retornar menor que 0, classifique apara um índice menor que b(isto é, a vem primeiro).

Se compareFunction(a, b)retornar 0, deixe aeb inalterados um com o outro, mas classificados com relação a todos os elementos diferentes. Nota: o padrão ECMAscript não garante esse comportamento; portanto, nem todos os navegadores (por exemplo, versões do Mozilla que datam de pelo menos 2003) respeitam isso.

Se compareFunction(a, b) retornar maior que 0, classifique bpara um índice menor que a (isto é, bvem primeiro).

compareFunction(a, b)sempre deve retornar o mesmo valor quando recebe um par específico de elementos ae bcomo seus dois argumentos. Se resultados inconsistentes forem retornados, a ordem de classificação será indefinida.

Portanto, a função de comparação tem o seguinte formato:

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);

Um passo adiante
fonte
3
+1 por fornecer a que parece ser a única solução até agora com uma função de comparação realmente consistente (conforme definido no padrão ) para todas as entradas, incluindo números negativos.
Ilmari Karonen 20/11/19
3
@IlmariKaronen: Infelizmente, a comparação é consistente para números negativos, mas é a comparação errada para números negativos. Os negativos são classificados antes de 0 com esse comparador e são classificados em ordem crescente.
User2357112 suporta Monica
2
@ user2357112supportsMonica No entanto, o OP não especificou o comportamento desejado para números negativos e, com este protótipo, é trivial ajustar o comportamento de números negativos para atender a qualquer requisito que o OP tenha. O que há de bom nessa resposta é que ela é idiomática e usa uma função de comparação padrão para fazer o trabalho.
J ...
13

Se você se preocupa com eficiência, provavelmente é mais rápido filtrar os zeros primeiro . Você não quer sortperder tempo olhando para eles, muito menos adicionando trabalho extra ao seu retorno de chamada de comparação para lidar com esse caso especial.

Especialmente se você espera um número significativo de zeros, uma passagem sobre os dados para filtrá-los deve ser muito melhor do que fazer uma classificação O (N log N) maior que analisará cada zero várias vezes.

Você pode acrescentar com eficiência o número certo de zeros depois de terminar.

Também é fácil ler o código resultante. Usei o TypedArray porque é eficiente e facilita a classificação numérica . Mas você pode usar essa técnica com Array regular, usando o idioma padrão de (a,b)=>a-bfor .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);
*/

Não sei se TypedArray .sort()e, em seguida, .reverseé mais rápido do que usar uma função de comparação personalizada para classificar em ordem decrescente. Ou se pudermos copiar e reverter em tempo real com um iterador.


Também vale a pena considerar: use apenas um TypedArray de todo o comprimento .

Em vez de usar .filter, faça um loop sobre ele e troque os zeros para a frente da matriz à medida que avança. Isso leva uma passagem sobre seus dados.

Em seguida, use .subarray()para obter uma nova exibição TypedArray dos elementos diferentes de zero do mesmo ArrayBuffer subjacente. Classificação que deixará a matriz completa com um início zero e uma cauda classificada, com a classificação apenas olhando para os elementos diferentes de zero.

Não vi uma função de partição nos métodos Array ou TypedArray, mas mal conheço JavaScript. Com um bom JIT, um loop não deve ser muito pior do que um método interno. (Especialmente quando esse método envolve um retorno de chamada .filtere, a menos que ele use reallocsob o capô para diminuir, ele precisa descobrir quanta memória alocar antes de realmente filtrar).

Eu usei Array regular .filter() antes de converter para um TypedArray. Se sua entrada já é um TypedArray, você não tem esse problema, e essa estratégia se torna ainda mais atraente.

Peter Cordes
fonte
Provavelmente, isso pode ser mais simples e / ou mais idiomático, ou pelo menos mais compacto. IDK se os mecanismos JS conseguirem eliminar / otimizar grande parte da cópia ou inicializar com zero, ou se isso ajudaria a usar loops em vez dos métodos TypedArray, por exemplo, copiar para trás em vez de reverse + .set. Não cheguei a testá-lo rapidamente; se alguém quiser fazer isso, eu estaria interessado.
Peter Cordes
De acordo com o teste de @ Salman, essa resposta funciona como uma porcaria para matrizes pequenas (de ~ 1000 elementos, 10% dos quais são 0). Presumivelmente, toda a criação de novas matrizes dói. Ou talvez descuidada mistura de TypedArray e regular? A partição no local dentro de um TypedArray em uma classificação de subarray zero e não zero + pode ser muito melhor, conforme sugerido na segunda seção da minha resposta.
Peter Cordes
8

Apenas modifique a condição da sua função de comparação assim:

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);

Harunur Rashid
fonte
6
Este não é um comparador consistente se alguma das entradas puder ser negativa; de acordo com o seu comparador, 0 tipo antes que classifica uma -1 antes que os tipos antes 0.
Ilmari Karonen
Atualizado com entradas negativas
Harunur Rashid
@ SalmanA, Se ambos forem zero, a condição !aainda é verdadeira. Ele retornará-1
Harunur Rashid 21/11/19
Eu não acho que isso é um grande negócio é um e b ambos são zero @SalmanA
Harunur Rashid
11
Para ser exato, editei a resposta. Acabei de adicionar uma condição paraa=b=0
Harunur Rashid
6

Não está a jogar golfe com códigos aqui:

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());

Salman A
fonte
5

Não escreva sua própria classificação numérica, se ela já existir. O que você quer fazer é exatamente o que você diz no título; classifique os números em ordem decrescente, exceto os zeros no início.

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]));

Não escreva nenhum código que não precise; você pode entender errado.

Escolha o TypedArray com base no tipo de número que você deseja que a matriz manipule. Float64 é um bom padrão, pois lida com todos os números JS normais.

JollyJoker
fonte
@IlmariKaronen Melhor?
JollyJoker #
Você não precisa filtrar duas vezes; como minha resposta mostra, você pode filtrar uma vez e subtrair comprimentos para obter um número Array(n).fill(0).
Peter Cordes
@PeterCordes Embora ele poderia ser chamado mais simples, acho que o código obtém o tempo suficiente para ser menos fácil de ler
jollyjoker
Sim, a eficiência exigiria uma linha extra para uma variável tmp. Minha resposta usa várias linhas extras porque estou acostumada a C e C ++ e linguagem assembly, e ter mais linhas separadas facilita o rastreamento do asm gerado pelo compilador de volta à declaração responsável por ele ao otimizar / criar perfis. Além disso, eu normalmente não faço JS, então não senti necessidade de colocar tudo em algumas linhas. Mas você poderia fazer let f64arr = new Float64Array(arr.filter(n => n != 0)), então [ ...Array(arr.length - f64arr.length).fill(0),... então adiciona 1 linha extra e simplifica a última linha.
Peter Cordes
4

Você pode fazer isso assim:

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)

ou você pode fazer isso:

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
fonte
4
Sua primeira solução tem o mesmo problema de consistência com dados negativos que a resposta de Harunur Rashid. O segundo é consistente, mas trata todos os números negativos como iguais a zero, deixando sua ordem de classificação mútua indefinida.
Ilmari Karonen 20/11/19
-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);

Máx.
fonte