Nos algoritmos de divisão e conquista, como quicksort e mergesort, a entrada é geralmente (pelo menos nos textos introdutórios) dividida em dois , e os dois conjuntos de dados menores são tratados recursivamente. Faz sentido para mim que isso acelere a solução de um problema se as duas metades demorarem menos da metade do trabalho de lidar com todo o conjunto de dados. Mas por que não dividir o conjunto de dados em três partes? Quatro? n ?
Eu acho que o trabalho de dividir os dados em muitos subconjuntos faz com que não valha a pena, mas me falta a intuição de ver que alguém deve parar em dois subconjuntos.
Também vi muitas referências ao quicksort de três vias. Quando isso é mais rápido? O que é usado na prática?
Respostas:
Essa não é a essência dos algoritmos de dividir e conquistar. Normalmente, o ponto é que os algoritmos não podem "lidar com todo o conjunto de dados". Em vez disso, é dividido em partes que são triviais para resolver (como classificar dois números), depois são resolvidas trivialmente e os resultados são recombinados de maneira a produzir uma solução para o conjunto de dados completo.
Principalmente porque dividi-lo em mais de duas partes e recombinar mais de dois resultados resulta em uma implementação mais complexa, mas não altera a característica fundamental (Big O) do algoritmo - a diferença é um fator constante e pode resultar em uma desaceleração se a divisão e recombinação de mais de 2 subconjuntos criar sobrecarga adicional.
Por exemplo, se você faz uma classificação de mesclagem de 3 vias, na fase de recombinação, agora você precisa encontrar o maior dos 3 elementos para cada elemento, o que requer 2 comparações em vez de 1, para que você faça o dobro da comparação geral . Em troca, você reduz a profundidade da recursão em um fator de ln (2) / ln (3) == 0,63, para que você tenha 37% menos swaps, mas 2 * 0,63 == 26% mais comparações (e acessos à memória). Se isso é bom ou ruim, depende do que é mais caro no seu hardware.
Aparentemente, pode-se provar que uma variante de pivô duplo do quicksort exige o mesmo número de comparações, mas em média 20% menos swaps, por isso é um ganho líquido.
Atualmente, quase ninguém mais programa seus próprios algoritmos de classificação; eles usam um fornecido por uma biblioteca. Por exemplo, a API Java 7 realmente usa o quicksort de pivô duplo.
As pessoas que realmente programam seu próprio algoritmo de classificação por algum motivo tendem a se ater à simples variante bidirecional, porque menos potencial de erros supera o desempenho 20% melhor na maioria das vezes. Lembre-se: de longe, a melhoria de desempenho mais importante é quando o código passa de "não está funcionando" para "funcionando".
fonte
Assintoticamente falando, isso não importa. Por exemplo, a pesquisa binária faz aproximadamente log 2 n comparações e a pesquisa ternária faz aproximadamente log 3 n comparações. Se você conhece seus logaritmos, sabe que log a x = log b x / log b a, portanto, a pesquisa binária gera apenas 1 / log 3 2 ≈ 1,5 vezes mais comparações que a pesquisa ternária. Essa também é a razão pela qual ninguém nunca especifica a base do logaritmo em grande notação Oh: é sempre um fator constante longe do logaritmo em uma determinada base, independentemente da base real. Portanto, dividir o problema em mais subconjuntos não melhora a complexidade do tempo e praticamente não é suficiente para compensar a lógica mais complexa. De fato, essa complexidade pode afetar negativamente o desempenho prático, aumentando a pressão do cache ou tornando as micro-otimizações menos inviáveis.
Por outro lado, alguma estrutura de dados de árvore usa um alto fator de ramificação (muito maior que 3, geralmente 32 ou mais), embora geralmente por outros motivos. Melhora a utilização da hierarquia de memória: as estruturas de dados armazenadas na RAM fazem melhor uso do cache, as estruturas de dados armazenadas no disco requerem menos leituras HDD-> RAM.
fonte
Existem algoritmos de busca / classificação que subdividem não por dois, mas por N.
Um exemplo simples é a pesquisa por codificação hash, que leva tempo O (1).
Se a função hash estiver preservando a ordem, ela poderá ser usada para criar um algoritmo de classificação O (N). (Você pode pensar em qualquer algoritmo de classificação apenas fazendo N pesquisas para onde um número deve ir no resultado.)
A questão fundamental é que, quando um programa examina alguns dados e entra em alguns estados seguintes, quantos estados seguintes existem e qual a probabilidade das mesmas?
Quando um computador faz uma comparação de dois números, digamos, e então salta ou não, se os dois caminhos são igualmente prováveis, o contador de programas "conhece" mais uma informação em cada caminho; portanto, em média, ele "aprendeu" um pouco. Se um problema exigir que M bits sejam aprendidos, o uso de decisões binárias não poderá obter a resposta em menos de M decisões. Assim, por exemplo, procurar um número em uma tabela classificada de tamanho 1024 não pode ser feito em menos de dez decisões binárias, nem que seja porque menos não teria resultados suficientes, mas certamente pode ser feito em mais do que isso.
Quando um computador pega um número e o transforma em um índice em uma matriz, ele "aprende" até registrar a base 2 do número de elementos na matriz e faz isso em tempo constante. Por exemplo, se houver uma tabela de salto de 1024 entradas, todas mais ou menos igualmente prováveis, então pular nessa tabela "aprende" 10 bits. Esse é o truque fundamental por trás da codificação de hash. Um exemplo de classificação disso é como você pode classificar um baralho de cartas. Tenha 52 caixas, uma para cada cartão. Jogue cada cartão em sua lixeira e pegue todos eles. Não é necessário subdividir.
fonte
Como se trata de uma divisão geral e conquista, não apenas de classificação, fico surpreso que ninguém tenha trazido o Teorema Mestre.
Em resumo, o tempo de execução dos algoritmos de dividir e conquistar é determinado por duas forças contrárias: o benefício que você obtém ao transformar problemas maiores em pequenos e o preço que você paga para resolver mais problemas. Dependendo dos detalhes do algoritmo, pode ou não ser pago para dividir um problema em mais de duas partes. Se você dividir no mesmo número de subproblemas em cada etapa e souber a complexidade do tempo de combinar os resultados em cada etapa, o Teorema Mestre dirá a complexidade do tempo do algoritmo geral.
O algoritmo de Karatsuba para multiplicação usa uma divisão de três vias e conquista para obter um tempo de execução de O (3 n ^ log_2 3) que supera O (n ^ 2) para o algoritmo de multiplicação comum (n é o número de dígitos no números).
fonte
b
aumento do teorema do mestre exige que oa
aumento seja mais lento para que você tenha uma melhoria em outras divisões.Devido à sua natureza binária, um computador é muito eficiente em dividir as coisas em 2 e não tanto em 3. Você obtém uma divisão em 3 dividindo em 2 primeiro e depois divide uma das partes novamente em 2. Portanto, se você precisar dividir por 2 para obter a sua 3 divisão, você também pode dividir em 2.
fonte