web-dev-qa-db-ja.com

二重リンクリスト-マージソート後にリストを更新->末尾

二重リンクリストの実装では、典型的な構造を使用しています。

struct node
{
    void *data;
    struct node *prev;
    struct node *next;
};

また、リストの最後にO(1)時間で挿入するので、structheadを格納する別のtailがあります。

struct linklist
{
    struct node *head;
    struct node *tail;
    size_t size;
};

プログラムはすべての挿入および削除操作で期待どおりに機能しますが、ソート機能に問題があります。リストをソートするのが最も効果的または最も効果的な方法の1つであることを理解しているため、マージソートアルゴリズムを使用しています。アルゴリズムはうまく機能します:

static struct node *split(struct node *head)
{
    struct node *fast = head;
    struct node *slow = head;

    while ((fast->next != NULL) && (fast->next->next != NULL))
    {
        fast = fast->next->next;
        slow = slow->next;
    }

    struct node *temp = slow->next;

    slow->next = NULL;
    return temp;
}

static struct node *merge(struct node *first, struct node *second, int (*comp)(const void *, const void *))
{
    if (first == NULL)
    {
        return second;
    }
    if (second == NULL)
    {
        return first;
    }
    if (comp(first->data, second->data) < 0)
    {
        first->next = merge(first->next, second, comp);
        first->next->prev = first;
        first->prev = NULL;
        return first;
    }
    else
    {
        second->next = merge(first, second->next, comp);
        second->next->prev = second;
        second->prev = NULL;
        return second;
    }
}

static struct node *merge_sort(struct node *head, int (*comp)(const void *, const void *))
{
    if ((head == NULL) || (head->next == NULL))
    {
        return head;
    }

    struct node *second = split(head);

    head = merge_sort(head, comp);
    second = merge_sort(second, comp);
    return merge(head, second, comp);
}

しかし、list->tailのアドレスを更新し続ける方法がわかりません。

void linklist_sort(struct linklist *list, int (*comp)(const void *, const void *))
{
    list->head = merge_sort(list->head, comp);
    // list->tail is no longer valid at this point
}

確かに、注文後にリスト全体を調べてlist->tailをブルートフォースで更新できますが、もっとおもしろい方法があるかどうか知りたいのですが。

循環リストを使用して問題を解決することができましたが、プログラムの構造を変更しないようにしたいと思います。

3
David Ranieri

私はアルゴリズムBig-O表記に関する詳細な分析を提供するのに最適な人ではありません。とにかく、すでに受け入れられている「標準的な」答えで質問に答えることは素晴らしいです。あまり圧力をかけずに代替ソリューションを探索する可能性があるからです。
これは、後でわかるように、興味深いものです分析されたソリューションは、質問で提示された現在のソリューションよりも優れていません


戦略は、コードをひっくり返すことなくテール要素の候補を追跡することが可能かどうか疑問に思うことから始まります。主な候補は、ソートされたリスト内のノードの順序を決定する関数、merge()関数です。

ここで、比較後、ソートされたリストの最初に来るノードを決定するので、末尾に近い "loser"になります。 。したがって、各ステップの現在のテール要素とさらに比較すると、最終的にはtail要素を "敗者の敗者"

マージ関数には、追加の_struct node **tail_パラメーターがあります(リストtailフィールドをその場で変更するため、ダブルポインターが必要です

_static struct node *merge(struct node *first, struct node *second, struct node **tail, int (*comp)(const void *, const void *))
{
    if (first == NULL)
    {
        return second;
    }
    if (second == NULL)
    {
        return first;
    }
    if (comp(first->data, second->data) < 0)
    {
        first->next = merge(first->next, second, tail, comp);

        /* The 'second' node is the "loser". Let's compare current 'tail' 
           with it, and in case it loses again, let's update  'tail'.      */
        if( comp(second->data, (*tail)->data) > 0)
            *tail = second;
        /******************************************************************/

        first->next->prev = first;
        first->prev = NULL;
        return first;
    }
    else
    {
        second->next = merge(first, second->next, tail, comp);

        /* The 'first' node is the "loser". Let's compare current 'tail' 
           with it, and in case it loses again, let's update  'tail'.      */
        if( comp(first->data, (*tail)->data) > 0)
            *tail = first;
        /******************************************************************/

        second->next->prev = second;
        second->prev = NULL;
        return second;
    }
}
_

merge_sort()およびlinklist_sort()関数によるtailダブルポインターパラメーターの「伝播」を除いて、コードに必要な変更はこれ以上ありません。

_static struct node *merge_sort(struct node *head, struct node **tail, int (*comp)(const void *, const void *));

void linklist_sort(List_t *list, int (*comp)(const void *, const void *))
{
    list->head = merge_sort(list->head, &(list->tail), comp);
}
_

テスト

この変更をテストするには、基本的なinsert()関数、降順で並べ替えられたリストを取得するように設計されたcompare()関数、およびprintList()ユーティリティを作成する必要がありました。それから私はすべてのものをテストするメインプログラムを書きました。

私はいくつかのテストを行いました。ここで私は例を示しますが、この回答では質問と上記で提示された機能を省略しています:

_#include <stdio.h>

typedef struct node
{
    void *data;
    struct node *prev;
    struct node *next;
} Node_t;

typedef struct linklist
{
    struct node *head;
    struct node *tail;
    size_t size;
} List_t;

void insert(List_t *list, int data)
{
    Node_t * newnode = (Node_t *) malloc(sizeof(Node_t) );
    int * newdata = (int *) malloc(sizeof(int));
    *newdata = data;

    newnode->data = newdata;
    newnode->prev = list->tail;
    newnode->next = NULL;
    if(list->tail)
        list->tail->next = newnode;

    list->tail = newnode;

    if( list->size++ == 0 )
        list->head = newnode;   
}

int compare(const void *left, const void *right)
{
    if(!left && !right)
        return 0;

    if(!left && right)
        return 1;
    if(left && !right)
        return -1;

    int lInt = (int)*((int *)left), rInt = (int)*((int *)right);

    return (rInt-lInt); 
}

void printList( List_t *l)
{
    for(Node_t *n = l->head; n != NULL; n = n->next )
    {
        printf( " %d ->", *((int*)n->data));
    }
    printf( " NULL (tail=%d)\n", *((int*)l->tail->data));
}


int main(void)
{
  List_t l = { 0 };

  insert( &l, 5 );
  insert( &l, 3 );
  insert( &l, 15 );
  insert( &l, 11 );
  insert( &l, 2 );
  insert( &l, 66 );
  insert( &l, 77 );
  insert( &l, 4 );
  insert( &l, 13 );
  insert( &l, 9 );
  insert( &l, 23 );

  printList( &l );

  linklist_sort( &l, compare );

  printList( &l );

  /* Free-list utilities omitted */

  return 0;
}
_

この特定のテストでは、次の出力が得られました。

_ 5 -> 3 -> 15 -> 11 -> 2 -> 66 -> 77 -> 4 -> 13 -> 9 -> 23 -> NULL (tail=23)
 77 -> 66 -> 23 -> 15 -> 13 -> 11 -> 9 -> 5 -> 4 -> 3 -> 2 -> NULL (tail=2)
_

結論

  • 良いニュースは、理論的に言えば、最悪の場合でもO(N log(N))時間の複雑さを持つアルゴリズムがまだあるということです。
  • 悪い知らせは、リンクされたリスト(N "単純なステップ")での線形検索を回避するために、関数の呼び出しを含むN * logN比較を行わなければならないことです。 これにより、線形検索がさらに優れたオプションになります
1
Roberto Caboni

1つのオプションは、単一のリストノードであるかのようにノードをマージソートすることです。その後、完了時に1回のパスを作成して前のポインターを設定し、テールポインターを更新します。

別のオプションは、C++ std :: listおよびstd :: list :: sortに類似したものを使用します。循環二重リンクリストが使用されます。 「next」を「head」、「prev」を「tail」として使用するダミーノードが1つあります。ソートとマージをマージするパラメーターはイテレーターまたはポインターであり、ノードが元のリスト内で移動することによってマージされるため、実行境界を追跡するためにのみ使用されます。マージ関数は、std :: list :: spliceを使用して、2回目の実行から最初の実行にノードをマージします。ロジックは、最初の実行要素が2番目の実行要素以下である場合、イテレータまたはポインタを最初の実行に進めるか、2番目の実行からノードを削除して、最初の実行の現在のノードの前に挿入します。これは、削除+挿入ステップに関係する場合、ダミーノードのヘッドポインターとテールポインターを自動的に更新します。

構造体ノードを次のように変更します:

struct node
{
    struct node *next;           // used as head for dummy node
    struct node *prev;           // used as tail for dummy node
    void *data;
};

もう少し一般的です。

リストの作成時にダミーノードが割り当てられるため、開始==ダミー->次、最終==ダミー->前、終了==ダミー。

1
rcgldr