Czy złożoność czasowa iteracyjnego łańcucha jest faktycznie dołączana O (n ^ 2), czy O (n)?

89

Pracuję nad problemem z CTCI.

Trzeci problem z rozdziału 1 polega na tym, że bierzesz ciąg, taki jak

'Mr John Smith '

i prosi o zastąpienie spacji pośrednich %20:

'Mr%20John%20Smith'

Autor oferuje takie rozwiązanie w Pythonie, nazywając je 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

Moje pytanie:

Rozumiem, że jest to O (n) pod względem przeglądania rzeczywistego ciągu od lewej do prawej. Ale czy łańcuchy w Pythonie nie są niezmienne? Jeśli mam ciąg i dodam do niego kolejny ciąg z +operatorem, czy nie przydziela on niezbędnej przestrzeni, nie kopiuje oryginału, a następnie kopiuje dołączany ciąg?

Jeśli mam kolekcję nciągów o długości 1, to przyjmuje:

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

lub O (n ^ 2) czas , tak? A może mylę się co do tego, jak Python obsługuje dołączanie?

Ewentualnie, gdybyś zechciał nauczyć mnie łowić ryby: jak bym się tego nauczył? Nie udało mi się znaleźć oficjalnego źródła w Google. Znalazłem https://wiki.python.org/moin/TimeComplexity, ale to nie ma nic na łańcuchach.

user5622964
źródło
17
Ktoś powinien powiedzieć autorowi ourllib.urlencode
wim
11
@wim To ma być problem praktyczny dotyczący tablic i ciągów
user5622964
3
Celem tej książki jest nauczenie pytań podczas rozmowy kwalifikacyjnej, które często wymagają ponownego wynalezienia koła, aby zobaczyć proces myślowy rozmówcy.
James Wierzba
1
Ponieważ jest to Python, myślę, że zrobienie rtrimi replacebyłoby bardziej preferowane i na boisku O(n). Kopiowanie po łańcuchach wydaje się najmniej wydajnym sposobem.
OneCricketeer
2
@RNar Czy możesz wyjaśnić, w jaki sposób kopia może trwać bez przerwy?
James Wierzba

Odpowiedzi:

83

W CPythonie, standardowej implementacji Pythona, istnieje szczegół implementacji, który sprawia, że ​​zwykle jest to O (n), zaimplementowane w kodzie, w którym pętla oceny kodu bajtowego wywołuje +lub +=z dwoma operandami łańcuchowymi . Jeśli Python wykryje, że lewy argument nie ma innych odniesień, wywołuje reallocpróbę uniknięcia kopii, zmieniając rozmiar ciągu w miejscu. Nie jest to coś, na czym powinieneś zawsze polegać, ponieważ jest to szczegół implementacji, a ponieważ jeśli w reallockońcu będziesz musiał często przesuwać ciąg, wydajność i tak spada do O (n ^ 2).

Bez dziwnych szczegółów implementacji algorytm ma wartość O (n ^ 2) ze względu na kwadratową wielkość kopiowania. Taki kod miałby sens tylko w języku ze zmiennymi łańcuchami, jak C ++, a nawet w C ++, którego chciałbyś użyć +=.

user2357112 obsługuje Monikę
źródło
2
Patrzę na kod, który podłączyłeś ... wygląda na to, że duża część tego kodu czyści / usuwa wskaźniki / odwołania do dołączanego ciągu, prawda? A następnie pod koniec wykonuje _PyString_Resize(&v, new_len)alokację pamięci dla połączonego ciągu, a następnie memcpy(PyString_AS_STRING(v) + v_len, PyString_AS_STRING(w), w_len);wykonuje kopię. Jeśli zmiana rozmiaru w miejscu nie powiedzie się, tak się dzieje PyString_Concat(&v, w);(zakładam, że oznacza to, że ciągła pamięć na końcu oryginalnego adresu ciągu nie jest wolna). Jak to pokazuje przyspieszenie?
user5622964
W moim poprzednim komentarzu zabrakło miejsca, ale moje pytanie dotyczy tego, czy poprawnie rozumiem ten kod i jak zinterpretować użycie pamięci / środowiska uruchomieniowe tych elementów.
user5622964
1
@ user5622964: Ups, źle zapamiętałem dziwne szczegóły implementacji. Nie ma skutecznych zasad zmiany rozmiaru; po prostu dzwoni realloci ma nadzieję na najlepsze.
user2357112 obsługuje Monikę
Jak to memcpy(PyString_AS_STRING(v) + v_len, PyString_AS_STRING(w), w_len);działa? Według cplusplus.com/reference/cstring/memcpy ma definicjęvoid * memcpy ( void * destination, const void * source, size_t num ); i opis: "Copies the values of num bytes from the location pointed to by source directly to the memory block pointed to by destination."num w tym przypadku jest rozmiarem dołączanego ciągu, a źródło to adres drugiego łańcucha, jak zakładam? Ale dlaczego w takim razie miejsce docelowe (pierwszy ciąg) + len (pierwszy ciąg)? Podwójna pamięć?
user5622964
7
@ user5622964: To jest arytmetyka wskaźnika. Jeśli chcesz zrozumieć kod źródłowy CPythona aż do dziwnych szczegółów implementacji, musisz znać C. Wersja superkondensowana PyString_AS_STRING(v)to adres danych pierwszego ciągu i dodaniev_len daje adres zaraz po ciągu dane się kończą.
user2357112 obsługuje Monikę
40

Autor polega na optymalizacji, która jest tutaj, ale nie jest wyraźnie niezawodna. strA = strB + strCjest zazwyczaj O(n)tworzeniem funkcji O(n^2). Jednak dość łatwo jest upewnić się, że cały proces jest taki sam O(n), użyj tablicy:

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

Krótko mówiąc, appendoperacja jest amortyzowana O(1) (chociaż można ją wzmocnić O(1), wstępnie przydzielając tablicę do odpowiedniego rozmiaru), tworząc pętlę O(n).

A potem joinjest również O(n), ale to jest w porządku, ponieważ jest poza pętlą.

njzk2
źródło
Ta odpowiedź jest dobra, ponieważ mówi, jak łączyć ciągi.
user877329
precyzyjna odpowiedź w kontekście obliczania czasu wykonania.
Intesar Haider
25

Znalazłem ten fragment tekstu w Python Speed> Użyj najlepszych algorytmów i najszybszych narzędzi :

Najlepiej jest wykonywać konkatenację ciągów, w przypadku ''.join(seq)których jest to O(n)proces. W przeciwieństwie do tego, użycie operatorów '+'lub '+='może spowodować powstanie O(n^2)procesu, ponieważ dla każdego kroku pośredniego mogą być budowane nowe łańcuchy. Interpreter CPythona 2.4 nieco łagodzi ten problem; jednak ''.join(seq)pozostaje najlepszą praktyką

OneCricketeer
źródło
3

Dla przyszłych odwiedzających: ponieważ jest to pytanie CTCI, wszelkie odniesienia do uczenia się urllib pakietu nie jest tutaj wymagane, szczególnie zgodnie z OP i książką, to pytanie dotyczy tablic i ciągów.

Oto bardziej kompletne rozwiązanie, zainspirowane pseudo @ 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
źródło