Алгоритм присоединения, например, массив строк

Некоторое время я задавался вопросом, как могло бы выглядеть красивое и чистое решение для объединения массива строк. Пример: у меня есть [«Альфа», «Бета», «Гамма»] и я хочу объединить строки в одну, разделенную запятыми - «Альфа, Бета, Гамма».

Теперь я знаю, что большинство языков программирования предлагают для этого какой-то метод соединения. Мне просто интересно, как это можно реализовать. Когда я проходил вводные курсы, я часто пытался пройти их в одиночку, но так и не нашел удовлетворительного алгоритма. Все выглядело довольно беспорядочно, проблема заключалась в том, что вы не можете просто перебрать массив, объединяя строки, так как вы добавили бы слишком много запятых (либо до, либо после последней строки). Я не хочу проверять условия в цикле. Я действительно не хочу добавлять первую или последнюю строку до / после цикла (наверное, это лучший способ?).

Может кто-нибудь подскажет элегантное решение? Или скажите, почему не может быть ничего элегантнее?


person knuton    schedule 12.09.2008    source источник


Ответы (16)


Самое элегантное решение, которое я нашел для подобных проблем, - это что-то вроде этого (в псевдокоде)

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

Вы просто запускаете цикл и только после того, как второй раз вокруг будет установлен разделитель. Так что в первый раз он не будет добавлен. Это не так чисто, как хотелось бы, поэтому я все равно буду добавлять комментарии, но это лучше, чем оператор if или добавление первого или последнего элемента вне цикла.

person Mendelt    schedule 12.09.2008
comment
На многих языках это будет иметь низкую производительность, если вы не используете Stringbuilder. - person Corey; 15.09.2008
comment
@Jon: Если вы рекламируете элемент вне цикла и внутри цикла, у вас есть дублированный код, в этом случае это не проблема, но в других случаях у вас могут повторяться несколько строк кода, это немного чище. - person Mendelt; 15.09.2008
comment
@ Кори: Ты прав. Я думаю, что это более понятно для псевдокода. Если вы реализуете это на C #, вы наверняка захотите использовать StringBuilder. - person Mendelt; 15.09.2008
comment
Не так элегантно, если stringColelction содержит много строк; поэтому решение бесполезно в качестве библиотечной функции общего назначения. Подробнее читайте в моем ответе ниже. - person blabla999; 13.01.2009
comment
@ blabla999 Просто использую + здесь, чтобы показать, что я выполняю конкатенацию строк. Какой механизм вы используете (построитель строк и т. Д.), Зависит от конкретного языка реализации. Просто хотел показать элегантный способ решения проблемы с забором, добавляя n-1 разделителей к n элементам. - person Mendelt; 14.01.2009

Все эти решения достойны, но для базовой библиотеки важны как независимость разделителя, так и приличная скорость. Вот функция, которая соответствует требованиям, предполагая, что в языке есть некоторая форма построителя строк.

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 () ;. Повторно используя один и тот же объект построителя строк, мы можем уменьшить количество выделяемых ресурсов и мусора, который мы производим.

И прежде чем кто-то вмешается в оптимизацию, это зло, мы говорим о реализации общей библиотечной функции. Приемлемая масштабируемая производительность - одно из их требований. Соединение, которое занимает много времени, редко используется.

person Patrick    schedule 12.09.2008
comment
Можем ли мы присоединиться к ярлыку языковой агностики? - person Nicolas; 12.09.2008
comment
Я считаю, что StringBuilder (или аналогичная конструкция) подразумевается в других ответах. Итак, где ваша вторая функция? Вы обещали два. - person Konrad Rudolph; 12.09.2008
comment
Не совсем. Независимо от того, как вы это сокращаете, каждый язык имеет дело со строками определенным количеством способов. Все другие решения предполагают, что у вас есть метод добавления строки, который определенно не зависит от языка, поскольку в c его нет. Вы не можете работать со строками, не делая некоторых предположений - person Patrick; 12.09.2008
comment
По поводу другой функции: я подумал, потом не подумал. Действительно. Полагаю, я мог бы продемонстрировать способ c, как это сделать. Тогда мы охватили бы по крайней мере все основные языковые семьи, но я оставлю это кому-нибудь другому. - person Patrick; 12.09.2008
comment
Если вас действительно беспокоит скорость выделения, вы можете инициализировать StringBuilder до необходимой емкости (strings.Sum (s = ›s.Length) + (strings.length - 1) * separator.length) и отказаться от перераспределения пространства. вообще. - person David Schmitt; 12.09.2008
comment
В C наверняка есть функция добавления строки: strcat. - person Jon Ericson; 12.09.2008
comment
Джон: не так, как в предыдущих примерах. strcat не возвращает новую строку и имеет совершенно другую семантику, чем strA + strB. - person Patrick; 12.09.2008
comment
Дэвид: затем вы дважды перебирали коллекцию. Я не уверен, что в управляемой среде, где реаллоки относительно дешевы, вы можете рассчитывать заранее. Опять же, есть только один способ узнать. :) - person Patrick; 12.09.2008

Большинство языков в настоящее время - например, perl (упоминается Джоном Эриксоном), php, javascript - имеют функцию или метод join (), и это, безусловно, самое элегантное решение. Чем меньше кода, тем лучше.

В ответ Мендельту Зибенге, если вам действительно нужно решение, скрученное вручную, я бы выбрал тернарный оператор для чего-то вроде:

separator = ","
foreach (item in stringCollection)
{
    concatenatedString += concatenatedString ? separator + item : item
}
person Bobby Jack    schedule 12.09.2008
comment
использование + для объединения многих строк всегда может снизить производительность. - person blabla999; 13.01.2009

Я обычно использую что-то вроде ...

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

Это работает, потому что на первом проходе разделитель пуст (поэтому вы не получаете запятую в начале, но на каждом последующем проходе вы добавляете запятую перед добавлением следующего элемента.

Конечно, вы могли бы немного развернуть это, чтобы сделать его немного быстрее (назначение разделителю снова и снова не идеально), хотя я подозреваю, что компилятор мог бы сделать это за вас автоматически.

В конце концов, я подозреваю, что именно к этому сводятся большинство функций соединения на уровне языка. Ничего более, чем синтаксический сахар, но это определенно мило.

person Matt Sheppard    schedule 12.09.2008
comment
Я что-то пропустил, или результат: Alpha Beta, Gamma, - person Nicolas; 12.09.2008
comment
Забудьте об этом: я прочитал это слишком быстро. Но мне все еще не нравится внутрицикловая настройка разделителя. - person Nicolas; 12.09.2008
comment
Николас: Я сомневаюсь, что вы сможете получить что-то более приятное в императивной настройке, поскольку вам обычно придется отдельно обрабатывать первый или последний элемент в списке. Я могу только представить, что было бы лучше, если бы у вас был оператор цикла с промежуточной частью: в то время как C делает S, а между T od. - person mweerden; 12.09.2008
comment
Расточительно устанавливать seperator каждый раз в цикле. Хотя, может быть, хороший оптимизатор это исключит. - person Jon Ericson; 12.09.2008
comment
См. Мой комментарий о тернарном операторе ниже и просто проверяю, что строка, которую вы строите, не пуста - person Bobby Jack; 12.09.2008
comment
Также прочтите мой ответ ниже w.r.t. временный мусор. Лучше используйте StringBuilder. - person blabla999; 13.01.2009

Для чистой элегантности типичное рекурсивное решение на функциональном языке весьма неплохо. Этого нет в реальном синтаксисе языка, но вы понимаете (он также жестко запрограммирован на использование разделителя запятой):

присоединиться ([]) = ""

присоединиться ([x]) = "x"

join ([x, rest]) = "x," + join (отдых)

На самом деле вы бы написали это более общим способом, чтобы повторно использовать тот же алгоритм, но абстрагироваться от типа данных (не обязательно строк) и операции (не обязательно конкатенации с запятой в середине) . Затем его обычно называют «уменьшить», и многие функциональные языки имеют это встроенное, например умножение всех чисел в списке в Лиспе:

(уменьшить # '*' (1 2 3 4 5)) => 120

person Luke Halliwell    schedule 12.09.2008

@Mendelt Siebenga

Строки - это краеугольный камень языков программирования. В разных языках строки реализуются по-разному. Реализация join() сильно зависит от базовой реализации строк. Псевдокод не отражает базовую реализацию.

Рассмотрим join() в Python. Его легко использовать:

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 (взято из ствол):

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
person jfs    schedule 14.09.2008

'Псевдокод Предположим, что на основе нуля

ResultString = InputArray[0]
n = 1
while n (is less than)  Number_Of_Strings
    ResultString (concatenate) ", "
    ResultString (concatenate) InputArray[n]
    n = n + 1
loop
person David L Morris    schedule 12.09.2008
comment
Я бы использовал + = вместо =, чтобы показать, что происходит concat. - person Ralph M. Rickenbach; 12.09.2008
comment
Я видел это перед комментарием и добавил (объединить), чтобы было действительно ясно (они могли не знать 'c'). - person David L Morris; 12.09.2008
comment
Разве вы не хотите изменить = на (assign?), чтобы он больше не зависел от языка? Бедные люди Паскаля не понимают, что вы имеете в виду. ;-) Если серьезно. - person Konrad Rudolph; 12.09.2008

В Perl я просто использую команду join:

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

(Элемент map в основном нужен для создания списка.)

В языках, у которых нет встроенного, например C, я использую простую итерацию (непроверенную):

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

Конечно, вам нужно будет проверить размер s, прежде чем добавлять к нему дополнительные байты.

Вы должны либо в особом случае использовать первую запись, либо последний.

person Jon Ericson    schedule 12.09.2008

собираете разные языковые реализации?
Вот, для вашего развлечения, версия 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, который является его прадедом) используют алгоритм удвоения буфера или даже адаптивного увеличения, для которого требуется на НАМНОГО меньше оперативной памяти.

Однако, если вы объединяете всего несколько маленьких строк, не обращайте внимания и «+» их; дополнительная работа с использованием StringBuilder может оказаться контрпродуктивной, вплоть до количества строк, зависящего от реализации и языка.

person blabla999    schedule 12.01.2009
comment
В зависимости от того, как язык реализует оператор '+' для строк, он может быть хорошим или плохим. Не все языки создают промежуточные строки. Я не умею читать smalltalk, но мне любопытно, как вы предотвращаете печать последнего ','. Вам понадобится всего n-1 разделителей для n элементов. - person Mendelt; 14.01.2009
comment
вот что делать: [] separatedBy: [] делает; он оценивает каждую заданную лямбду как do: arg, а inbetween оценивает значения, заданные вторым аргументом. Итак, логика n-1 здесь. - person blabla999; 28.11.2012

Следующее больше не зависит от языка (но это не имеет значения для обсуждения, потому что реализация легко переносится на другие языки). Я попытался реализовать решение Люка (теоретически лучшее) на императивном языке программирования. Сделайте ваш выбор; мой 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();
}
person Konrad Rudolph    schedule 12.09.2008
comment
Конечно, если мы откажемся от языковой независимости, вы можете просто использовать string.Join (separator, xs) :) - person Simon Steele; 12.09.2008
comment
‹Вставьте здесь ехидный комментарий о выборе более лаконичного языка.› ;-) - person Jon Ericson; 12.09.2008

join() функция в Ruby:

def join(seq, sep) 
  seq.inject { |total, item| total << sep << item } or "" 
end

join(["a", "b", "c"], ", ")
# => "a, b, c"
person jfs    schedule 14.09.2008

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 ''
 }
person jfs    schedule 14.09.2008

Perl 6

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

Да, я знаю, что это бессмысленно, потому что в Perl 6 уже есть функция соединения.

person Brad Gilbert    schedule 15.09.2008

Я написал рекурсивную версию решения на лиспе. Если длина списка больше 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"))

Я пробовал применить к проблеме стратегию «разделяй и властвуй», но полагаю, это не дает лучшего результата, чем простая итерация. Пожалуйста, дайте мне знать, можно ли было сделать это лучше.

Я также провел анализ рекурсии, полученной алгоритмом, он доступен здесь.

person Community    schedule 17.09.2008

Используйте метод String.join в C #

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

person Community    schedule 05.04.2009

В 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"));
    }
}
person Mark B    schedule 16.09.2008
comment
Иметь параметр-разделитель, а затем предполагать, что все также хотят добавить пробел, - это немного глупо. - person Jon; 24.09.2009
comment
Да, я об этом не подумал. Обновил мой код выше, чтобы исправить жестко запрограммированное пространство. - person Mark B; 26.09.2009