Executando verificação de segurança...
1

Obrigado por esclarecer a questão, amigo. A tabela deixou bem claro a grandissíma diferença que existe. Esse conhecimento, aparentemente, é uma ponte para quem atravessa todos os dias o laguinho a pé. Parece fácil fazer a travessia, mas depois de um tempo se percebe o quão mais prático e e útil foi ter construído uma ponte enquanto outros se dedicaram apenas a se molhar no laguinho.

Carregando publicação patrocinada...
1

Só pra ilustrar, um exemplo prático em JavaScript. Suponha que eu tenha um array assim:

[
  { nome: 'Fulano', idade: 20 },
  { nome: 'Ciclano', idade: 31 },
  { nome: 'Beltrano', idade: 20 },
  { nome: 'Trajano', idade: 31 },
  { nome: 'João', idade: 42 },
]

E eu queira criar outro array que tem os nomes agrupados por idade:

[
  { idade: 20, nomes: [ 'Fulano', 'Beltrano' ] },
  { idade: 31, nomes: [ 'Ciclano', 'Trajano' ] },
  { idade: 42, nomes: [ 'João' ] }
]

A ideia mais simples é usar um for, e para cada elemento, verifico se a idade já existe no resultado. Se não existir, cria o array de nomes, e se já existir, adiciona nesse array:

// usando for e find
const result = [];
for (const item of dados) {
    // procura pela idade no resultado
    let elemento = result.find(e => e.idade == item.idade);
    if (!elemento) { // se não tem, cria o array
        elemento = { idade: item.idade, nomes: [] };
        result.push(elemento);
    }
    // adiciona o nome
    elemento.nomes.push(item.nome);
}
console.log(result);

Ok, funciona. Mas esse algoritmo tem um porém: para cada item do array dados, eu uso find para verificar se a idade já está no resultado - pois eu tenho que saber se preciso adicionar uma idade que ainda não está lá. Só que a busca em um array é linear, ou seja, find no fundo é outro loop (que está "escondido", pois ele faz isso internamente). Claro que ele interrompe o loop quando encontra o elemento, mas mesmo assim, conforme o array result cresce, fica cada vez mais custoso percorrê-lo para fazer esta busca.

Daria para melhorar isso usando inicialmente um objeto para mapear cada idade para seu respectivo array, assim o custo da busca é diminuído:

let result = {};
for (const item of dados) {
    result[item.idade] ||= { idade: item.idade, nomes: [] };
    result[item.idade].nomes.push(item.nome);
}
result = Object.values(result);
console.log(result);

O operador ||= é usado para atribuir um valor caso ele ainda não esteja definido. Por exemplo, a ||= b é o mesmo que a = a || b (se a estiver definido, usa o valor dele, senão usa o valor de b). No caso, result[item.idade] estará definido somente se a idade já estiver no resultado. Caso não esteja, adiciona-a no objeto, cujo valor será o objeto que contém a idade e o respectivo array de nomes.

Isso é mais rápido que find porque a busca de uma propriedade em objetos é mais rápida (não é linear como no array, não precisa fazer um loop pelos elementos até encontrar a idade).

No final, pegamos apenas os valores do objeto, e o resultado é o array que queríamos.

Fiz um teste usando o Benchmark.js, segue todo o código abaixo:

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;

const dados = [];
// cria 10 mil itens
for (let i = 0; i < 10000; i++) {
    dados.push({ nome: 'Fulano ' + i, idade: i % 80 });
}

suite
.add('for/find', function () {
    const result = [];
    for (const item of dados) {
        let elemento = result.find(e => e.idade == item.idade);
        if (!elemento) {
            elemento = { idade: item.idade, nomes: [] };
            result.push(elemento);
        }
        elemento.nomes.push(item.nome);
    }
})
.add('object', function () {
    let result = {};
    for (const item of dados) {
        result[item.idade] ||= { idade: item.idade, nomes: [] };
        result[item.idade].nomes.push(item.nome);
    }
    result = Object.values(result);
})
.on('complete', function () {
    console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.on('cycle', function (event) {
    console.log(String(event.target));
})
.run({ 'async': true });

Criei um array com 10 mil itens, com idades variando entre 0 e 79. O resultado está abaixo (lembrando que os números são de operações por segundo, ou seja, quanto mais, melhor):

for/find x 1,021 ops/sec ±0.72% (90 runs sampled)
object x 9,144 ops/sec ±1.70% (91 runs sampled)
Fastest is object

Vale lembrar também que os números podem variar de acordo com cada máquina. Mas em geral, o resultado final (qual é mais rápido) não deve variar.

Enfim, usando o objeto deu umas 9 mil operações por segundo, enquanto o for com find deu apenas mil.

Se aumentarmos o intervalo de idades para 0 a 99, a diferença entre eles aumenta:

for/find x 849 ops/sec ±0.73% (88 runs sampled)
object x 8,354 ops/sec ±1.03% (90 runs sampled)
Fastest is object

Se usarmos um intervalo de 0 a 999 (sei que 999 é muito para idade, mas imagine que fosse outro campo, como uma pontuação qualquer, por exemplo), a diferença aumenta ainda mais:

for/find x 95.69 ops/sec ±2.09% (68 runs sampled)
object x 6,275 ops/sec ±1.38% (79 runs sampled)
Fastest is object

Sim, mais de 6 mil operações por segundo, contra menos de 100 (os números estão no formato americano, a vírgula separa os milhares e o ponto é o separador decimal).

Quanto menos idades repetidas, pior fica, porque o array result fica com mais elementos, e find acaba - na média - demorando mais para verificar se uma idade já existe. Já para um objeto o impacto é menor, porque a busca por uma propriedade não é linear (provavelmente usa alguma tabela de hash ou algo similar - cuja busca é geralmente constante - para ser tão rápido).


Mas como eu já disse, para arrays pequenos não faz tanta diferença assim, e em alguns casos o for pode até ser mais rápido. Por exemplo, para um array com 30 itens e idades entre 0 e 29, o for foi mais rápido:

for/find x 879,554 ops/sec ±1.50% (86 runs sampled)
object x 774,850 ops/sec ±2.04% (82 runs sampled)
Fastest is for/find

Isso porque tem o custo extra de criar o objeto e no final criar outro array contendo somente os valores (quando chamamos Object.values). Esse custo só se paga se tivermos mais elementos. E novamente, isso pode variar de acordo com o hardware, mas enfim, na minha máquina o for já ficou mais lento com apenas 50 itens.

E reforço a questão de que isso será imperceptível para poucos arrays pequenos (a questão de 0.1 segundo versus 0.001 segundo, ambos não dá para perceber, apesar de um ter sido 100 vezes mais rápido). Isso só começa a fazer uma diferença notável com muitos arrays grandes, e por isso que muita gente não liga pra isso e acha que é inútil.

Mas é importante saber sim. São várias dessas pequenas ineficiências que quando somadas acabam degradando aos poucos o sistema. Claro que se for um sistema simples e pequeno, de criticidade baixa, e que seguramente trabalha sempre com poucos dados, aí talvez não precise otimizar tanto. Mas é importante saber desses detalhes sim, para não ser pego de surpresa quando os problemas de desempenho começarem.

1

Excelente explicacação!! So queria fazer uma correção, hash table não tem tempo O(LogN) e sim O(1) ou seja é constante. E sim o javascript usa hash table no object e no Map. Quando você passa uma chave para um objeto ele transforma isso no hash que vai apontar diretamente para o enredeço de memoria (simplificando). Ou seja independente no numero de elementos dentro do object voce vai ter sempre o mesmo tempo de acesso.

2

Já corrigi o texto, obrigado!

Mas vale lembrar que "constante" não quer dizer "sempre o mesmo tempo", pois depende de detalhes internos do algoritmo. Por exemplo, conforme a quantidade de itens cresce, pode ter alguma demora adicional para resolver colisões (e o tempo total disso tudo depende dos algoritmos escolhidos), etc.

Mas enfim, O(logn) estava errado mesmo, eu pensei uma coisa e escrevi outra :-)