2013-03-14 8 views
18

dieses Stück Code vor:String Verkettung Komplexität in C++ und Java

public String joinWords(String[] words) { 
    String sentence = ""; 
    for(String w : words) { 
     sentence = sentence + w; 
    } 
    return sentence; 
} 

Auf jeder Verkettung eine neue Kopie der Zeichenfolge erstellt wird, so dass die Gesamtkomplexität O(n^2) ist. Glücklicherweise konnten wir dies in Java mit einer StringBuffer lösen, die O(1) Komplexität für jeden Anhang hat, dann wäre die Gesamtkomplexität O(n).

Während in C++ std::string::append() hat Komplexität von O(n), und ich bin nicht klar über die Komplexität von stringstream. In C++ gibt es Methoden wie die in StringBuffer mit der gleichen Komplexität?

+4

Also das ist eine C++ Frage? Warum ist es mit Java getaggt und warum ist das Beispiel in Java? –

+0

Dieses Thema anzeigen? http://StackOverflow.com/Questions/2462951/c-eqivalent-of-stringbuffer-stringbuilder – taocp

+0

Oder dieses: http://StackOverflow.com/Questions/7156122 – phs

Antwort

18

C++ - Zeichenfolgen sind veränderbar und so dynamisch wie ein StringBuffer. Im Gegensatz zu Java würde dieser Code nicht jedes Mal eine neue Zeichenfolge erstellen. Es hängt nur an den aktuellen.

std::string joinWords(std::vector<std::string> const &words) { 
    std::string result; 
    for (auto &word : words) { 
     result += word; 
    } 
    return result; 
} 

Dies läuft in linearer Zeit, wenn Sie die Größe reserve Sie vorher benötigen. Die Frage ist, ob das Schleifen über den Vektor zum Erhalten von Größen langsamer ist als das automatische Ändern der Größe der Zeichenkette. Das kann ich dir nicht sagen. Zeit es. :)

Wenn Sie nicht aus irgendeinem Grund std::string verwenden möchten (und Sie sollten es in Betracht ziehen; es ist eine absolut respektable Klasse), C++ hat auch String-Streams.

#include <sstream> 
... 

std::string joinWords(std::vector<std::string> const &words) { 
    std::ostringstream oss; 
    for (auto &word : words) { 
     oss << word; 
    } 
    return oss.str(); 
} 

Es ist wahrscheinlich std::string nicht effizienter als die Verwendung, aber es ist ein bisschen flexibler in anderen Fällen - Sie fast jede primitive Art mit ihm, sowie jede Art stringify kann, die eine operator <<(ostream&, its_type&) Überschreibung angegeben hat .

+0

Beachten Sie, dass, wenn Ihre 'std :: string' eine Exponentialreservierungsstrategie verwendet, dies auch ohne Reserve linear ist. Wenn es um einen Faktor von 1,5 wächst, wenn es zu klein ist, wird jedes Zeichen im Durchschnitt 1/(1-1/1,5) oder 3 Mal kopiert: und ein konstanter Faktor oder 3 oder 4 bedeutet, dass wir immer noch O (n). Ich bin mir nicht bewusst, ob der Standard diese Strategie vorschreibt. – Yakk

+0

Exponentielle Größenanpassung scheint vernünftig genug zu sein, dass Sie oft eine lineare Zeitleistung sehen können, selbst wenn Sie die Größe automatisch anpassen ... aber ich erinnere mich nicht daran, eine solche Anforderung im Standard zu sehen. (Abgesehen von den Leistungsaspekten ist es jedoch weniger wahrscheinlich, dass die Reservierung der richtigen Größe den Heap fragmentiert.) – cHao

1

Als ein Beispiel für eine sehr einfache Struktur, die O(n) Komplexität in C++ 11 hat:

template<typename TChar> 
struct StringAppender { 
    std::vector<std::basic_string<TChar>> buff; 
    StringAppender& operator+=(std::basic_string<TChar> v) { 
    buff.push_back(std::move(v)); 
    return *this; 
    } 
    explicit operator std::basic_string<TChar>() { 
    std::basic_string<TChar> retval; 
    std::size_t total = 0; 
    for(auto&& s:buff) 
     total+=s.size(); 
    retval.reserve(total+1); 
    for(auto&& s:buff) 
     retval += std::move(s); 
    return retval; 
    } 
}; 

Verwendung:

StringAppender<char> append; 
append += s1; 
append += s2; 
std::string s3 = append; 

Dies geschieht O (n), wobei n die Anzahl von Charakteren.

Schließlich, wenn Sie wissen, wie lang alle Strings sind, macht nur reserve mit genügend Platz append oder += insgesamt O (n) Zeit. Aber ich stimme zu, dass das peinlich ist.

Verwendung von std::move mit dem oben StringAppender (dh sa += std::move(s1)) erhöht sich die Leistung erheblich für nicht-kurze Strings (oder die Verwendung mit XValues ​​etc)

Ich weiß nicht, die Komplexität der std::ostringstream, aber ostringstream ist für Pretty Print formatierte Ausgabe oder Fälle, in denen hohe Leistung nicht wichtig ist. Ich meine, sie sind nicht schlecht, und sie können sogar Skript-/Interpretierungs-/Bytecode-Sprachen ausführen, aber wenn Sie in Eile sind, brauchen Sie etwas anderes.

Wie üblich müssen Sie profilieren, da konstante Faktoren wichtig sind.

Ein rvalue-reference-to-this-Operator + könnte auch ein guter sein, aber nur wenige Compiler implementieren rvalue-Referenzen zu diesem.

+0

Woher kommt das 'log n'? Warum glauben Sie, dass das besser ist, als nur an eine Zeichenkette anzuhängen? Sie kopieren jeden String zweimal (einmal in die Parametervariable von 'operator + =' und einmal in den letzten String). Der Standard-Exponential-Reallokationsalgorithmus, der von (den meisten) std :: string-Implementierungen verwendet wird, kopiert jedes Zeichen durchschnittlich zweimal (gib oder nimm ein bisschen). – rici

+0

Sie haben Recht, kein lg k oder n Faktor. Ich kopiere jedes Zeichen zweimal, nicht jede Zeichenfolge. Ein 1,5-fach exponentielles Realloc kopiert jedes Zeichen durchschnittlich 3 Mal, wenn meine Serviettenmathematik richtig ist. Ich bewege jeden String 4-mal im Durchschnitt (einmal, 1/(1-1/1.5) während Reallocs), aber das beinhaltet 0 Zeichen-Kopien (ignorieren kurze String-Optimierung). Kurz gesagt, die Cachekohärenz könnte leicht + = schlagen Sie mich (seine Kopien sind cache-freundlich): aber wenn Sie mir xvalue Strings füttern, ich denke, ich kann + = Hands-down (1 Kopie pro Char) schlagen. – Yakk

+2

Ich habe nur libcxx (clang) und libstdC++ (gcc) überprüft, und beide verwenden einen exponentiellen Doubling-Algorithmus. Jedes Zeichen wird also zweimal kopiert (ungefähr: ein bisschen mehr, weil die letzte Zeichenkette keine Potenz von 2 ist, sondern ein bisschen weniger, weil die Zwischenstrings verdoppelt werden, bevor die Zeichenkette, die sie über die Kante schiebt, hineinkopiert wird.) – rici

10

Dies ist etwas tangential zu Ihrer Frage, aber dennoch relevant. (Und zu groß für einen Kommentar!)

Bei jeder Verkettung wird eine neue Kopie der Zeichenfolge erstellt, so dass die Gesamtkomplexität O (n^2) ist.

In Java, die Komplexität von s1.concat(s2) oder s1 + s2 ist O(M1 + M2) wo M1 und M2 sind die jeweiligen Streichlängen. Dies in die Komplexität einer Folge von Verkettungen umzuwandeln, ist im Allgemeinen schwierig. Allerdings, wenn Sie annehmen, N Verkettungen von Strings der Länge M, dann ist die Komplexität in der Tat O(M * N^2) was entspricht, was Sie in der Frage gesagt haben.

Zum Glück in Java wir dies mit einem StringBuffer lösen könnte, die für jede append O(1) Komplexität hat, dann ist die Gesamtkomplexität O(n) wäre.

Im StringBuilder Fall amortisieren die Komplexität von N Anrufe sb.append(s) für Streicher von Größe MO(M*N) ist. Das Schlüsselwort hier ist amortisiert. Wenn Sie Zeichen an StringBuilder anhängen, muss die Implementierung möglicherweise das interne Array erweitern. Die Expansionsstrategie besteht jedoch darin, die Größe des Arrays zu verdoppeln. Und wenn Sie die Mathematik machen, werden Sie sehen, dass im Durchschnitt jedes Zeichen im Puffer während der gesamten Folge von append Aufrufen eine zusätzliche Zeit kopiert wird. Die gesamte Sequenz funktioniert also immer noch als O(M*N) ... und so ist M*N die gesamte Stringlänge.

So ist Ihr Endergebnis korrekt, aber Ihre Aussage über die Komplexität eines einzelnen Anrufs zu append ist nicht korrekt. (Ich verstehe, was du meinst, aber so, wie Sie es sagen, ist facially nicht korrekt.)

Schließlich würde ich beachten, dass in Java Sie StringBuilder statt StringBuffer es sei denn, Sie Notwendigkeit der Puffer sein Thread-sicher verwenden sollten .

+1

_Wenn Sie jedoch M Verkettungen von Strings der Länge N annehmen, dann ist die Komplexität tatsächlich O (M * N^2) stimmt mit dem, was Sie in der Frage gesagt haben. Ich sehe es nicht. Beginnen Sie mit der leeren Zeichenfolge und verketten Sie eine Zeichenfolge der Länge N, M-mal. Dann ist die Zeit proportional zu N + (N + N) + ((N + N) + N) + ... = N + 2N + 3N + ... + MN = N · M * (M + 1)/2. Dies ist O (N * (M^2)). – David

+0

Ähm ... ist das nicht was ich gesagt habe? 'O (N * (M^2))' und 'O (M * N^2)' sind dasselbe. (Es sei denn, Sie nahmen an, dass '*' und '^' den gleichen Vorrang hatten ... was die akzeptierte mathematische Konvention verletzt.) –

+0

Ich sage O (N * (M^2)). Sie sagen O (M * (N^2)). Sie sind nicht dasselbe. Sie haben M und N definiert, indem wir sagen, wir "nehmen M Verkettungen von Strings der Länge N an". – David