web-dev-qa-db-fra.com

Complexité temporelle d'une tranche de chaîne

Quelle est la complexité temporelle du découpage d'une chaîne Python? Étant donné que Python sont immuables, je peux imaginer qu'elles soient soit O(1) ou O(n) selon la façon dont le découpage est mis en œuvre.

J'ai besoin d'écrire une fonction qui itère sur tous les suffixes d'une chaîne (potentiellement grosse). Je pourrais éviter de trancher la chaîne en représentant un suffixe comme un tuple de la chaîne entière plus un index pour commencer à lire les caractères, mais c'est moche. Si à la place j'écris naïvement ma fonction comme ceci:

def do_something_on_all_suffixes(big_string):
    for i in range(len(big_string)):
        suffix = big_string[i:]
        some_constant_time_operation(suffix)

... sa complexité temporelle sera-t-elle O(n) ou O(n2), où n est len(big_string)?

23
Mark Amery

Réponse courte: str tranches, en général, copie. Cela signifie que votre fonction qui effectue une tranche pour chacun des suffixes n de votre chaîne effectue O(n2) travail. Cela dit, vous pouvez éviter les copies si vous pouvez travailler avec des objets de type bytes en utilisant memoryviews pour obtenir des vues sans copie des données d'octets d'origine . Voir comment vous pouvez effectuer un découpage sans copie ci-dessous pour savoir comment le faire fonctionner.

Réponse longue: (C) Python str ne tranche pas en référençant une vue d'un sous-ensemble des données. Il existe exactement trois modes de fonctionnement pour le découpage str:

  1. Tranche complète, par ex. mystr[:]: Renvoie une référence à la même str (pas seulement les données partagées, le même objet réel, mystr is mystr[:] Puisque str est immuable donc il n'y a pas risque de le faire)
  2. La tranche de longueur nulle et (dépendante de l'implémentation) les tranches de longueur en cache 1; la chaîne vide est un singleton (mystr[1:1] is mystr[2:2] is ''), et les chaînes ordinales basses de longueur un sont également des singletons mis en cache (sur CPython 3.5.0, il ressemble à tous les caractères représentables en latin-1, c'est-à-dire les ordinaux Unicode dans range(256), sont mis en cache)
  3. Toutes les autres tranches: le str tranché est copié au moment de la création, puis sans rapport avec le str d'origine

La raison pour laquelle # 3 est la règle générale est d'éviter les problèmes avec les gros str étant gardés en mémoire par une vue d'une petite partie de celui-ci. Si vous aviez un fichier de 1 Go, lisez-le et découpez-le comme ça (oui, c'est du gaspillage quand vous pouvez le chercher, c'est à titre d'illustration):

with open(myfile) as f:
    data = f.read()[-1024:]

alors vous auriez 1 Go de données en mémoire pour prendre en charge une vue qui montre le 1 Ko final, un sérieux gaspillage. Comme les tranches sont généralement de petite taille, il est presque toujours plus rapide de copier sur la tranche au lieu de créer des vues. Cela signifie également que str peut être plus simple; il a besoin de connaître sa taille, mais il n'a pas besoin de suivre également un décalage dans les données.

Comment vous pouvez effectuer un découpage sans copie

Il y a façons d'effectuer un découpage basé sur la vue en Python, et dans Python 2, cela fonctionnera sur str (car str ressemble à des octets dans Python 2, prenant en charge le protocole tampon ). Avec Py2 str et Py3 bytes (ainsi que de nombreux autres types de données comme bytearray, array.array, numpy tableaux, mmap.mmap S, etc.), vous pouvez créer un memoryview qui est une vue à copie nulle de l'objet d'origine , et peut être découpé sans copier de données. Donc, si vous pouvez utiliser (ou encoder) vers Py2 str/Py3 bytes, et votre fonction peut fonctionner avec des objets arbitraires semblables à bytes, alors vous pourriez faire:

def do_something_on_all_suffixes(big_string):
    # In Py3, may need to encode as latin-1 or the like
    remaining_suffix = memoryview(big_string)
    # Rather than explicit loop, just replace view with one shorter view
    # on each loop
    while remaining_suffix:  # Stop when we've sliced to empty view
        some_constant_time_operation(remaining_suffix)
        remaining_suffix = remaining_suffix[1:]

Les tranches de memoryviews créent de nouveaux objets d'affichage (ils sont juste ultra-légers avec une taille fixe sans rapport avec la quantité de données qu'ils consultent), tout simplement pas de données, donc some_constant_time_operation Peut stocker un copier si nécessaire et il ne sera pas modifié lorsque nous le couperons plus tard. Si vous avez besoin d'une copie appropriée en tant que Py2 str/Py3 bytes, vous pouvez appeler .tobytes() pour obtenir le brut bytes obj, ou (dans Py3 uniquement il apparaît), le décoder directement dans un str qui copie à partir du tampon, par exemple str(remaining_suffix[10:20], 'latin-1').

33
ShadowRanger

Tout dépend de la taille de vos tranches. J'ai jeté ensemble les deux repères suivants. La première tranche la chaîne entière et la seconde seulement un peu. Ajustement de courbe avec cet outil donne

# s[1:-1]
y = 0.09 x^2 + 10.66 x - 3.25

# s[1:1000]
y = -0.15 x + 17.13706461

Le premier semble assez linéaire pour des tranches de chaînes jusqu'à 4 Mo. Je suppose que cela mesure vraiment le temps nécessaire pour construire une deuxième chaîne. Le second est assez constant, bien qu'il soit si rapide qu'il n'est probablement pas aussi stable.

enter image description hereenter image description here

import time

def go(n):
    start = time.time()
    s = "abcd" * n
    for j in xrange(50000):

        #benchmark one
        a = s[1:-1]

        #benchmark two
        a = s[1:1000]

    end = time.time()
    return (end - start) * 1000

for n in range(1000, 100000, 5000):
    print n/1000.0, go(n)
4
jozxyqk