Réduire la recherche de chaîne répétée de quadratique à plus rapide

Excuses si le titre n'était pas assez précis d'une description.

J'ai un programme qui recherche une chaîne pour les voyelles, mais il le fait par glisser des fenêtres de tailles spécifiques à travers la chaîne. Je veux calculer la densité des voyelles. Faites comme si je ne me souciais pas d'avoir des espaces et des symboles; c'est juste laissé là. La solution évidente que j'ai codée comme suit:

s = "Lorem ipsum dolor sit amet, consectetur adipiscing elit ... consequat."
# assume s continues after the "..."
v = "aeiou"
v_dict = {}
max_win_size = 100
if max_win_size > len(s): max_win_size = len(s)

for i in range(1, max_win_size):
    v_dict[i] = {}
    for j in range(0, i+1):
        v_dict[i][j] = 0 # set counts to zero
    for j in range(1, len(s)-i+1):
        s_slice = s[j:j+i].lower()
        v_count = sum([s_slice.count(c) for c in v])
        v_dict[i][v_count] += 1

Cela me donne finalement la fréquence des voyelles à différentes fenêtres glissantes de 1 à la taille de la chaîne. Ce programme fonctionne comme je le veux, mais le problème est qu'il est très lent. C'est clairement quadratique, et je voudrais améliorer l'efficacité, car à mesure que le texte grossit, le programme prend exponentiellement plus de temps. Le problème est que je ne sais pas comment transformer l'espace problème en un algorithme plus efficace.

Est-ce que quelqu'un a des idées sur la façon de faire cela, disons, loglinear?

Modifier:

J'ai essayé la réponse de Ben Voigt implémentée par cr1msonB1ade. Cela a fonctionné comme annoncé. En outre, je pensais inclure des preuves empiriques dans les revendications de vitesse.

First is the run time for increasing string size. Both functions perform linearly, however the overhead of using pure python to implement it results in a significantly greater coefficient on the linear run time. Run time for increasing string size

La deuxième est le temps d'exécution pour augmenter la taille de la fenêtre. J'ai corrigé la taille de la fenêtre et j'ai utilisé des tailles de fenêtre de plus en plus grandes. Cette fois, le taux quadratique de ma fonction s'est révélé, alors que la fonction de somme cumulative numpy est restée linéaire.

enter image description here

0
Correction de l'absence de max_win_size.
ajouté l'auteur user1502381, source
Je ne vois pas comment max_win_size est utilisé
ajouté l'auteur AdamSkywalker, source
Votre algorithme contient un pythonisme (je suppose) que je ne connais pas. Que signifie s [j: j + i] .lower() ?
ajouté l'auteur RBarryYoung, source
@RBarryYoung: Ressemble à une construction de découpage de tableau, équivalente à substring() dans d'autres langages
ajouté l'auteur Ben Voigt, source
Je suppose que max_win_size devrait être la plage pour la première boucle. Il devrait remplacer len (s).
ajouté l'auteur cr1msonB1ade, source

2 Réponses

Réécrivez l'algorithme en fonction des comptes cumulés.

Le nombre d'occurrences entre la position i et le j (inclus) est juste cumul_count (j) - cumul_count (i-1) , et le calcul nécessaire n'augmente pas avec la taille de la fenêtre.

0
ajouté

En termes de temps d'exécution théorique, vous calculez les valeurs max_win_size * (len (s) -max_win_size + 1) , donc il n'y a aucun moyen d'obtenir ci-dessous o (max_win_size * len (s)) durée d'exécution.

En termes de temps de calcul réel, il y a quelques problèmes avec votre code actuel. Premièrement, il n'y a aucune raison de faire une correspondance de lettres sur chaque chaîne. Vous devez d'abord convertir votre chaîne en une liste TRUE FALSE , puis l'interroger à la place.

Deuxièmement, vous pouvez mettre à jour dynamiquement les sommes lorsque vous déplacez votre fenêtre coulissante. En d'autres termes, vous n'avez qu'à interroger la lettre que vous laissez tomber et celle que vous ajoutez pour obtenir le nombre de voyelles dans la sous-chaîne suivante.

De plus, vous n'avez pas vraiment besoin d'un dictionnaire pour le deuxième niveau de votre structure de données. Vous savez exactement combien de temps la liste est et vous allez indexer avec des valeurs entières, alors utilisez simplement une liste.

Je suis certain qu'il pourrait y avoir d'autres gains d'efficience, mais en les réunissant, nous avons:

s = "Lorem ipsum dolor sit amet, consectetur adipiscing elit ... consequat."
# assume s continues after the "..."
v = list("aeiou")
isVowel = [l in v for l in s]
v_dict = {}
max_win_size = 100
if max_win_size > len(s): max_win_size = len(s)

for i in range(1, max_win_size):
    v_dict[i] = [0,] * (i+1)
    curr_vowels = sum(isVowel[:i])
    v_dict[i][curr_vowels]  += 1
    for curr_pos in range(1, len(s)-i):
        # add next letter position and subtract letter falling out of window
        curr_vowels += isVowel[curr_pos + i] - isVowel[curr_pos - 1]
        v_dict[i][curr_vowels] += 1

EDIT: Utilisation de la méthode cumulative comme recommandé par @Ben:

import numpy as np
s = "Lorem ipsum dolor sit amet, consectetur adipiscing elit ... consequat."
# assume s continues after the "..."
v = list("aeiou")
cumVowels = [0,] + np.cumsum([l in v for l in s])
v_dict = {}
max_win_size = 100
if max_win_size > len(s): max_win_size = len(s)

for i in range(1, max_win_size):
    v_dict[i] = [0,] * (i+1)
    for pos in range(0, len(s)-i):
        v_dict[i][cumVowels[pos + i] - cumVowels[pos]] += 1
0
ajouté
Cette "requête la lettre que vous lâchez et celle que vous ajoutez" est bonne si vous avez besoin d'un seul passage avec une seule taille de fenêtre. Si vous souhaitez essayer plusieurs tailles de fenêtre, le stockage des nombres cumulés sera meilleur, car un seul tableau de résultats cumulés s'applique à toutes les tailles de fenêtre.
ajouté l'auteur Ben Voigt, source
Effectivement. J'ajoute une version éditée avec les comptes cumulatifs maintenant. Merci @Ben!
ajouté l'auteur cr1msonB1ade, source