Алгоритъм за присъединяване напр. масив от низове

От известно време се чудех как може да изглежда хубаво, чисто решение за свързване на масив от низове. Пример: Имам ["Алфа", "Бета", "Гама"] и искам да съединя низовете в един, разделен със запетаи – "Алфа, Бета, Гама".

Сега знам, че повечето езици за програмиране предлагат някакъв метод за присъединяване за това. Просто се чудя как те могат да бъдат приложени. Когато ходих на въвеждащи курсове, често се опитвах да се справя сам, но никога не намирах задоволителен алгоритъм. Всичко изглеждаше доста объркано, проблемът беше, че не можете просто да преминете през масива, свързвайки низовете, тъй като бихте добавили твърде много запетаи (преди или след последния низ). Не искам да проверявам условията в цикъла. Всъщност не искам да добавя първия или последния низ преди/след цикъла (предполагам, че това е може би най-добрият начин?).

Може ли някой да ми покаже елегантно решение? Или ми кажете точно защо не може да има нещо по-елегантно?


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
@Corey: Прав си. Мисля, че това е по-ясно за псевдо кода. Ако внедрите това в C#, със сигурност искате да използвате StringBuilder. - person Mendelt; 15.09.2008
comment
Не е толкова елегантно, ако stringColelction съдържа много низове; следователно решението не е полезно като библиотечна функция за общо ползване. За повече информация прочетете отговора ми по-долу. - person blabla999; 13.01.2009
comment
@blabla999 Просто използвам + тук, за да покажа, че правя конкатенация на низове. Какъв механизъм използвате (stringbuilder и т.н.) е специфичен за езика детайл на изпълнението. Просто исках да покажа елегантен начин за решаване на проблема с оградния стълб за добавяне на 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 (споменато от Jon Ericson), php, javascript - имат функция или метод join() и това е най-елегантното решение. По-малко код е по-добър код.

В отговор на Mendelt Siebenga, ако се нуждаете от ръчно навито решение, бих избрал троичния оператор за нещо като:

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
Пропуснал съм нещо или резултатът е: Алфа Бета, Гама, - person Nicolas; 12.09.2008
comment
Забравете: прочетох го твърде бързо. Но все още не ми харесва настройката на сепаратора в цикъл. - person Nicolas; 12.09.2008
comment
Николас: Съмнявам се, че ще можете да получите нещо по-хубаво в императивната настройка, тъй като обикновено ще трябва да обработвате отделно първия или последния елемент в списъка. Мога само да си представя да го направя по-добре, ако имате някакъв оператор за цикъл с част между: докато C do 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"

присъединяване ([x, почивка]) = "x," + присъединяване (почивка)

В действителност бихте написали това по по-общ начин, за да използвате повторно същия алгоритъм, но да абстрахирате типа данни (не е необходимо да са низове) и операцията (не е необходимо да е конкатенация със запетая в средата) . След това обикновено се нарича „намаляване“ и много функционални езици имат това вградено, напр. умножаване на всички числа в списък, в Lisp:

(намали #'* '(1 2 3 4 5)) => 120

person Luke Halliwell    schedule 12.09.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 (взето от багажник):

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

(Нещата от картата са там най-вече за създаване на списък.)

В езици, които нямат вграден, като 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 (или това, което се нарича "WriteStream" в Smalltalk) е силно препоръчително. Не свързвайте низове, като използвате "+" в цикъл - резултатът ще бъде много много междинни низове за изхвърляне. Ако имате добър Garbage Collector, това е добре. Но някои не са и много памет трябва да бъде възстановена. 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

Написах рекурсивна версия на решението в 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"))

Опитах се да приложа стратегията "разделяй и владей" към проблема, но предполагам, че това не дава по-добър резултат от обикновената итерация. Моля, уведомете ме дали това можеше да бъде направено по-добре.

Направих също анализ на рекурсията, получена от алгоритъма, достъпен е тук.

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