Объединить Сортировать связанный список - PullRequest
52 голосов
/ 11 августа 2008

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

Ответы [ 17 ]

78 голосов
/ 23 ноября 2011

Интересно, почему это должно быть большой проблемой, как указано здесь, вот простая реализация на Java без каких-либо "хитрых уловок".

//The main function
public static Node merge_sort(Node head) 
{
    if(head == null || head.next == null) 
        return head;

    Node middle = getMiddle(head);      //get the middle of the list
    Node left_head = head;
    Node right_head = middle.next; 
    middle.next = null;             //split the list into two halfs

    return merge(merge_sort(left_head), merge_sort(right_head));  //recurse on that
}

//Merge subroutine to merge two sorted lists
public static Node merge(Node a, Node b)
{
    Node dummyHead = new Node();

    for(Node current  = dummyHead; a != null && b != null; current = current.next;)
    {
        if(a.data <= b.data) 
        {
            current.next = a; 
            a = a.next; 
        }
        else
        { 
            current.next = b;
            b = b.next; 
        }

    }
    current.next = (a == null) ? b : a;
    return dummyHead.next;
}

//Finding the middle element of the list for splitting
public static Node getMiddle(Node head)
{
    if(head == null) 
        return head;

    Node slow = head, fast = head;

    while(fast.next != null && fast.next.next != null)
    {
        slow = slow.next;
        fast = fast.next.next;
    }
    return slow;
}
17 голосов
/ 13 июня 2010

Более простой / понятной реализацией может быть рекурсивная реализация, из которой время выполнения NLog (N) является более ясным.

typedef struct _aList {
    struct _aList* next;
    struct _aList* prev; // Optional.
    // some data
} aList;

aList* merge_sort_list_recursive(aList *list,int (*compare)(aList *one,aList *two))
{
    // Trivial case.
    if (!list || !list->next)
        return list;

    aList *right = list,
          *temp  = list,
          *last  = list,
          *result = 0,
          *next   = 0,
          *tail   = 0;

    // Find halfway through the list (by running two pointers, one at twice the speed of the other).
    while (temp && temp->next)
    {
        last = right;
        right = right->next;
        temp = temp->next->next;
    }

    // Break the list in two. (prev pointers are broken here, but we fix later)
    last->next = 0;

    // Recurse on the two smaller lists:
    list = merge_sort_list_recursive(list, compare);
    right = merge_sort_list_recursive(right, compare);

    // Merge:
    while (list || right)
    {
        // Take from empty lists, or compare:
        if (!right) {
            next = list;
            list = list->next;
        } else if (!list) {
            next = right;
            right = right->next;
        } else if (compare(list, right) < 0) {
            next = list;
            list = list->next;
        } else {
            next = right;
            right = right->next;
        }
        if (!result) {
            result=next;
        } else {
            tail->next=next;
        }
        next->prev = tail;  // Optional.
        tail = next;
    }
    return result;
}

NB. Для рекурсии предусмотрено требование хранения журнала (N). Производительность должна быть примерно сопоставима с другой стратегией, которую я опубликовал. Здесь возможна оптимизация путем запуска цикла слияния while (list && right) и простого добавления оставшегося списка (так как нас не волнует конец списков; достаточно знать, что они объединены).

10 голосов
/ 13 июня 2010

В большой степени основано на ОТЛИЧНОМ коде от: http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html

Слегка урезан и приведен в порядок:


typedef struct _aList {
    struct _aList* next;
    struct _aList* prev; // Optional.
       // some data
} aList;

aList *merge_sort_list(aList *list,int (*compare)(aList *one,aList *two))
{
    int listSize=1,numMerges,leftSize,rightSize;
    aList *tail,*left,*right,*next;
    if (!list || !list->next) return list;  // Trivial case

    do { // For each power of two<=list length
        numMerges=0,left=list;tail=list=0; // Start at the start

        while (left) { // Do this list_len/listSize times:
            numMerges++,right=left,leftSize=0,rightSize=listSize;
            // Cut list into two halves (but don't overrun)
            while (right && leftSize<listSize) leftSize++,right=right->next;
            // Run through the lists appending onto what we have so far.
            while (leftSize>0 || (rightSize>0 && right)) {
                // Left empty, take right OR Right empty, take left, OR compare.
                if (!leftSize)                  {next=right;right=right->next;rightSize--;}
                else if (!rightSize || !right)  {next=left;left=left->next;leftSize--;}
                else if (compare(left,right)<0) {next=left;left=left->next;leftSize--;}
                else                            {next=right;right=right->next;rightSize--;}
                // Update pointers to keep track of where we are:
                if (tail) tail->next=next;  else list=next;
                // Sort prev pointer
                next->prev=tail; // Optional.
                tail=next;          
            }
            // Right is now AFTER the list we just sorted, so start the next sort there.
            left=right;
        }
        // Terminate the list, double the list-sort size.
        tail->next=0,listSize<<=1;
    } while (numMerges>1); // If we only did one merge, then we just sorted the whole list.
    return list;
}

NB. Это гарантировано O (NLog (N)) и использует O (1) ресурсов (без рекурсии, без стека, ничего).

6 голосов
/ 11 августа 2008

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

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

Самый простой из Gonnet + Baeza Yates Справочник по алгоритмам . Вы называете это с нужным количеством отсортированных элементов, которые рекурсивно делятся пополам, пока не достигнет запроса на список первого размера, который вы затем просто снимаете с начала оригинального списка. Все они объединяются в полноразмерный отсортированный список.

[Обратите внимание, что в первом посте классная основанная на стеке запись называется Online Mergesort, и в упражнении в Knuth Vol 3 она упоминается мельчайшим образом]

2 голосов
/ 15 июля 2012

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

element* mergesort(element *head,long lengtho)
{ 
  long count1=(lengtho/2), count2=(lengtho-count1);
  element *next1,*next2,*tail1,*tail2,*tail;
  if (lengtho<=1) return head->next;  /* Trivial case. */

  tail1 = mergesort(head,count1);
  tail2 = mergesort(tail1,count2);
  tail=head;
  next1 = head->next;
  next2 = tail1->next;
  tail1->next = tail2->next; /* in case this ends up as the tail */
  while (1) {
    if(cmp(next1,next2)<=0) {
      tail->next=next1; tail=next1;
      if(--count1==0) { tail->next=next2; return tail2; }
      next1=next1->next;
    } else {
      tail->next=next2; tail=next2;
      if(--count2==0) { tail->next=next1; return tail1; }
      next2=next2->next;
    }
  }
}
2 голосов
/ 24 ноября 2015

Я решил проверить здесь примеры, а также еще один подход, изначально написанный Джонатаном Каннингемом в Pop-11. Я закодировал все подходы в C # и провел сравнение с рядом разных размеров списков. Я сравнил подход Mono eglib от Raja R Harinath, код C # от Shital Shah, подход Java от Jayadev, рекурсивные и нерекурсивные версии от David Gamble, первый код C от Ed Wynn (этот сбой произошел с моим примером набора данных, Я не отлаживал) и версию Каннингема. Полный код здесь: https://gist.github.com/314e572808f29adb0e41.git.

Mono eglib основан на идее, похожей на идею Каннингема, и имеет сравнимую скорость, если только список не отсортирован, и в этом случае подход Каннингема гораздо быстрее (если он частично отсортирован, eglib немного быстрее). Код eglib использует фиксированную таблицу для хранения рекурсии сортировки слиянием, тогда как подход Каннингема работает с использованием повышающихся уровней рекурсии - поэтому он начинается без рекурсии, затем рекурсии с 1 глубиной, затем рекурсии с 2 глубинами и так далее, в соответствии с сколько шагов нужно, чтобы сделать сортировку. Я считаю, что код Каннингема немного легче следовать, и нет никаких сомнений в том, насколько велика составление таблицы рекурсии, поэтому он получает мой голос. Другие подходы, которые я попробовал на этой странице, были в два или более раз медленнее.

Вот порт C # типа Pop-11:

/// <summary>
/// Sort a linked list in place. Returns the sorted list.
/// Originally by Jonathan Cunningham in Pop-11, May 1981.
/// Ported to C# by Jon Meyer.
/// </summary>
public class ListSorter<T> where T : IComparable<T> {
    SingleListNode<T> workNode = new SingleListNode<T>(default(T));
    SingleListNode<T> list;

    /// <summary>
    /// Sorts a linked list. Returns the sorted list.
    /// </summary>
    public SingleListNode<T> Sort(SingleListNode<T> head) {
        if (head == null) throw new NullReferenceException("head");
        list = head;

        var run = GetRun(); // get first run
        // As we progress, we increase the recursion depth. 
        var n = 1;
        while (list != null) {
            var run2 = GetSequence(n);
            run = Merge(run, run2);
            n++;
        }
        return run;
    }

    // Get the longest run of ordered elements from list.
    // The run is returned, and list is updated to point to the
    // first out-of-order element.
    SingleListNode<T> GetRun() {
        var run = list; // the return result is the original list
        var prevNode = list;
        var prevItem = list.Value;

        list = list.Next; // advance to the next item
        while (list != null) {
            var comp = prevItem.CompareTo(list.Value);
            if (comp > 0) {
                // reached end of sequence
                prevNode.Next = null;
                break;
            }
            prevItem = list.Value;
            prevNode = list;
            list = list.Next;
        }
        return run;
    }

    // Generates a sequence of Merge and GetRun() operations.
    // If n is 1, returns GetRun()
    // If n is 2, returns Merge(GetRun(), GetRun())
    // If n is 3, returns Merge(Merge(GetRun(), GetRun()),
    //                          Merge(GetRun(), GetRun()))
    // and so on.
    SingleListNode<T> GetSequence(int n) {
        if (n < 2) {
            return GetRun();
        } else {
            n--;
            var run1 = GetSequence(n);
            if (list == null) return run1;
            var run2 = GetSequence(n);
            return Merge(run1, run2);
        }
    }

    // Given two ordered lists this returns a list that is the
    // result of merging the two lists in-place (modifying the pairs
    // in list1 and list2).
    SingleListNode<T> Merge(SingleListNode<T> list1, SingleListNode<T> list2) {
        // we reuse a single work node to hold the result.
        // Simplifies the number of test cases in the code below.
        var prevNode = workNode;
        while (true) {
            if (list1.Value.CompareTo(list2.Value) <= 0) {
                // list1 goes first
                prevNode.Next = list1;
                prevNode = list1;
                if ((list1 = list1.Next) == null) {
                    // reached end of list1 - join list2 to prevNode
                    prevNode.Next = list2;
                    break;
                }
            } else {        // same but for list2
                prevNode.Next = list2;
                prevNode = list2;
                if ((list2 = list2.Next) == null) {
                    prevNode.Next = list1;
                    break;
                }
            }
        }

        // the result is in the back of the workNode
        return workNode.Next;
    }
}
2 голосов
/ 27 декабря 2014

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

В приведенном ниже коде содержится минимальное количество переменных и минимальное количество логических шагов, необходимых для алгоритма (т. Е., Насколько я знаю, без кода, который нельзя поддерживать / нечитать). Однако я не пытался минимизировать LOC и оставил столько пустого пространства, сколько необходимо для удобства чтения. Я тестировал этот код с помощью довольно строгих модульных тестов.

Обратите внимание, что этот ответ сочетает в себе несколько приемов из другого ответа https://stackoverflow.com/a/3032462/207661. Пока код написан на C #, преобразование в C ++, Java и т. Д. Должно быть тривиальным

SingleListNode<T> SortLinkedList<T>(SingleListNode<T> head) where T : IComparable<T>
{
    int blockSize = 1, blockCount;
    do
    {
        //Maintain two lists pointing to two blocks, left and right
        SingleListNode<T> left = head, right = head, tail = null;
        head = null; //Start a new list
        blockCount = 0;

        //Walk through entire list in blocks of size blockCount
        while (left != null)
        {
            blockCount++;

            //Advance right to start of next block, measure size of left list while doing so
            int leftSize = 0, rightSize = blockSize;
            for (;leftSize < blockSize && right != null; ++leftSize)
                right = right.Next;

            //Merge two list until their individual ends
            bool leftEmpty = leftSize == 0, rightEmpty = rightSize == 0 || right == null;
            while (!leftEmpty || !rightEmpty)
            {
                SingleListNode<T> smaller;
                //Using <= instead of < gives us sort stability
                if (rightEmpty || (!leftEmpty && left.Value.CompareTo(right.Value) <= 0))
                {
                    smaller = left; left = left.Next; --leftSize;
                    leftEmpty = leftSize == 0;
                }
                else
                {
                    smaller = right; right = right.Next; --rightSize;
                    rightEmpty = rightSize == 0 || right == null;
                }

                //Update new list
                if (tail != null)
                    tail.Next = smaller;
                else
                    head = smaller;
                tail = smaller;
            }

            //right now points to next block for left
            left = right;
        }

        //terminate new list, take care of case when input list is null
        if (tail != null)
            tail.Next = null;

        //Lg n iterations
        blockSize <<= 1;

    } while (blockCount > 1);

    return head;
}

Достопримечательности

  • Не требуется специальной обработки для случаев, таких как нулевой список из списка 1 и т. Д. Эти дела "просто работают".
  • Во многих «стандартных» текстах алгоритмов есть два цикла для обхода оставшихся элементов для обработки случая, когда один список короче другого. Выше код устраняет необходимость в этом.
  • Мы уверены, что сортировка стабильна
  • Внутренний цикл while, который является горячей точкой, оценивает в среднем 3 выражения на итерацию, что, я думаю, является минимальным.

Обновление: @ ideasman42 имеет переведенный выше код на C / C ++ вместе с предложениями по исправлению комментариев и еще большим улучшением. Выше код теперь актуален с этими.

1 голос
/ 14 июля 2012

Вот моя реализация «сортировки по списку» Кнута (Алгоритм 5.2.4L от Vol.3 TAOCP, 2-е изд.). Я добавлю несколько комментариев в конце, но вот резюме:

При случайном вводе он работает немного быстрее, чем код Саймона Тэтхэма (см. Нерекурсивный ответ Дейва Гэмбла, со ссылкой), но немного медленнее, чем рекурсивный код Дейва Гэмбла. Это сложнее понять, чем либо. По крайней мере, в моей реализации требуется, чтобы каждый элемент имел ДВУХ указателей на элементы. (Альтернативой может быть один указатель и логический флаг.) Таким образом, это, вероятно, бесполезный подход. Тем не менее, одним из отличительных моментов является то, что он работает очень быстро, если на входе есть длинные отрезки, которые уже отсортированы.

element *knuthsort(element *list)
{ /* This is my attempt at implementing Knuth's Algorithm 5.2.4L "List merge sort"
     from Vol.3 of TAOCP, 2nd ed. */
  element *p, *pnext, *q, *qnext, head1, head2, *s, *t;
  if(!list) return NULL;

L1: /* This is the clever L1 from exercise 12, p.167, solution p.647. */
  head1.next=list;
  t=&head2;
  for(p=list, pnext=p->next; pnext; p=pnext, pnext=p->next) {
    if( cmp(p,pnext) > 0 ) { t->next=NULL; t->spare=pnext; t=p; }
  }
  t->next=NULL; t->spare=NULL; p->spare=NULL;
  head2.next=head2.spare;

L2: /* begin a new pass: */
  t=&head2;
  q=t->next;
  if(!q) return head1.next;
  s=&head1;
  p=s->next;

L3: /* compare: */
  if( cmp(p,q) > 0 ) goto L6;
L4: /* add p onto the current end, s: */
  if(s->next) s->next=p; else s->spare=p;
  s=p;
  if(p->next) { p=p->next; goto L3; } 
  else p=p->spare;
L5: /* complete the sublist by adding q and all its successors: */
  s->next=q; s=t;
  for(qnext=q->next; qnext; q=qnext, qnext=q->next);
  t=q; q=q->spare;
  goto L8;
L6: /* add q onto the current end, s: */
  if(s->next) s->next=q; else s->spare=q;
  s=q;
  if(q->next) { q=q->next; goto L3; } 
  else q=q->spare;
L7: /* complete the sublist by adding p and all its successors: */
  s->next=p;
  s=t;
  for(pnext=p->next; pnext; p=pnext, pnext=p->next);
  t=p; p=p->spare;
L8: /* is this end of the pass? */
  if(q) goto L3;
  if(s->next) s->next=p; else s->spare=p;
  t->next=NULL; t->spare=NULL;
  goto L2;
}
1 голос
/ 15 августа 2013

В mono eglib .

есть нерекурсивная слияние связанных списков.

Основная идея состоит в том, что цикл управления для различных слияний параллелен побитному приращению двоичного целого числа. Имеется O (n) слияний для «вставки» n узлов в дерево слияний, и ранг этих слияний соответствует двоичной цифре, которая увеличивается. Используя эту аналогию, только O (log n) узлов дерева слияния должны быть материализованы в массив временного хранения.

...