Алгоритм присоединения, например, массив строк - PullRequest
13 голосов
/ 12 сентября 2008

Некоторое время я задавался вопросом, как может выглядеть хорошее, чистое решение для объединения массива строк. Пример: у меня есть ["Alpha", "Beta", "Gamma"] и я хочу объединить строки в одну, разделенную запятыми - "Alpha, Beta, Gamma".

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

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

Ответы [ 16 ]

19 голосов
/ 12 сентября 2008

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

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

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

10 голосов
/ 12 сентября 2008

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

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

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

5 голосов
/ 12 сентября 2008

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

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

separator = ","
foreach (item in stringCollection)
{
    concatenatedString += concatenatedString ? separator + item : item
}
3 голосов
/ 12 сентября 2008

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

join ([]) = ""

join ([x]) = "x"

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

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

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

3 голосов
/ 12 сентября 2008

Я обычно хожу с чем-то вроде ...

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

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

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

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

2 голосов
/ 14 сентября 2008

@ Мендель Зибенга

Строки являются краеугольными объектами в языках программирования. Разные языки реализуют строки по-разному. Реализация 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 (взят из 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
1 голос
/ 13 января 2009

собирать различные языковые реализации?
Вот для вашего развлечения версия 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 (или то, что называется «WriteStream» в Smalltalk). Не объединяйте строки, используя «+» в цикле - результатом будет много много промежуточных одноразовых строк. Если у вас есть хороший сборщик мусора, это нормально. Но некоторые нет, и много памяти необходимо восстановить. StringBuilder (и WriteStream, который является его прапрадедушкой) используют алгоритм удвоения буфера или даже адаптивного растущего алгоритма, который требует НАМНОГО меньшего количества временной памяти.

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

1 голос
/ 12 сентября 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 , прежде чем добавить к нему больше байтов.

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

1 голос
/ 12 сентября 2008

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

ResultString = InputArray[0]
n = 1
while n (is less than)  Number_Of_Strings
    ResultString (concatenate) ", "
    ResultString (concatenate) InputArray[n]
    n = n + 1
loop
0 голосов
/ 05 апреля 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...