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

У меня есть код для сортировки слиянием с использованием связанного списка, он работает нормально, мой вопрос, какова сложность этого алгоритма? Это O (nlog (n))? Также он стабильный? Мне интересно, потому что, поскольку я знаю, что сортировка слиянием стабильна, как насчет с использованием связанного списка? если у нас есть элементы, некоторые из которых равны друг другу, сохраняет ли этот код порядок элементов? Большое спасибо

#include<stdio.h>
#include <stdlib.h>
struct node
{
    int number;
    struct node *next;

};
struct node *addnode(int number,struct node *next);
struct node*mergesort(struct node *head);
struct node *merge(struct node *one,struct node *two);

int main(void){
    struct node *head;
    struct node *current;
    struct node *next;
    int test[]={8,3,1,4,2,5,7,0,11,14,6};
    int n=sizeof(test)/sizeof(test[0]);
    int i;
    head=NULL;
     for (i=0;i<n;i++)
         head=addnode(test[i],head);
     i=0;
     head=mergesort(head);
    printf("before----after sort \n");
    for (current=head;current!=NULL;current=current->next)
        printf("%4d\t%4d\n",test[i++],current->number);

    /* free list */
    for (current=head;current!=NULL;current=current->next)
        next=current->next;free(current);
return 0;
}

struct node *addnode(int number,struct node* next){
    struct node *tnode;
    tnode=(struct node*)malloc(sizeof(*tnode));
    if(tnode!=NULL){
        tnode->number=number;
        tnode->next=next;
            }

     return tnode;
     }
struct node *mergesort(struct node *head){

    struct node *head_one;
    struct node *head_two;
    if((head==NULL) ||(head->next==NULL))
         return head;
    head_one=head;
    head_two=head->next;
    while( (head_two!=NULL) &&(head_two->next!=NULL)){
        head=head->next;
        head_two=head->next->next;
        }
    head_two=head->next;
    head->next=NULL;
    return merge(mergesort(head_one),mergesort(head_two));
    }
struct node *merge(struct node*head_one,struct node*head_two){

    struct node *head_three;
    if(head_one==NULL)
         return head_two;
    if(head_two==NULL)
         return head_one;
    if(head_one->number<head_two->number){

head_three=head_one;
head_three->next=merge(head_one->next,head_two);
    }
    else
    {

        head_three=head_two;
        head_three->next=merge(head_one,head_two->next);


    }

    return head_three;
    }

person dato datuashvili    schedule 08.03.2012    source источник
comment
Какой анализ вы уже сделали? Вы можете ответить на свой вопрос, запустив алгоритм на разных входах и построив график времени выполнения, а также проверив его стабильность.   -  person templatetypedef    schedule 09.03.2012
comment
Вопросы вроде какова сложность сортировки слиянием? и стабильная сортировка слиянием, тривиально легко ответить с помощью веб-поиска.   -  person David Heffernan    schedule 09.03.2012
comment
@DavidHeffernan: OP, похоже, знает о сложности и стабильности Mergesort в целом, но интересуется этой конкретной реализацией.   -  person ruakh    schedule 09.03.2012
comment
@templatetypedef: это даст только среднюю временную сложность.   -  person    schedule 09.03.2012


Ответы (3)


В вашем коде есть опечатка. После исправления он действительно стабилен и имеет O(n log n) сложность. Хотя, конечно, вам действительно следует переопределить свой merge как цикл вместо рекурсии. В C нет оптимизации хвостовых вызовов (верно?), Так что это может все испортить:

struct node *mergesort(struct node *head){

    struct node *head_one;
    struct node *head_two;
    if((head==NULL) ||(head->next==NULL))
         return head;
    head_one=head;
    head_two=head->next;
    while( (head_two!=NULL) &&(head_two->next!=NULL)){
        head=head->next;
        // head_two=head->next->next;      // -- the typo, corrected:
        head_two=head_two->next->next;
        }
    head_two=head->next;
    head->next=NULL;
    return merge(mergesort(head_one),mergesort(head_two));
    }

И пока мы занимаемся этим, измените рабочий процесс с

    return merge(mergesort(head_one),mergesort(head_two));

to

    struct node *p1, *p2; 
    // ......
    p1 = mergesort(head_one);
    p2 = mergesort(head_two);
    return merge(p1,p2);

таким образом будет намного проще работать со стеком (будет использоваться гораздо меньше его).

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

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

person Will Ness    schedule 09.03.2012
comment
Я, должно быть, что-то пропустил, но разве продвижение головы_одна и голова_два части O(n) не время само по себе? Тем не менее, как общая временная сложность все еще O(nlogn)? Потому что в массиве вы можете получить среднюю точку с O(1) временем без необходимости увеличивать быстрые и медленные указатели - person benjaminz; 11.11.2017
comment
@benjaminz да, продвижение указателей ~ 3n / 2; это позволяет разбить список на две половины; Таким образом, существует O (log n) шагов (повторное деление пополам является двойником повторного удвоения: x = 2 ^ n === log2 (x) = n). Слияние тоже O (n). - в массиве разделение составляет O (1), но слияние по-прежнему O (n), поэтому все еще n log n в целом для шагов log n. - Я отредактировал, чтобы упомянуть схему снизу вверх: тогда нет рекурсивного разделения, просто повторное слияние. - person Will Ness; 11.11.2017
comment
Спасибо @WillNess, я подумал еще раз и понял, что шаг слияния - O (n), поэтому продвижение, которое равно O (n / 2), не хуже, чем шаг слияния, поэтому это не имеет большого значения для общего времени работы O (nlogn) =) - person benjaminz; 11.11.2017
comment
@benjaminz продвижение составляет ~ 1.5n, и да, это не имеет значения по сложности. см. ответ Кристофа и обсуждение в его комментариях; также мое редактирование; для предпочтительного метода восходящей сортировки слиянием. - person Will Ness; 11.11.2017

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

  • не делите список рекурсивно пополам - произвольный доступ не бесплатен
  • не делить на подсписки размером 1 и n - 1, как объяснил ruakh

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

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

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

person Christoph    schedule 09.03.2012
comment
рекурсивное разделение односвязного списка пополам добавляет O(n log n) сложность алгоритму. То есть здесь постоянный коэффициент (после исправления опечатки; см. Мой ответ). Явное построение собственного стека нормально, если вы делаете это лучше, чем это уже делал писатель компилятора. Но вам это не нужно, чтобы построить отсортированный список снизу вверх - вам нужно всего лишь log n передать список с параметром размера n, равным 2,4,8, ..., при каждом повторном связывании-слиянии на месте блоки размером n при условии, что блоки размером n/2 уже отсортированы. ср. Ричард О'Киф такой код на Прологе и Схеме. - person Will Ness; 09.03.2012
comment
... так что это действительно отличная идея! Таким образом, нет разделения, только слияние. :) - person Will Ness; 10.03.2012
comment
@WillNess: фактический прирост производительности, конечно, будет зависеть от конкретного варианта использования, но я видел, как время выполнения сокращалось вдвое, заменяя рекурсивную версию версией на основе стека; кроме того, версия на основе стека является интерактивной, т.е. отлично подходит при чтении данных с диска (или других источников с задержкой) ... - person Christoph; 10.03.2012
comment
ага, on-line - значит, вы хотите объединить куски как можно скорее, но с перекосом. Таким образом, он также является более инкрементальным, имея минимальный элемент до сих пор, в то время как ввод потребляется. Отлично. Также возможно начальное разбиение по порядку прогонов, с различной длиной, повторное связывание при обнаружении даже для нисходящих. Но при вводе в память все это не нужно, стек не нужен, и вы просто выполняете полное сканирование списка по схеме 2,4,8 ..., объединяя куски вверх попарно, пока не останется только один. Конечно, все в цикле, без рекурсии. - person Will Ness; 10.03.2012

Сортировка слиянием означает разделение и слияние. Разделение в приведенном ниже фрагменте не идеально (оно приводит к квадратичному поведению при одинаково распределенных длинах прогонов, см. Комментарий Кристофа)), но оно поможет:

#include <stdio.h>
#include <string.h>

struct llist {
        struct llist *next;
        char *payload;
        };

int llist_cmp(struct llist *l, struct llist *r);
struct llist * llist_split(struct llist **hnd
                        , int (*cmp)(struct llist *l, struct llist *r) );
struct llist * llist_merge(struct llist *one, struct llist *two
                        , int (*cmp)(struct llist *l, struct llist *r) );
struct llist * llist_sort(struct llist *ptr
                        , int (*cmp)(struct llist *l, struct llist *r) );

struct llist * llist_split(struct llist **hnd, int (*cmp)(struct llist *l, struct llist *r) )
{
struct llist *this, *save, **tail;

for (save=NULL, tail = &save; this = *hnd; ) {
        if (! this->next) break;
        if ( cmp( this, this->next) <= 0) { hnd = &this->next; continue; }
        *tail = this->next;
        this->next = this->next->next;
        tail = &(*tail)->next;
        *tail = NULL;
        }
return save;
}

struct llist * llist_merge(struct llist *one, struct llist *two, int (*cmp)(struct llist *l, struct llist *r) )
{
struct llist *result, **tail;

for (result=NULL, tail = &result; one && two; tail = &(*tail)->next ) {
        if (cmp(one,two) <=0) { *tail = one; one=one->next; }
        else { *tail = two; two=two->next; }
        }
*tail = one ? one: two;
return result;
}
struct llist * llist_sort(struct llist *ptr, int (*cmp)(struct llist *l, struct llist *r) )
{
struct llist *save;

save=llist_split(&ptr, cmp);
if (!save) return ptr;

save = llist_sort(save, cmp);

return llist_merge(ptr, save, cmp);
}

int llist_cmp(struct llist *l, struct llist *r)

{
if (!l) return 1;
if (!r) return -1;
return strcmp(l->payload,r->payload);
}


struct llist lists[] =
{{ lists+1, "one" }
,{ lists+2, "two" }
,{ lists+3, "three" }
,{ lists+4, "four" }
,{ lists+5, "five" }
,{ lists+6, "six" }
,{ lists+7, "seven" }
,{ lists+8, "eight" }
,{ lists+9, "nine" }
,{ NULL, "ten" }
        };

int main()
{
struct llist *root,*tmp;

root = lists;

fprintf(stdout, "## %s\n", "initial:" );
for (tmp=root; tmp; tmp=tmp->next) {
        fprintf(stdout, "%s\n", tmp->payload);
        }

fprintf(stdout, "## %s\n", "sorting..." );
root = llist_sort(root, llist_cmp);
for (tmp=root; tmp; tmp=tmp->next) {
        fprintf(stdout, "%s\n", tmp->payload);
        }
fprintf(stdout, "## %s\n", "done." );

return 0;
}
person wildplasser    schedule 09.03.2012