La complexité temporelle de l'ajout de chaîne itérative est-elle réellement O (n ^ 2) ou O (n)?

89

Je travaille sur un problème avec CTCI.

Le troisième problème du chapitre 1 consiste à prendre une chaîne telle que

'Mr John Smith '

et vous demande de remplacer les espaces intermédiaires par %20:

'Mr%20John%20Smith'

L'auteur propose cette solution en Python, en l'appelant O (n):

def urlify(string, length):
    '''function replaces single spaces with %20 and removes trailing spaces'''
    counter = 0
    output = ''
    for char in string:
        counter += 1
        if counter > length:
            return output
        elif char == ' ':
            output = output + '%20'
        elif char != ' ':
            output = output + char
    return output

Ma question:

Je comprends que c'est O (n) en termes de balayage de la chaîne réelle de gauche à droite. Mais les chaînes en Python ne sont-elles pas immuables? Si j'ai une chaîne et que j'y ajoute une autre chaîne avec l' +opérateur, n'alloue-t-elle pas l'espace nécessaire, ne copie-t-elle pas l'original, puis ne copie-t-elle pas la chaîne d'ajout?

Si j'ai une collection de nchaînes chacune de longueur 1, alors cela prend:

1 + 2 + 3 + 4 + 5 + ... + n = n(n+1)/2

ou O (n ^ 2) fois , oui? Ou est-ce que je me trompe sur la façon dont Python gère l'ajout?

Sinon, si vous êtes prêt à m'apprendre à pêcher: comment pourrais-je le découvrir par moi-même? J'ai échoué dans mes tentatives pour rechercher une source officielle sur Google. J'ai trouvé https://wiki.python.org/moin/TimeComplexity mais cela n'a rien sur les chaînes.

user5622964
la source
17
Quelqu'un devrait en parler à l'auteururllib.urlencode
wim
11
@wim C'est censé être un problème de pratique concernant les tableaux et les chaînes
user5622964
3
Le but du livre est d'enseigner les questions d'entrevue, qui vous demandent généralement de réinventer la roue pour voir le processus de pensée de la personne interrogée.
James Wierzba
1
Comme il s'agit de Python, je pense que faire un rtrimet replaceserait plus préféré et dans le ballpark de O(n). La copie de chaînes semble la manière la moins efficace.
OneCricketeer
2
@RNar Pouvez-vous expliquer comment une copie peut prendre un temps constant?
James Wierzba

Réponses:

83

Dans CPython, l'implémentation standard de Python, il existe un détail d'implémentation qui en fait généralement O (n), implémenté dans le code que la boucle d'évaluation de bytecode appelle +ou +=avec deux opérandes de chaîne . Si Python détecte que l'argument de gauche n'a pas d'autres références, il appelle reallocpour tenter d'éviter une copie en redimensionnant la chaîne sur place. Ce n'est pas quelque chose sur lequel vous devriez jamais vous fier, car c'est un détail d'implémentation et parce que si vous devez reallocdéplacer la chaîne fréquemment, les performances se dégradent de toute façon à O (n ^ 2).

Sans les détails d'implémentation étranges, l'algorithme est O (n ^ 2) en raison de la quantité quadratique de copie impliquée. Un code comme celui-ci n'aurait de sens que dans un langage avec des chaînes mutables, comme C ++, et même en C ++ que vous voudriez utiliser +=.

user2357112 prend en charge Monica
la source
2
Je regarde le code que vous avez lié ... il semble qu'une grande partie de ce code nettoie / supprime les pointeurs / références à la chaîne ajoutée, n'est-ce pas? Et puis vers la fin, il effectue _PyString_Resize(&v, new_len)pour allouer la mémoire pour la chaîne concaténée, puis memcpy(PyString_AS_STRING(v) + v_len, PyString_AS_STRING(w), w_len);qui fait la copie. Si le redimensionnement sur place échoue, c'est le cas PyString_Concat(&v, w);(je suppose que cela signifie que la mémoire contiguë à la fin de l'adresse de chaîne d'origine n'est pas libre). Comment cela montre-t-il l'accélération?
user5622964
J'ai manqué d'espace dans mon commentaire précédent, mais ma question est de savoir si je comprends correctement ce code et comment interpréter l'utilisation de la mémoire / les durées d'exécution de ces pièces.
user5622964
1
@ user5622964: Oups, je me suis mal souvenu du détail étrange de l'implémentation. Il n'y a pas de politique de redimensionnement efficace; il appelle reallocet espère le meilleur.
user2357112 prend en charge Monica
Comment ça memcpy(PyString_AS_STRING(v) + v_len, PyString_AS_STRING(w), w_len);marche? Selon cplusplus.com/reference/cstring/memcpy, il a une définition void * memcpy ( void * destination, const void * source, size_t num );et une description: "Copies the values of num bytes from the location pointed to by source directly to the memory block pointed to by destination."le nombre dans ce cas est la taille de la chaîne d'ajout, et la source est l'adresse de la deuxième chaîne, je suppose? Mais alors pourquoi la destination (première chaîne) + len (première chaîne)? Double mémoire?
user5622964
7
@ user5622964: C'est l'arithmétique du pointeur. Si vous voulez comprendre le code source de CPython jusqu'aux détails de l'implémentation étranges, vous devez connaître C.La version super-condensée PyString_AS_STRING(v)est l'adresse des données de la première chaîne, et l'ajout v_lenvous donne l'adresse juste après la chaîne. les données se terminent.
user2357112 prend en charge Monica
40

L'auteur s'appuie sur une optimisation qui se trouve ici, mais qui n'est pas explicitement fiable. strA = strB + strCest généralement O(n), faire la fonction O(n^2). Cependant, il est assez facile de s'assurer que tout le processus est O(n), utilisez un tableau:

output = []
    # ... loop thing
    output.append('%20')
    # ...
    output.append(char)
# ...
return ''.join(output)

En un mot, l' appendopération est amortie O(1) (bien que vous puissiez la rendre forte O(1)en pré-allouant le tableau à la bonne taille), ce qui fait la boucle O(n).

Et puis joinc'est aussi O(n), mais ce n'est pas grave car c'est en dehors de la boucle.

njzk2
la source
Cette réponse est bonne car elle indique comment concaténer des chaînes.
user877329
réponse précise dans le cadre du calcul des temps d'exécution.
Intesar Haider le
25

J'ai trouvé cet extrait de texte sur Python Speed> Utilisez les meilleurs algorithmes et les outils les plus rapides :

La concaténation de chaînes est mieux effectuée avec ''.join(seq)un O(n)processus. En revanche, l'utilisation des opérateurs '+'ou '+='peut entraîner un O(n^2)processus car de nouvelles chaînes peuvent être créées pour chaque étape intermédiaire. L'interpréteur CPython 2.4 atténue quelque peu ce problème; cependant, ''.join(seq)reste la meilleure pratique

OneCricketeer
la source
3

Pour les futurs visiteurs: Puisqu'il s'agit d'une question CTCI, aucune référence à l'apprentissage du package urllib n'est requise ici, en particulier selon OP et le livre, cette question concerne les tableaux et les chaînes.

Voici une solution plus complète, inspirée du pseudo de @ njzk2:

text = 'Mr John Smith'#13 
special_str = '%20'
def URLify(text, text_len, special_str):
    url = [] 
    for i in range(text_len): # O(n)
        if text[i] == ' ': # n-s
            url.append(special_str) # append() is O(1)
        else:
            url.append(text[i]) # O(1)

    print(url)
    return ''.join(url) #O(n)


print(URLify(text, 13, '%20'))
geekidharsh
la source