web-dev-qa-db-fra.com

Complexité de concaténation de chaînes en C ++ et Java

Considérez ce morceau de code:

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

À chaque concaténation, une nouvelle copie de la chaîne est créée, de sorte que la complexité globale est O(n^2). Heureusement dans Java nous pourrions résoudre ce problème avec un StringBuffer, qui a O(1) complexité pour chaque ajout, alors la complexité globale serait O(n).

En C++, std::string::append() a la complexité de O(n), et je ne suis pas clair sur la complexité de stringstream.

En C++, existe-t-il des méthodes comme celles de StringBuffer avec la même complexité?

21
ethanjyx

Les chaînes C++ sont modifiables et à peu près aussi dynamiquement dimensionnables qu'un StringBuffer. Contrairement à son équivalent en Java, ce code ne créerait pas une nouvelle chaîne à chaque fois; il s'ajoute simplement à l'actuel.

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

Cela fonctionne en temps linéaire si vous reserve la taille dont vous aurez besoin au préalable. La question est de savoir si une boucle sur le vecteur pour obtenir des tailles serait plus lente que de laisser la chaîne se redimensionner automatiquement. Ça, je ne pourrais pas te le dire. Le chronométrer. :)

Si vous ne voulez pas utiliser std::string Lui-même pour une raison quelconque (et vous devriez le considérer; c'est une classe parfaitement respectable), C++ a également des flux de chaînes.

#include <sstream>
...

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

Ce n'est probablement pas plus efficace que d'utiliser std::string, Mais c'est un peu plus flexible dans d'autres cas - vous pouvez stringifier n'importe quel type primitif avec lui, ainsi que tout type qui a spécifié une fonction operator <<(ostream&, its_type&) passer outre.

23
cHao

Ceci est quelque peu tangentiel à votre question, mais néanmoins pertinent. (Et trop gros pour un commentaire !!)

À chaque concaténation, une nouvelle copie de la chaîne est créée, de sorte que la complexité globale est O (n ^ 2).

En Java, la complexité de s1.concat(s2) ou s1 + s2 Est O(M1 + M2)M1 Et M2 Sont les longueurs de chaîne respectives. Transformer cela en complexité d'une séquence de concaténations est difficile en général. Cependant, si vous supposez N concaténations de chaînes de longueur M, alors la complexité est bien O(M * N ^ 2) qui correspond à ce que vous avez dit dans la question.

Heureusement dans Java nous pourrions résoudre ce problème avec un StringBuffer, qui a O(1) complexité pour chaque ajout, alors la complexité globale serait O(n).

Dans le cas StringBuilder, la complexité amortie de N appelle sb.append(s) pour les chaînes de taille Mis O(M*N). Le mot clé ici est amorti. Lorsque vous ajoutez des caractères à un StringBuilder, l'implémentation peut avoir besoin d'étendre son tableau interne. Mais la stratégie d'expansion consiste à doubler la taille de la baie. Et si vous faites le calcul, vous verrez qu'en moyenne, chaque caractère du tampon va être copié une fois de plus pendant toute la séquence d'appels append. Ainsi, la séquence entière fonctionne toujours comme O(M*N) ... et, en l'occurrence, M*N Est la longueur totale de la chaîne.

Votre résultat final est donc correct, mais votre déclaration sur la complexité d'un seul appel à append n'est pas correcte. (Je comprends ce que vous voulez dire, mais la façon dont vous le dites est visiblement incorrecte.)

Enfin, je noterais qu'en Java vous devez utiliser StringBuilder plutôt que StringBuffer à moins que vous besoin le tampon soit thread -sûr.

13
Stephen C

Comme exemple d'une structure vraiment simple qui a O(n) complexité en C++ 11:

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;
  }
};

utilisation:

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

Cela prend O (n), où n est le nombre de caractères.

Enfin, si vous savez combien de temps toutes les chaînes sont, faire juste un reserve avec assez d'espace fait que append ou += Prend un total de O(n) fois. Mais je conviens que c'est gênant.

L'utilisation de std::move Avec le StringAppender ci-dessus (c'est-à-dire sa += std::move(s1)) augmentera considérablement les performances pour les chaînes non courtes (ou l'utiliser avec des valeurs x, etc.)

Je ne connais pas la complexité de std::ostringstream, Mais ostringstream est pour une sortie formatée assez imprimable, ou des cas où les hautes performances ne sont pas importantes. Je veux dire, ils ne sont pas mauvais, et ils peuvent même exécuter des langages scriptés/interprétés/bytecode, mais si vous êtes pressé, vous avez besoin d'autre chose.

Comme d'habitude, vous devez profiler, car des facteurs constants sont importants.

Un opérateur rvalue-reference-to-this + peut également être un bon, mais peu de compilateurs implémentent des références rvalue à cela.

1