【问题标题】:Algorithm for joining e.g. an array of strings加入算法,例如字符串数组
【发布时间】:2010-09-08 16:13:26
【问题描述】:

一段时间以来,我一直想知道,连接字符串数组的一个漂亮、干净的解决方案会是什么样子。 示例:我有 ["Alpha", "Beta", "Gamma"] 并想将字符串合并为一个,用逗号分隔 - "Alpha, Beta, Gamma"。

现在我知道大多数编程语言都为此提供了某种连接方法。我只是想知道如何实现这些。 上入门课程时,我经常尝试单干,但始终没有找到令人满意的算法。一切看起来都相当混乱,问题是你不能只循环遍历数组,连接字符串,因为你会添加一个太多的逗号(在最后一个字符串之前或之后)。 我不想检查循环中的条件。我真的不想在循环之前/之后添加第一个或最后一个字符串(我想这可能是最好的方法?)。

谁能告诉我一个优雅的解决方案?或者告诉我为什么没有更优雅的东西?

【问题讨论】:

    标签: arrays algorithm string language-agnostic


    【解决方案1】:

    我为此类问题找到的最优雅的解决方案是这样的(在伪代码中)

    separator = ""
    foreach(item in stringCollection)
    {
        concatenatedString += separator + item
        separator = ","
    }
    

    您只需运行循环,并且仅在设置分隔符的第二次之后。所以第一次它不会被添加。它不像我希望的那样干净,所以我仍然会添加 cmets,但它比 if 语句或在循环外添加第一个或最后一个项目要好。

    【讨论】:

    • 在许多语言中,除非您使用 Stringbuilder,否则性能会很差。
    • @Jon:如果你在循环外和循环内添加一个项目,你有重复的代码,在这种情况下这不是问题,但在其他情况下你可能会重复几行代码,这有点干净。
    • @Corey:你是对的。我认为这对于伪代码来说更清楚。如果你在 C# 中实现这一点,你肯定想使用 StringBuilder。
    • 如果 stringColelction 包含很多字符串,那就不那么优雅了;因此,该解决方案不能用作通用库函数。有关更多信息,请阅读下面的答案。
    • @blabla999 只是在这里使用 + 来表明我正在做字符串连接。您使用的机制(stringbuilder 等)是特定于语言的实现细节。只是想展示一种优雅的方法来解决将 n-1 个分隔符添加到 n 个项目的栅栏柱问题。
    【解决方案2】:

    所有这些解决方案都不错,但对于底层库来说,分隔符的独立性和不错的速度都很重要。假设语言具有某种形式的字符串生成器,这是一个符合要求的函数。

    public static string join(String[] strings, String sep) {
      if(strings.length == 0) return "";
      if(strings.length == 1) return strings[0];
      StringBuilder sb = new StringBuilder();
      sb.append(strings[0]);
      for(int i = 1; i < strings.length; i++) {
        sb.append(sep);
        sb.append(strings[i]);
      }
      return sb.toString();
    }
    

    编辑:我想我应该提到为什么这会更快。主要原因是因为任何时候你调用 c = a + b;底层构造通常是 c = (new StringBuilder()).append(a).append(b).toString();。通过重用相同的字符串构建器对象,我们可以减少我们产生的分配和垃圾的数量。

    在有人插话优化是邪恶的之前,我们正在讨论实现一个通用的库函数。可接受的、可扩展的性能是它们的要求之一。一个需要很长时间的连接是不会经常使用的。

    【讨论】:

    • 我们可以加入“语言不可知论”标签吗?
    • 我相信 StringBuilder (或类似的构造)隐含在其他响应中。那么,您的第二个功能在哪里?你答应了两个。
    • 并非如此。不管你怎么切,每种语言都以多种方式处理字符串。所有其他解决方案都假设您有一个附加字符串方法,这当然与语言无关,因为 c 没有它。如果不做一些假设,你就无法真正处理字符串
    • 其他功能:我考虑过,然后没有。真的。我想我可以演示一下 c 的做法。然后我们至少会涵盖所有主要的语言家族,但我会把它留给其他人去做。
    • 如果您真的担心分配速度,您可以将 StringBuilder 初始化为所需的容量 (strings.Sum(s => s.Length) + (strings.length - 1) * 分隔符。长度)并完全取消重新分配空间。
    【解决方案3】:

    当今大多数语言 - 例如perl(由 Jon Ericson 提到)、php、javascript - 有一个 join() 函数或方法,这是迄今为止最优雅的解决方案。更少的代码就是更好的代码。

    作为对 Mendelt Siebenga 的回应,如果您确实需要手动解决方案,我会选择三元运算符,例如:

    separator = ","
    foreach (item in stringCollection)
    {
        concatenatedString += concatenatedString ? separator + item : item
    }
    

    【讨论】:

    • 使用“+”连接多个字符串总是可能的性能消耗者。
    【解决方案4】:

    我通常会选择...

    list = ["Alpha", "Beta", "Gamma"];
    output = "";
    separator = "";
    for (int i = 0; i < list.length ; i++) {
      output = output + separator;
      output = output + list[i];
      separator = ", ";
    }
    

    这是可行的,因为在第一次传递时,分隔符是空的(因此您在开始时不会得到逗号,但在随后的每一次传递中,您都会在添加下一个元素之前添加一个逗号。

    您当然可以稍微展开它以使其更快一些(一遍又一遍地分配给分隔符并不理想),但我怀疑这是编译器可以自动为您做的事情。

    最后,我怀疑这就是大多数语言级别的连接函数归结为的原因。只不过是语法糖,但它确实很甜。

    【讨论】:

    • 我遗漏了什么或者结果是:“Alpha Beta, Gamma”
    • 算了:我读得太快了。但我还是不喜欢分隔符的循环内设置。
    • Nicolas:我怀疑您是否能够在命令式设置中获得“更好”的东西,因为您通常必须单独处理列表中的第一项或最后一项。我只能想象如果你有一些带有“中间”部分的循环运算符会做得更好:while C do S and in between T od。
    • 每次循环都设置分隔符很浪费。不过,也许一个好的优化器会将其排除在外。
    • 在下面查看我对三元运算符的评论,并简单地检查您正在构建的字符串是否为非空
    【解决方案5】:

    为了纯粹的优雅,典型的递归函数式语言解决方案非常好。这不是实际的语言语法,但你明白了(它也被硬编码为使用逗号分隔符):

    加入([]) = ""

    加入([x]) = "x"

    join([x, rest]) = "x," + join(rest)

    实际上,您可以以更通用的方式编写它,以重用相同的算法,但抽象出数据类型(不必是字符串)和操作(不必用逗号连接)中间)。然后它通常被称为“reduce”,许多函数式语言都内置了这个,例如在 Lisp 中将列表中的所有数字相乘:

    (减少 #'* '(1 2 3 4 5)) => 120

    【讨论】:

      【解决方案6】:

      @Mendelt Siebenga

      字符串是编程语言中的基石对象。不同的语言以不同的方式实现字符串。 join() 的实现很大程度上依赖于字符串的底层实现。伪代码不反映底层实现。

      在 Python 中考虑join()。它可以很容易地使用:

      print ", ".join(["Alpha", "Beta", "Gamma"])
      # Alpha, Beta, Gamma
      

      它可以很容易地实现如下:

      def join(seq, sep=" "):
          if not seq:         return ""
          elif len(seq) == 1: return seq[0]
          return reduce(lambda x, y: x + sep + y, seq)
      
      print join(["Alpha", "Beta", "Gamma"], ", ")
      # Alpha, Beta, Gamma
      

      这里是join()方法是如何在C中实现的(取自trunk):

      PyDoc_STRVAR(join__doc__,
      "S.join(sequence) -> string\n\
      \n\
      Return a string which is the concatenation of the strings in the\n\
      sequence.  The separator between elements is S.");
      
      static PyObject *
      string_join(PyStringObject *self, PyObject *orig)
      {
          char *sep = PyString_AS_STRING(self);
          const Py_ssize_t seplen = PyString_GET_SIZE(self);
          PyObject *res = NULL;
          char *p;
          Py_ssize_t seqlen = 0;
          size_t sz = 0;
          Py_ssize_t i;
          PyObject *seq, *item;
      
          seq = PySequence_Fast(orig, "");
          if (seq == NULL) {
              return NULL;
          }
      
          seqlen = PySequence_Size(seq);
          if (seqlen == 0) {
              Py_DECREF(seq);
              return PyString_FromString("");
          }
          if (seqlen == 1) {
              item = PySequence_Fast_GET_ITEM(seq, 0);
              if (PyString_CheckExact(item) || PyUnicode_CheckExact(item)) {
                  Py_INCREF(item);
                  Py_DECREF(seq);
                  return item;
              }
          }
      
          /* There are at least two things to join, or else we have a subclass
           * of the builtin types in the sequence.
           * Do a pre-pass to figure out the total amount of space we'll
           * need (sz), see whether any argument is absurd, and defer to
           * the Unicode join if appropriate.
           */
          for (i = 0; i < seqlen; i++) {
              const size_t old_sz = sz;
              item = PySequence_Fast_GET_ITEM(seq, i);
              if (!PyString_Check(item)){
      #ifdef Py_USING_UNICODE
                  if (PyUnicode_Check(item)) {
                      /* Defer to Unicode join.
                       * CAUTION:  There's no gurantee that the
                       * original sequence can be iterated over
                       * again, so we must pass seq here.
                       */
                      PyObject *result;
                      result = PyUnicode_Join((PyObject *)self, seq);
                      Py_DECREF(seq);
                      return result;
                  }
      #endif
                  PyErr_Format(PyExc_TypeError,
                           "sequence item %zd: expected string,"
                           " %.80s found",
                           i, Py_TYPE(item)->tp_name);
                  Py_DECREF(seq);
                  return NULL;
              }
              sz += PyString_GET_SIZE(item);
              if (i != 0)
                  sz += seplen;
              if (sz < old_sz || sz > PY_SSIZE_T_MAX) {
                  PyErr_SetString(PyExc_OverflowError,
                      "join() result is too long for a Python string");
                  Py_DECREF(seq);
                  return NULL;
              }
          }
      
          /* Allocate result space. */
          res = PyString_FromStringAndSize((char*)NULL, sz);
          if (res == NULL) {
              Py_DECREF(seq);
              return NULL;
          }
      
          /* Catenate everything. */
          p = PyString_AS_STRING(res);
          for (i = 0; i < seqlen; ++i) {
              size_t n;
              item = PySequence_Fast_GET_ITEM(seq, i);
              n = PyString_GET_SIZE(item);
              Py_MEMCPY(p, PyString_AS_STRING(item), n);
              p += n;
              if (i < seqlen - 1) {
                  Py_MEMCPY(p, sep, seplen);
                  p += seplen;
              }
          }
      
          Py_DECREF(seq);
          return res;
      }
      

      注意上面的Catenate everything.代码只是整个函数的一小部分。

      在伪代码中:

      /* Catenate everything. */
      for each item in sequence
          copy-assign item
          if not last item
              copy-assign separator
      

      【讨论】:

        【解决方案7】:

        ' 伪代码假设从零开始

        结果字符串 = 输入数组 [0] n = 1 而 n (小于) Number_Of_Strings 结果字符串(连接)“,” ResultString(连接) InputArray[n] n = n + 1 环形

        【讨论】:

        • 我会使用 += 而不是 = 来表明有一个 concat 发生。
        • 我在评论之前看到并添加(连接)以非常清楚(他们可能不知道'c')。
        • 您不想将= 更改为(assign?),使其与语言无关吗?可怜的帕斯卡人不明白你的意思。 ;-) 说真的。
        【解决方案8】:

        在 Perl 中,我只使用 join 命令:

        $ echo "Alpha
        Beta
        Gamma" | perl -e 'print(join(", ", map {chomp; $_} <> ))'
        Alpha, Beta, Gamma
        

        ma​​p 主要用于创建列表。)

        在没有内置语言(如 C)中,我使用简单迭代(未经测试):

        for (i = 0; i < N-1; i++){
            strcat(s, a[i]);
            strcat(s, ", ");
        }
        strcat(s, a[N]);
        

        当然,在向其添加更多字节之前,您需要检查 s 的大小。

        您必须将first entry 或最后一个特殊情况。

        【讨论】:

          【解决方案9】:

          收集不同的语言实现?
          这是一个 Smalltalk 版本,供您娱乐:

          join:collectionOfStrings separatedBy:sep
          
            |buffer|
          
            buffer := WriteStream on:''.
            collectionOfStrings 
                do:[:each | buffer nextPutAll:each ]
                separatedBy:[ buffer nextPutAll:sep ].
            ^ buffer contents.
          

          当然,上面的代码已经在标准库中找到了:

          集合 >> asStringWith:

          所以,使用它,你会写:

          #('A' 'B' 'C') asStringWith:','
          

          但这是我的主要观点

          我想更加强调一个事实,即强烈推荐使用 StringBuilder(或 Smalltalk 中所谓的“WriteStream”)。不要在循环中使用“+”连接字符串 - 结果将是许多中间丢弃的字符串。如果你有一个好的垃圾收集器,那很好。但有些不是,需要回收大量内存。 StringBuilder(和它的祖父 WriteStream)使用缓冲区加倍甚至自适应增长算法,它需要 MUCH 更少的暂存内存。

          但是,如果您要连接的只是几个小字符串,则不要在意,并“+”它们;使用 StringBuilder 的额外工作实际上可能适得其反,最多取决于实现和语言的字符串数量。

          【讨论】:

          • 取决于一种语言如何在字符串上实现“+”运算符,它可能是好是坏。并非所有语言都创建中间字符串。我看不懂 smalltalk,但我很好奇你如何防止最后一个“,”被打印出来。 n 个项目只需要 n-1 个分隔符。
          • 这就是做什么:[] separatorBy:[] 做什么;它评估每个给定的 lambda 为 do: arg 和 inbetween 评估由第二个参数给出的那些。所以n-1逻辑就在那里。
          【解决方案10】:

          以下内容不再与语言无关(但这对于讨论无关紧要,因为该实现很容易移植到其他语言)。我尝试用命令式编程语言实现 Luke 的(理论上最好的)解决方案。任君挑选;我的 C#。一点也不优雅。但是,(没有任何测试)我可以想象它的性能相当不错,因为递归实际上是尾递归。

          我的挑战:提供更好的递归实现(使用命令式语言)。你说“更好”是什么意思:更少的代码,更快,我愿意接受建议。

          private static StringBuilder RecJoin(IEnumerator<string> xs, string sep, StringBuilder result) {
              result.Append(xs.Current);
              if (xs.MoveNext()) {
                  result.Append(sep);
                  return RecJoin(xs, sep, result);
              } else
                  return result;
          }
          
          public static string Join(this IEnumerable<string> xs, string separator) {
              var i = xs.GetEnumerator();
              if (!i.MoveNext())
                  return string.Empty;
              else
                  return RecJoin(i, separator, new StringBuilder()).ToString();
          }
          

          【讨论】:

          • 当然,如果我们要放弃语言独立性,那么您可以使用 string.Join(separator, xs) :)
          • ;-)
          【解决方案11】:

          join() Ruby 中的函数:

          def join(seq, sep) 
            seq.inject { |total, item| total << sep << item } or "" 
          end
          
          join(["a", "b", "c"], ", ")
          # => "a, b, c"
          

          【讨论】:

            【解决方案12】:

            join() 在 Perl 中:

            use List::Util qw(reduce);
            
            sub mjoin($@) {$sep = shift; reduce {$a.$sep.$b} @_ or ''}
            
            say mjoin(', ', qw(Alpha Beta Gamma));
            # Alpha, Beta, Gamma
            

            或者没有reduce:

             sub mjoin($@) 
             {
               my ($sep, $sum) = (shift, shift); 
               $sum .= $sep.$_ for (@_); 
               $sum or ''
             }
            

            【讨论】:

              【解决方案13】:

              Perl 6

              sub join( $separator, @strings ){
                my $return = shift @strings;
                for @strings -> ( $string ){
                  $return ~= $separator ~ $string;
                }
                return $return;
              }
              

              是的,我知道这毫无意义,因为 Perl 6 已经有一个连接函数。

              【讨论】:

                【解决方案14】:

                我用 lisp 写了一个递归版本的解决方案。如果列表的长度大于 2,它会尽可能将列表分成两半,然后尝试合并子列表

                    (defun concatenate-string(list)
                       (cond ((= (length list) 1) (car list))
                             ((= (length list) 2) (concatenate 'string (first list) "," (second list)))
                             (t (let ((mid-point (floor (/ (- (length list) 1) 2))))
                                   (concatenate 'string 
                                                (concatenate-string (subseq list 0 mid-point))
                                                ","
                                                (concatenate-string (subseq list mid-point (length list))))))))
                
                
                
                    (concatenate-string '("a" "b"))
                

                我尝试将分而治之的策略应用于该问题,但我想这并没有比简单的迭代产生更好的结果。请让我知道这是否可以做得更好。

                我还对算法得到的递归进行了分析,可在here

                【讨论】:

                  【解决方案15】:

                  在 C# 中使用 String.join 方法

                  http://msdn.microsoft.com/en-us/library/57a79xd0.aspx

                  【讨论】:

                    【解决方案16】:

                    在 Java 5 中,带有单元测试:

                    import junit.framework.Assert;
                    import org.junit.Test;
                    
                    public class StringUtil
                    {
                        public static String join(String delim, String... strings)
                        {
                            StringBuilder builder = new StringBuilder();
                    
                            if (strings != null)
                            {
                                for (String str : strings)
                                {
                                    if (builder.length() > 0)
                                    {
                                        builder.append(delim);
                                    }
                    
                                    builder.append(str);
                                }
                            }           
                    
                            return builder.toString();
                        }
                    
                        @Test
                        public void joinTest()
                        {
                            Assert.assertEquals("", StringUtil.join(", ", null));
                            Assert.assertEquals("", StringUtil.join(", ", ""));
                            Assert.assertEquals("", StringUtil.join(", ", new String[0]));
                            Assert.assertEquals("test", StringUtil.join(", ", "test"));
                            Assert.assertEquals("foo, bar", StringUtil.join(", ", "foo", "bar"));
                            Assert.assertEquals("foo, bar, baz", StringUtil.join(", ", "foo", "bar", "baz"));
                        }
                    }
                    

                    【讨论】:

                    • 有一个delimiter参数,然后假设每个人也想附加一个空格,有点傻。
                    • 是的,我没想到。更新了我上面的代码以更正硬编码的空间。
                    猜你喜欢
                    • 2017-02-04
                    • 1970-01-01
                    • 2018-10-01
                    • 2013-06-07
                    • 2021-09-30
                    • 2021-04-30
                    • 2017-02-03
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多