Estou procurando uma maneira de testar se uma determinada string se repete ou não para a string inteira.
Exemplos:
[
'0045662100456621004566210045662100456621', # '00456621'
'0072992700729927007299270072992700729927', # '00729927'
'001443001443001443001443001443001443001443', # '001443'
'037037037037037037037037037037037037037037037', # '037'
'047619047619047619047619047619047619047619', # '047619'
'002457002457002457002457002457002457002457', # '002457'
'001221001221001221001221001221001221001221', # '001221'
'001230012300123001230012300123001230012300123', # '00123'
'0013947001394700139470013947001394700139470013947', # '0013947'
'001001001001001001001001001001001001001001001001001', # '001'
'001406469760900140646976090014064697609', # '0014064697609'
]
são cordas que se repetem, e
[
'004608294930875576036866359447',
'00469483568075117370892018779342723',
'004739336492890995260663507109',
'001508295625942684766214177978883861236802413273',
'007518796992481203',
'0071942446043165467625899280575539568345323741',
'0434782608695652173913',
'0344827586206896551724137931',
'002481389578163771712158808933',
'002932551319648093841642228739',
'0035587188612099644128113879',
'003484320557491289198606271777',
'00115074798619102416570771',
]
são exemplos daqueles que não o fazem.
As seções repetidas das strings que me são dadas podem ser bastante longas, e as strings em si podem ter 500 ou mais caracteres, então percorrer cada caractere tentando criar um padrão e verificar o padrão versus o restante da string parece muito lento. Multiplique isso por potencialmente centenas de strings e não vejo nenhuma solução intuitiva.
Analisei um pouco as regexes e elas parecem boas quando você sabe o que está procurando ou pelo menos a duração do padrão que está procurando. Infelizmente, eu também não conheço.
Como posso saber se uma sequência está se repetindo e se for, qual é a subsequência de repetição mais curta?
Respostas:
Aqui está uma solução concisa que evita expressões regulares e loops in-Python lentos:
Veja a resposta do Community Wiki iniciada por @davidism para obter resultados de benchmark. Em suma,
(Palavras dessa resposta, não minhas.)
Isso se baseia na observação de que uma string é periódica se, e somente se, é igual a uma rotação não trivial de si mesma. Parabéns a @AleksiTorhamo por perceber que podemos recuperar o período principal do índice da primeira ocorrência de
s
in(s+s)[1:-1]
e por me informar sobre os argumentosstart
eend
argumentos opcionais do Pythonstring.find
.fonte
.find()
ou em.index()
vez dein
, por exemplo.(s+s).find(s, 1, -1)
.(s+s).find(s, 1, -1)
que será (um pouco) mais rápido do que(s+s)[1:-1].find(s)
, pelo menos para seqüências maiores, já que o fatiamento significa que você precisa criar outra cópia (quase) de toda a sequência."abcd"
o caractere à direita e coloque-o novamente à esquerda para obter"dabc"
. Esse procedimento é chamado de rotação de uma string para a direita por 1 caractere . Repita asn
vezes para girar uma seqüência de caracteres para a direitan
. Agora observe que, se tivermos uma sequência dek
caracteres, girar para a direita por qualquer múltiplo dek
deixa a sequência inalterada. Uma rotação não trivial de uma sequência é aquela cujo número de caractere não é um múltiplo do comprimento da sequência.Aqui está uma solução usando expressões regulares.
Iterando sobre os exemplos na pergunta:
... produz esta saída:
A expressão regular
(.+?)\1+$
é dividida em três partes:(.+?)
é um grupo correspondente que contém pelo menos um (mas o mínimo possível) de qualquer caractere (porque+?
não é ganancioso ).\1+
verifica pelo menos uma repetição do grupo correspondente na primeira parte.$
verifica o final da sequência para garantir que não haja conteúdo extra e não repetitivo após as substrings repetidas (e usarre.match()
garante que não haja texto não repetitivo antes das substrings repetidas).No Python 3.4 e posterior, você pode largar o
$
e usarre.fullmatch()
, ou (em qualquer Python, pelo menos, já em 2.3), seguir o outro caminho e usarre.search()
com o regex^(.+?)\1+$
, que são mais de gosto pessoal do que qualquer outra coisa.fonte
Você pode observar que, para que uma sequência seja considerada repetida, seu comprimento deve ser divisível pelo comprimento de sua sequência repetida. Dado que, aqui está uma solução que gera divisores de comprimento a partir
1
den / 2
inclusiva, divide a string original em substrings com o comprimento dos divisores, e testa a igualdade do conjunto de resultados:EDIT: No Python 3, o
/
operador mudou para fazer a divisão de flutuação por padrão. Para obter aint
divisão do Python 2, você pode usar o//
operador. Obrigado a @ TigerhawkT3 por trazer isso à minha atenção.O
//
operador executa a divisão inteira no Python 2 e Python 3, por isso atualizei a resposta para suportar as duas versões. A parte em que testamos para ver se todas as substrings são iguais agora é uma operação de curto-circuito usandoall
e uma expressão geradora.ATUALIZAÇÃO: em resposta a uma alteração na pergunta original, o código agora foi atualizado para retornar a menor substring de repetição, se existir e
None
se não existir . A @godlygeek sugeriu o usodivmod
para reduzir o número de iterações nodivisors
gerador, e o código foi atualizado para corresponder a isso também. Agora ele retorna todos os divisores positivos den
em ordem crescente, exclusivos den
si mesmos.Atualização adicional para alto desempenho: Após vários testes, cheguei à conclusão de que simplesmente testar a igualdade de cadeias tem o melhor desempenho de qualquer solução de fatiamento ou iterador no Python. Portanto, peguei uma folha do livro de @ TigerhawkT3 e atualizei minha solução. Agora é 6x mais rápido do que antes, notavelmente mais rápido que a solução da Tigerhawk, mas mais lento que a de David.
fonte
(n/2)
inclusivo.n / 2
emdivisors()
sern // 2
?Aqui estão alguns parâmetros de referência para as várias respostas a esta pergunta. Houve alguns resultados surpreendentes, incluindo um desempenho totalmente diferente, dependendo da string sendo testada.
Algumas funções foram modificadas para funcionar com o Python 3 (principalmente substituindo
/
por//
para garantir a divisão inteira). Se você vir algo errado, deseja adicionar sua função ou adicionar outra sequência de teste, execute ping em @ZeroPiraeus na sala de chat do Python .Em resumo: há uma diferença de 50x entre as soluções com melhor e pior desempenho para o grande conjunto de dados de exemplo fornecidos pelo OP aqui (através deste comentário). A solução de David Zhang é a vencedora, superando todas as outras em cerca de 5x no grande conjunto de exemplos.
Algumas das respostas são muito lentas em casos extremamente grandes "sem correspondência". Caso contrário, as funções parecem ter vencedores igualmente iguais ou claros, dependendo do teste.
Aqui estão os resultados, incluindo gráficos feitos com matplotlib e seaborn para mostrar as diferentes distribuições:
Corpus 1 (exemplos fornecidos - conjunto pequeno)
Corpus 2 (exemplos fornecidos - conjunto grande)
Corpus 3 (casos extremos)
Os testes e resultados brutos estão disponíveis aqui .
fonte
Solução não regex:
Solução não-regex mais rápida, graças a @ThatWeirdo (veja comentários):
A solução acima raramente é mais lenta que a original em alguns por cento, mas geralmente é um pouco mais rápida - às vezes muito mais rápida. Ainda não é mais rápido que o davidism para strings mais longos, e a solução regex do zero é superior para strings curtos. Ele é o mais rápido (de acordo com o teste do davidism no github - veja a resposta dele) com seqüências de caracteres de 1000 a 1500 caracteres. Independentemente disso, é confiavelmente o segundo mais rápido (ou melhor) em todos os casos que testei. Obrigado, ThatWeirdo.
Teste:
Resultados:
fonte
repeat('aa')
retornaNone
len(string[0:i])
é sempre igual ai
(neste caso, pelo menos). Substituí-los e também salvarlen(string)
estring[0:i]
variáveis pode acelerar as coisas. Também IMO isso é uma grande solução, impressionante;)Primeiro, corte pela metade a corda, contanto que seja uma duplicata em "2 partes". Isso reduz o espaço de pesquisa se houver um número par de repetições. Em seguida, trabalhando para a frente para encontrar a menor sequência repetida, verifique se a divisão da sequência completa por sub-sequência cada vez maior resulta apenas em valores vazios. Apenas sub-strings
length // 2
precisam ser testadas, pois qualquer coisa acima disso não teria repetições.Isso retorna a correspondência mais curta ou Nenhuma, se não houver correspondência.
fonte
O problema também pode ser resolvido
O(n)
na pior das hipóteses com a função prefixo.Observe que, em geral, pode ser mais lento (UPD: e muito mais lento) do que outras soluções que dependem do número de divisores de
n
, mas geralmente encontram falhas mais cedo, acho que um dos casos ruins para eles seráaaa....aab
, onde existemn - 1 = 2 * 3 * 5 * 7 ... *p_n - 1
a
'sAntes de tudo, você precisa calcular a função de prefixo
então, ou não há resposta ou o menor período é
e você só precisa verificar se
k != n and n % k == 0
(se ak != n and n % k == 0
resposta fors[:k]
sim, então não há respostaVocê pode verificar a prova aqui (em russo, mas o tradutor on-line provavelmente fará o truque)
fonte
prefix_function()
não é Python válida: você tem que faltam dois pontos em seuswhile
eif
declarações, e&&
em vez deand
. Depois de consertá-los, ele falha porUnboundLocalError: local variable 'i' referenced before assignment
causa da linhafor i in range(i, n):
.prefix_function()
para retornar resultados semelhantes às outras respostas - a substring mais curta ouNone
- eu a incluirei em uma referência revisada que estou montando.Esta versão tenta apenas os comprimentos de sequência candidatos que são fatores do comprimento da sequência; e usa o
*
operador para criar uma sequência completa a partir da sequência candidata:Agradecemos a TigerhawkT3 por perceber que,
length // 2
sem,+ 1
isso não corresponderia aoabab
caso.fonte
range
limite delength//2
, exatamente como eu tive - você deve mudar isso paralength//2+1
se quiser pegar as cordas que são exatamente duplicadas (por exemplo'aabaab'
).Aqui está uma solução direta, sem regexes.
Para substrings de
s
partida do índice zeroth, de comprimentos 1 alen(s)
, verifique se essa substringsubstr
é o padrão repetido. Essa verificação pode ser realizada concatenandosubstr
-se osratio
tempos, de modo que o comprimento da cadeia assim formada seja igual ao comprimento des
. Por issoratio=len(s)/len(substr)
.Retornar quando a primeira substring for encontrada. Isso forneceria a menor substring possível, se houver.
fonte
Comecei com mais de oito soluções para esse problema. Alguns eram baseados em regex (match, findall, split), outros em fatiamento e teste de strings e outros com métodos de strings (find, count, split). Cada um tinha benefícios em termos de clareza, tamanho, velocidade e consumo de memória. Eu ia postar minha resposta aqui quando percebi que a velocidade de execução era classificada como importante, então fiz mais testes e melhorias para chegar a isso:
Esta resposta parece semelhante a algumas outras respostas aqui, mas possui algumas otimizações de velocidade que outros não usaram:
xrange
é um pouco mais rápido nesta aplicação,s[:n]
diretamente, evitamos criar uma variável em cada loop.Eu estaria interessado em ver como isso funciona nos testes padrão com hardware comum. Acredito que será muito aquém do excelente algoritmo de David Zhang na maioria dos testes, mas, caso contrário, deve ser bastante rápido.
Eu achei esse problema muito contra-intuitivo. As soluções que pensei que seriam rápidas eram lentas. As soluções que pareciam lentas eram rápidas! Parece que a criação de strings do Python com o operador multiply e as comparações de strings são altamente otimizadas.
fonte
statistics
módulo), então tive que alterar seus/
s para//
s e substituaxrange()
porrange()
(que se comporta como 2.xxrange()
em 3.x).//
em 3.x é divisão inteira (assim como o comportamento 2.x de/
), enquanto 3.x/
é divisão flutuante (que eu tenho certeza que seria muito mais lenta, mesmo que não quebre sua solução, causando uma tentativa de usar um flutuador como um índice). Como mencionado, 3.xrange()
é a mesma coisa que 2.xxrange()
; não existe equivalente a 2.xrange()
no 3.x. Portanto, não acho que essa seja a causa de qualquer discrepância entre o benchmark e os tempos que você fez. Provavelmente, o 3.x é mais lento do que o 2.x (ou talvez sua máquina seja mais rápida que a minha).Essa função é executada muito rapidamente (testada e é três vezes mais rápida que a solução mais rápida aqui em strings com mais de 100k caracteres e a diferença fica maior quanto mais tempo o padrão de repetição). Ele tenta minimizar o número de comparações necessárias para obter a resposta:
Observe que, por exemplo, para a sequência de comprimento 8, ele verifica apenas fragmentos de tamanho 4 e não precisa mais testar porque o padrão de comprimento 1 ou 2 resultaria na repetição do padrão de comprimento 4:
fonte
Na resposta de David Zhang, se temos algum tipo de buffer circular Isso não vai funcionar:
principal_period('6210045662100456621004566210045662100456621')
devido à partida621
, onde eu teria gostado para cuspir:00456621
.Estendendo sua solução, podemos usar o seguinte:
fonte
Aqui está o código em python que verifica a repetição da sub string na string principal fornecida pelo usuário .
Entrada :
Saída :
Entrada :
Saída :
fonte