繁体   English   中英

双向链表 - 合并排序后更新 list->tail

[英]Doubly linked list - Update list->tail after a merge sort

在双向链表的实现中,我使用了典型的结构:

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

我还将在 O(1) 时间内在列表末尾插入,所以我有另一个存储headtailstruct

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

该程序对所有插入和删除操作都按预期工作,但我对排序 function 有问题,我正在使用合并排序算法,据我所知,它是最有效或最有效的排序列表之一,算法运行良好:

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后通过蛮力遍历整个列表,但我想知道是否有更好的方法来做到这一点。

我设法使用循环列表解决了这个问题,但我想避免改变程序的结构。

您的算法通过在每个步骤的merge function 中递归来使用 O(N) 堆栈空间。 使用这种方法,跟踪tail节点会非常麻烦。 您可以简单地扫描列表以找到它并更新linklist_sort中的list结构。 这个额外的步骤不会改变排序操作的复杂性。 您可以从link->tail的当前值开始节省一些时间:如果列表已经排序,则循环将立即停止。

这是修改后的版本:

void linklist_sort(struct linklist *list, int (*comp)(const void *, const void *)) {
    list->head = merge_sort(list->head, comp);
    if (list->tail) {
        struct node *tail = list->tail;
        while (tail->next)
            tail = tail->next;
        list->tail = tail;
    }
}

使用合并排序对链表进行排序应该只使用O(log(N))空间和O(N log(N))时间。

以下是改进此算法的一些想法:

  • 由于您知道列表的长度,因此您无需扫描完整列表进行拆分。 您可以将长度与列表指针一起传递,并使用它来确定拆分位置并仅扫描列表的一半。

  • 如果将merge转换为非递归版本,则可以跟踪合并阶段的最后一个节点并更新作为参数传递的指针struct node **tailp以指向最后一个节点。 这将保存最后一次扫描,并且删除递归将降低空间复杂度。 这是否提高了效率并不明显,基准测试会告诉我们。

  • 从经验来看,使用指向列表节点的辅助数组 N 指针更有效地实现对链表进行单向排序,更何况是双链表排序。 您将对该数组进行排序并根据排序数组的顺序重新链接节点。 额外的要求是O(N)大小。

这是使用列表长度和非递归merge的修改版本:

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

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

static struct node *split(struct node *head, size_t pos) {
    struct node *slow = head;

    while (pos-- > 1) {
        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 *))
{
    struct node *head = NULL;
    struct node *prev = NULL;
    struct node **linkp = &head;

    for (;;) {
        if (first == NULL) {
            second->prev = prev;
            *linkp = second;
            break;
        }
        if (second == NULL) {
            first->prev = prev;
            *linkp = first;
            break;
        }
        if (comp(first->data, second->data)) <= 0 {
            first->prev = prev;
            prev = *linkp = first;
            linkp = &first->next;
        } else {
            second->prev = prev;
            prev = *linkp = second;
            linkp = &second->next;
        }
    }
    return head;
}

static struct node *merge_sort(struct node *head, size_t size,
                               int (*comp)(const void *, const void *))
{
    if (size < 2) {
        return head;
    }

    struct node *second = split(head, size / 2);

    head = merge_sort(head, size / 2, comp);
    second = merge_sort(second, size - size / 2, comp);
    return merge(head, second, comp);
}

void linklist_sort(struct linklist *list, int (*comp)(const void *, const void *)) {
    list->head = merge_sort(list->head, comp, list->size);
    if (list->tail) {
        struct node *tail = list->tail;
        while (tail->next)
            tail = tail->next;
        list->tail = tail;
    }
}

请注意,您还可以简化merge function 并且在排序期间不更新反向指针,因为您可以在最后一次扫描期间重新链接整个列表。 最后一次扫描会更长,缓存友好度更低,但它应该仍然更有效,更不容易出错。

一种选择是将节点合并排序,就好像它们是单个列表节点一样,然后在完成后进行一次传递以设置先前的指针,并更新尾指针。

另一种选择是使用类似于 C++ std::list 和 std::list::sort 的东西。 使用循环双向链表。 有一个虚拟节点使用“next”作为“head”,“prev”作为“tail”。 合并排序和合并的参数是迭代器或指针,仅用于跟踪运行边界,因为节点是通过在原始列表中移动它们来合并的。 合并 function 使用 std::list::splice 将第二次运行的节点合并到第一次运行。 逻辑是如果第一个运行元素小于或等于第二个运行元素,只需将迭代器或指针推进到第一次运行,否则从第二次运行中删除节点并将其插入到第一次运行中的当前节点之前。 如果涉及删除 + 插入步骤,这将自动更新虚拟节点中的头和尾指针。

将结构节点更改为:

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

会更通用一点。

由于虚拟节点是在创建列表时分配的,所以 begin == dummy->next,last == dummy-> prev,end == dummy。

我不是提供有关算法Big-O表示法的深入分析的最佳人选。 无论如何,用已经接受的“规范”答案来回答问题是很棒的,因为有可能在没有太大压力的情况下探索替代解决方案。
这很有趣,即使如您所见,分析的解决方案并不比问题中提出的当前解决方案更好


该策略首先想知道是否可以在不颠倒代码的情况下跟踪候选尾部元素。 主要候选者是 function 决定排序列表中节点的顺序: merge() function。

现在,因为在比较之后我们决定哪个节点将在排序列表中排在第一位,我们将有一个更接近尾部的“失败者” 因此,通过与每个步骤的当前尾部元素进行进一步比较,最终我们将能够使用“失败者中的失败者”更新tail元素。

合并 function 将具有额外的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 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() function,一个compare() function,设计用于按降序获取排序列表,以及一个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;
}

在这个特定的测试中,我得到了以下 output:

 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比较,包括调用 function。 这使得线性搜索仍然是一个更好的选择

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM