Как изменить эту процедуру, чтобы избежать использования рекурсии - PullRequest
4 голосов
/ 05 августа 2010

Итак, я писал mergesort в C # как упражнение , и, хотя это сработало, оглядываясь на код, было место для улучшения.

По существу, вторая часть алгоритма требует подпрограммы для объединения двух отсортированных списков .

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

private static List<int> MergeSortedLists(List<int> sLeft, List<int> sRight)
{
    if (sLeft.Count == 0 || sRight.Count == 0)
    {
        sLeft.AddRange(sRight);
        return sLeft;
    }
    else if (sLeft.Count == 1 && sRight.Count == 1)
    {
        if (sLeft[0] <= sRight[0])
            sLeft.Add(sRight[0]);
        else
            sLeft.Insert(0, sRight[0]);
        return sLeft;
    }
    else if (sLeft.Count == 1 && sRight.Count > 1)
    {
        for (int i=0; i<sRight.Count; i++)
        {
            if (sLeft[0] <= sRight[i])
            {
                sRight.Insert(i, sLeft[0]);
                return sRight;
            }
        }
        sRight.Add(sLeft[0]);
        return sRight;
    }
    else if (sLeft.Count > 1 && sRight.Count == 1)
    {
        for (int i=0; i<sLeft.Count; i++)
        {
            if (sRight[0] <= sLeft[i])
            {
                sLeft.Insert(i, sRight[0]);
                return sLeft;
            }
        }
        sLeft.Add(sRight[0]);
        return sLeft;
    }
    else
    {
        List<int> list = new List<int>();
        if (sLeft[0] <= sRight[0])
        {
            list.Add(sLeft[0]);
            sLeft.RemoveAt(0);
        }
        else
        {
            list.Add(sRight[0]);
            sRight.RemoveAt(0);
        }

        list.AddRange(MergeSortedLists(sLeft, sRight));
        return list;
    }       
}

Конечно, эту процедуру можно улучшить / сократить, удалив рекурсию и т. Д.даже другие способы объединения 2 отсортированных списков.Так что любой рефакторинг приветствуется.

Хотя у меня есть ответ, мне любопытно, как другие программисты могли бы улучшить эту процедуру.

Спасибо!

Ответы [ 8 ]

14 голосов
/ 05 августа 2010

Объединение двух отсортированных списков можно выполнить в O (n).

List<int> lList, rList, resultList;
int r,l = 0;

while(l < lList.Count && r < rList.Count)
{
  if(lList[l] < rList[r]
    resultList.Add(lList[l++]);
  else
    resultList.Add(rList[r++]);
}
//And add the missing parts.
while(l < lList.Count)
  resultList.Add(lList[l++]);
while(r < rList.Count)
  resultList.Add(rList[r++]);
4 голосов
/ 05 августа 2010

Мой взгляд на это будет:

private static List<int> MergeSortedLists(List<int> sLeft, List<int> sRight)
{
    List<int> result = new List<int>();
    int indexLeft = 0;
    int indexRight = 0;

    while (indexLeft < sLeft.Count || indexRight < sRight.Count)
    {
        if (indexRight == sRight.Count ||
            (indexLeft < sLeft.Count && sLeft[indexLeft] < sRight[indexRight]))
        {
            result.Add(sLeft[indexLeft]);
            indexLeft++;
        }
        else
        {
            result.Add(sRight[indexRight]);
            indexRight++;
        }
    }
    return result;
}

Именно то, что я бы сделал, если бы мне пришлось делать это вручную.=)

3 голосов
/ 05 августа 2010

Вы действительно уверены, что ваш код работает вообще? Не проверяя это, я вижу следующее:

...
else if (sLeft.Count > 1 && sRight.Count == 0)  //<-- sRight is empty
{
    for (int i=0; i<sLeft.Count; i++)
    {
        if (sRight[0] <= sLeft[i]) //<-- IndexError?
        {
            sLeft.Insert(i, sRight[0]);
            return sLeft;
        }
    }
    sLeft.Add(sRight[0]);
    return sLeft;
}
...
2 голосов
/ 05 августа 2010

Часто вы можете использовать стек вместо рекурсии

2 голосов
/ 05 августа 2010

Вы также просили разные подходы. Я мог бы сделать как ниже в зависимости от использования. Приведенный ниже код является ленивым, поэтому он не будет сортировать весь список сразу, а только при запросе элементов.

class MergeEnumerable<T> : IEnumerable<T>
    {
        public IEnumerator<T> GetEnumerator()
        {
            var left = _left.GetEnumerator();
            var right = _right.GetEnumerator();
            var leftHasSome = left.MoveNext();
            var rightHasSome = right.MoveNext();
            while (leftHasSome || rightHasSome)
            {
                if (leftHasSome && rightHasSome)
                {
                  if(_comparer.Compare(left.Current,right.Current) < 0)
                  {
                    yield return returner(left);
                  } else {
                    yield return returner(right);
                  }
                }
                else if (rightHasSome)
                {
                    returner(right);
                }
                else
                {
                    returner(left);
                }
            }
        }

        private T returner(IEnumerator<T> enumerator)
        {
            var current = enumerator.Current;
            enumerator.MoveNext();
            return current;
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return ((IEnumerable<T>)this).GetEnumerator();
        }

        private IEnumerable<T> _left;
        private IEnumerable<T> _right;
        private IComparer<T> _comparer;

        MergeEnumerable(IEnumerable<T> left, IEnumerable<T> right, IComparer<T> comparer)
        {
            _left = left;
            _right = right;
            _comparer = comparer;
        }
    }

РЕДАКТИРОВАТЬ: Это в основном то же осуществление, что и Сергей Осипчук, его воля от начала и до конца, если смотреть только на сортировку, будет самой быстрой, но задержка будет также выше из-за факта сортировки всего списка авансом. Поэтому, как я сказал, в зависимости от использования, я мог бы пойти с этим подходом, и альтернативой было бы что-то похожее на Сергея Осипчука

2 голосов
/ 05 августа 2010

В качестве отправной точки я бы исключил ваши особые случаи, когда любой из списков имеет Count == 1 - они могут обрабатываться вашим более общим (в настоящее время повторяющимся) случаем.

Будет if (sLeft.Count > 1 && sRight.Count == 0) никогда не будет правдой, потому что вы проверили sRight.Count == 0 в начале - поэтому этот код никогда не будет достигнут и является избыточным.

Наконец, вместо повторения (что очень дорого вв этом случае из-за количества новых списков, которые вы создаете - по одному на элемент!), я бы сделал что-то подобное в вашем else (на самом деле, это может заменить весь ваш метод):

List<int> list = new List<int>();

while (sLeft.Count > 0 && sRight.Count > 0)
{
    if (sLeft[0] <= sRight[0])
    {
        list.Add(sLeft[0]);
        sLeft.RemoveAt(0);
    }
    else
    {
        list.Add(sRight[0]);
        sRight.RemoveAt(0);
    }
}

// one of these two is already empty; the other is in sorted order...
list.AddRange(sLeft);
list.AddRange(sRight);
return list;

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

1 голос
/ 05 августа 2010

Список слияния (по теории, входные списки сортируются заранее) сортировка может быть реализована следующим образом:

List<int> MergeSorting(List<int> a, List<int> b)
    {
        int apos = 0;
        int bpos = 0;
        List<int> result = new List<int>();
        while (apos < a.Count && bpos < b.Count)
        {
            int avalue = int.MaxValue;
            int bvalue = int.MaxValue;
            if (apos < a.Count)
                avalue = a[apos];
            if (bpos < b.Count)
                bvalue = b[bpos];
            if (avalue < bvalue)
            {
                result.Add(avalue);
                apos++;
            }
            else
            {
                result.Add(bvalue);
                bpos++;
            }
        }
        return result;
    }

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

0 голосов
/ 05 августа 2010

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

Редактировать: Некоторая информация, которую я оставил ранее, была неправильной из-за моего недопонимания - список в C # похож на массив, а не на связанный список. Мои извинения.

...