Equivalência Big O para LINQ select

10

Estou tentando determinar se há uma alteração na equivalência Big O de um loop aninhado ao usar uma seleção LINQ.

public void myFunc(List<Foo> fooList, List<Bar> barList)
{
    foreach(Foo foo in fooList)
    {
        foreach(Bar bar in barList)
        {
            if(foo.PropA == bar.PropA && bar.PropZ.HasValue)
                foo.PropC = foo.PropB * bar.PropZ;
        }
    }
}

Acredito que este exemplo de loop aninhado seja O (n ^ 2) por complexidade.

Substituí o loop aninhado por um LINQ selecione assim:

public void myFunc(List<Foo> fooList, List<Bar> barList)
{
    foreach(Foo foo in fooList)
    {
        Bar bar = (from b in barList
                    where foo.PropA == b.PropA
                    select b).FirstOrDefault();

        if(bar.PropZ.HasValue)
            foo.PropC = foo.PropB * bar.PropZ;
    }
}

Se nada mais, o código parece ser um pouco mais limpo de ler, uma vez que afirma explicitamente "estamos procurando este particular Barpara trabalhar".

Minha pergunta é a seguinte : O uso do LINQ select reduz a complexidade do Big O?


fonte
3
Seria interessante comparar o IL produzido pelo compilador para cada um deles.
Dan Pichelman
@DanPichelman - Pare de nerd-sniping me! Sim, concordo que seria interessante descobrir. Eles são equivalentes ou não?
Pode economizar algum tempo para repetir se barfiltrar bar.PropZ.HasValueprimeiro, se você espera que mais do que uma pequena quantia seja avaliada como falsa? Realmente não responde à sua pergunta, estou apenas revisando seu código.
1
Esses dois loops estão fazendo a mesma coisa, principalmente no caso em que foo.PropA == bar.PropAé verdadeiro para várias entradas barList? Edit: definitivamente não, pois o segundo lançará a NullReferenceExceptionquando o select retornar null.
Philip Kendall
1
Eu acho que isso .FirstOrDefault()fará com que o loop linq saia cedo se uma correspondência for encontrada, enquanto seus loops aninhados mudos não saem mais cedo, então sim, eu acho que o linq terá um melhor oh grande. Mas não tenho certeza, portanto, um comentário e não uma resposta.
Mike Nakis

Respostas:

14

Big O é o mesmo, pois os dois códigos (no pior caso) terão que realizar uma comparação para cada combinação de itens das duas listas. Por exemplo, se não houver correspondências entre as listas, nenhuma otimização na implementação do Linq evitará a necessidade de verificar todos os itens.

Mas ei, você realmente não quer saber sobre big-O, não é? Você quer saber se o segundo é mais rápido . A resposta é sim, a solução baseada em linq é mais rápida (desde que seja grande o suficiente) porque ela só precisa executar a verificação até que a primeira correspondência na lista seja encontrada, enquanto a solução aninhada sempre precisa visitar todos os itens . O Wheremétodo não varre a lista inteira imediatamente, mas o avaliador cria um iterador lento. O FirstOrDefaultmétodo então executa o iterador até a primeira correspondência ser encontrada, o que significa que, no caso comum, não é necessário percorrer a lista inteira. É claro que há alguma sobrecarga na solução Linq, portanto, se n for baixo o suficiente, o loop for aninhado será mais rápido.

Você mesmo pode conferir a fonte: https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Enumerable.cs

(Mas se você adicionasse um breakno primeiro exemplo, os dois códigos seriam semanticamente equivalentes e o primeiro seria mais rápido devido a menos sobrecarga.)

JacquesB
fonte
"a solução baseada em linq é mais rápida" Quem sabe? O LINQ possui uma sobrecarga de fator constante que pode facilmente exceder 2x.
usr
@usr: Você está certo, depende da frequência das correspondências e não do tamanho das listas. Quanto maior a frequência de correspondências, maior a vantagem relativa da solução baseada em linq.
JacquesB
5

Não há diferença na complexidade, ambos são O (n²) porque Onde é O (n) de Linq.
O uso de FirstOrDefault é equivalente a fazer isso:

if(foo.PropA == bar.PropA && bar.PropZ.HasValue) 
{
    foo.PropC = foo.PropB * bar.PropZ;
    break;
}

Isso é uma melhoria no tempo de cálculo, mas não na complexidade.

Você pode fazer melhor usando um Hashmap para uma das duas listas.
O operador Join do Linq criará um Hashmap para uma das 2 listas, para que você ganhe desempenho ao pesquisar dois elementos correspondentes:

    public void myFunc(List<Foo> fooList, List<Bar> barList)
    {
        var rows = fooList.Join(
                        barList, 
                        f => f.PropA, 
                        b => b.PropA, 
                        (f, b) => new { Foo = f, Bar = b }
                   );

        foreach (var row in rows)
        {
            if (row.Bar.PropZ.HasValue)
                row.Foo.PropC = row.Foo.PropB * row.Bar.PropZ.Value;
        }
    }

Isso deve ser executado em O (n log (n))

Cyril Gandon
fonte
Você poderia explicar por que ele executaria O (n log (n))? Após o código-fonte, parece criar Lookup(Of TKey, TElement), iterar barList, obter o Groupingpara cada item e adicionar itens ao Grouping. Em seguida, itera fooList, obtém o Groupinge retorna cada elemento do agrupamento. De onde vem o `log (n)?
Zach Mierzejewski