Flashsort - Flashsort
Flashsort é um algoritmo de classificação de distribuição que mostra a complexidade computacional linear O ( n ) para conjuntos de dados uniformemente distribuídos e relativamente poucos requisitos de memória adicional. O trabalho original foi publicado em 1998 por Karl-Dietrich Neubert.
Conceito
Flashsort é uma implementação local eficiente de classificação de histograma , ela própria um tipo de classificação de bucket . Ele atribui cada um dos n elementos de entrada a um dos m baldes , reorganiza com eficiência a entrada para colocar os baldes na ordem correta e, a seguir, classifica cada balde. O algoritmo original classifica uma matriz de entrada A da seguinte maneira:
- Usando uma primeira passagem sobre a entrada ou conhecimento a priori , encontre as chaves de classificação mínima e máxima.
- Divida linearmente o intervalo [ A min , A max ] em m intervalos .
- Faça uma passagem sobre a entrada, contando o número de elementos A i que caem em cada segmento. (Neubert chama os buckets de "classes" e a atribuição de elementos a seus buckets de "classificação".)
- Converta as contagens de elementos em cada segmento em uma soma de prefixo , onde L b é o número de elementos A i no segmento b ou menos. ( L 0 = 0 e L m = n .)
- Reorganize a entrada para todos os elementos de cada intervalo b são armazenados nas posições A i onde L b −1 < i ≤ L b .
- Classifique cada intervalo usando a classificação por inserção .
As etapas 1–3 e 6 são comuns a qualquer classificação de bucket e podem ser aprimoradas usando técnicas genéricas para classificações de bucket. Em particular, o objetivo é que os intervalos sejam aproximadamente do mesmo tamanho ( n / m elementos cada), com o ideal sendo a divisão em m quantis . Embora o algoritmo básico seja uma classificação de interpolação linear , se a distribuição de entrada for conhecida como não uniforme, uma divisão não linear se aproximará mais desse ideal. Da mesma forma, a classificação final pode usar qualquer uma de várias técnicas, incluindo uma classificação flash recursiva.
O que distingue a classificação flash é a etapa 5: um algoritmo O ( n ) eficiente no local para coletar os elementos de cada balde juntos na ordem relativa correta usando apenas m palavras de memória adicional.
Implementação eficiente de memória
A fase de rearranjo Flashsort opera em ciclos . Os elementos começam como "não classificados", depois são movidos para o intervalo correto e considerados "classificados". O procedimento básico é escolher um elemento não classificado, encontrar seu intervalo correto, trocá-lo por um elemento não classificado lá (que deve existir, porque contamos o tamanho de cada intervalo com antecedência), marcá-lo como classificado e, em seguida, repetir com o elemento não classificado recém-trocado. Eventualmente, o elemento é trocado consigo mesmo e o ciclo termina.
Os detalhes são fáceis de entender usando duas variáveis (do tamanho de uma palavra) por segmento. A parte inteligente é a eliminação de uma dessas variáveis, permitindo que o dobro de baldes sejam usados e, portanto, metade do tempo gasto na classificação final de O ( n 2 ) .
Para entendê-lo com duas variáveis por segmento, suponha que haja duas matrizes de m palavras adicionais: K b é o limite superior (fixo) do segmento b (e K 0 = 0 ), enquanto L b é um índice (móvel) no segmento b , então K b −1 ≤ L b ≤ K b .
Nós mantemos a invariante de loop de que cada segmento é dividido por L b em um prefixo não classificado ( A i para K b −1 < i ≤ L b ainda não foi movido para seus intervalos de destino) e um sufixo classificado ( A i para L b < i ≤ K b estão todos no intervalo correto e não serão movidos novamente). Inicialmente L b = K b e todos os elementos não são classificados. Conforme a classificação prossegue, os L b são decrementados até L b = K b −1 para todos b e todos os elementos são classificados no intervalo correto.
Cada rodada começa encontrando o primeiro segmento c classificado de forma incompleta (que tem K c −1 < L c ) e pegando o primeiro elemento não classificado nesse segmento A i, onde i = K c −1 + 1 . (Neubert chama isso de "líder do ciclo".) Copie A i para uma variável temporária t e repita:
- Calcule o intervalo b ao qual t pertence.
- Seja j = L b o local onde t será armazenado.
- Troque t por A j , isto é, armazene t em A j enquanto busca o valor anterior A j deslocado.
- Reduza L b para refletir o fato de que A j agora está classificado corretamente.
- Se j ≠ i , reinicie este loop com o novo t .
- Se j = i , esta rodada termina e encontre um novo primeiro elemento não classificado A i .
- Quando não há mais elementos não classificados, a distribuição em intervalos é concluída.
Quando implementado com duas variáveis por intervalo dessa maneira, a escolha do ponto de partida i de cada rodada é de fato arbitrária; qualquer elemento não classificado pode ser usado como um líder de ciclo. O único requisito é que os líderes de ciclo possam ser encontrados de forma eficiente.
Embora a descrição anterior use K para encontrar os líderes do ciclo, na verdade é possível fazer sem ele, permitindo que todo o array de palavras- m seja eliminado. (Depois que a distribuição for concluída, os limites do intervalo podem ser encontrados em L. )
Suponha que classificamos todos os elementos até i −1 e estamos considerando A i como um potencial novo líder de ciclo. É fácil calcular seu intervalo de destino b . Pela invariante do laço, é classificado se L b < i ≤ K b , e não classificado se i está fora dessa faixa. A primeira desigualdade é fácil de testar, mas a segunda parece exigir o valor K b .
Acontece que a hipótese de indução de que todos os elementos até i −1 são classificados implica que i ≤ K b , portanto, não é necessário testar a segunda desigualdade.
Considere o balde c em que a posição i cai. Ou seja, K c −1 < i ≤ K c . Pela hipótese de indução, todos os elementos abaixo de i , que inclui todos os intervalos até K c −1 < i , são completamente classificados. Ou seja, nenhum elemento que pertença a esses baldes permanece no resto da matriz. Portanto, não é possível que b < c .
O único caso restante é b ≥ c , o que implica K b ≥ K c ≥ i , QED
Incorporando isso, o algoritmo de distribuição flashsort começa com L conforme descrito acima ei = 1 . Em seguida, prossiga:
- Se i > n , a distribuição está completa.
- Dado A i , calcule o intervalo b ao qual ele pertence.
- Se i ≤ L b , então A i não é classificado. Copie para uma variável temporária t e:
- Seja j = L b o local onde t será armazenado.
- Troque t por A j , isto é, armazene t em A j enquanto busca o valor anterior A j deslocado.
- Reduza L b para refletir o fato de que A j agora está classificado corretamente.
- Se j ≠ i , calcule o intervalo b ao qual t pertence e reinicie este loop (interno) com o novo t .
- A i agora está classificado corretamente. Incremente ie reinicie o loop (externo).
Enquanto economiza memória, o Flashsort tem a desvantagem de recalcular o balde para muitos elementos já classificados. Isso já é feito duas vezes por elemento (uma vez durante a fase de contagem de intervalos e uma segunda vez ao mover cada elemento), mas a busca pelo primeiro elemento não classificado requer um terceiro cálculo para a maioria dos elementos. Isso pode ser caro se os intervalos forem atribuídos usando uma fórmula mais complexa do que a interpolação linear simples. Uma variante reduz o número de cálculos de quase 3 n para no máximo 2 n + m - 1 , tomando o último elemento não classificado em um intervalo inacabado como líder do ciclo:
- Mantenha uma variável c identificando o primeiro segmento classificado de forma incompleta. Deixe c = 1 para começar, e quando c > m , a distribuição está completa.
- Seja i = L c . Se i = L c −1 , incremente c e reinicie este loop. ( L 0 = 0. )
- Calcule o intervalo b ao qual A i pertence.
- Se b < c , então L c = K c −1 e terminamos com o intervalo c . Incremente ce reinicie este loop.
- Se b = c , a classificação é trivial. Decremente L c e reinicie este loop.
- Se b > c , então A i não é classificado. Execute o mesmo loop de classificação do caso anterior e reinicie este loop.
A maioria dos elementos tem seus buckets computados apenas duas vezes, exceto para o elemento final em cada bucket, que é usado para detectar a conclusão do bucket seguinte. Uma pequena redução adicional pode ser alcançada mantendo uma contagem de elementos não classificados e parando quando chegar a zero.
Desempenho
Os únicos requisitos extras de memória são o vetor auxiliar L para armazenar os limites do balde e o número constante de outras variáveis usadas. Além disso, cada elemento é movido (por meio de um buffer temporário, portanto, duas operações de movimentação) apenas uma vez. No entanto, essa eficiência de memória tem a desvantagem de que o array é acessado aleatoriamente, portanto, não se pode tirar proveito de um cache de dados menor do que o array inteiro.
Como acontece com todos os tipos de baldes, o desempenho depende criticamente do equilíbrio dos baldes. No caso ideal de um conjunto de dados balanceado, cada intervalo terá aproximadamente o mesmo tamanho. Se o número m de baldes for linear no tamanho de entrada n , cada balde tem um tamanho constante, portanto, classificar um único balde com um algoritmo O ( n 2 ) como classificação por inserção tem complexidade O (1 2 ) = O (1) . O tempo de execução das classificações de inserção finais é, portanto, m ⋅ O (1) = O ( m ) = O ( n ) .
Escolher um valor para m , o número de intervalos, troca o tempo gasto na classificação de elementos (alto m ) e o tempo gasto na etapa de classificação de inserção final (baixo m ). Por exemplo, se m é escolhido proporcional a √ n , então o tempo de execução das classificações de inserção final é, portanto, m ⋅ O ( √ n 2 ) = O ( n 3/2 ) .
Nos piores cenários, onde quase todos os elementos estão em alguns intervalos, a complexidade do algoritmo é limitada pelo desempenho do método de classificação de intervalo final, então degrada para O ( n 2 ) . As variações do algoritmo melhoram o desempenho do pior caso, usando classificações de melhor desempenho, como quicksort ou flashsort recursivo em intervalos que excedem um determinado limite de tamanho.
Para m = 0,1 n com dados aleatórios uniformemente distribuídos, o flashsort é mais rápido do que o heapsort para todos n e mais rápido do que o quicksort para n > 80 . Torna-se duas vezes mais rápido do que a classificação rápida em n = 10000 . Observe que essas medições foram feitas no final da década de 1990, quando as hierarquias de memória eram muito menos dependentes do cache.
Devido à permutação in situ que o flashsort realiza em seu processo de classificação, o flashsort não é estável . Se a estabilidade for necessária, é possível usar uma segunda matriz para que os elementos possam ser classificados sequencialmente. No entanto, neste caso, o algoritmo exigirá O ( n ) memória adicional.
Veja também
- Pesquisa de interpolação , usando a distribuição de itens para pesquisar em vez de classificar