【问题标题】:How to improve this flawed doubly-linked list insert implementation efficiently?如何有效地改进这个有缺陷的双向链表插入实现?
【发布时间】:2016-08-27 00:55:20
【问题描述】:

假设你在 C 中有一个简单的 DLL(双向链表)实现,如下所示:

struct list {
  list* prev;
  list* next;
}

您打算像这样在list 中实现元素:

struct element {
  int value;
  struct list link;
}

使用像这样的insert 操作:

void list_insert(struct list* list, struct list* element) {
    element->prev = list;
    element->next = list->next;
    list->next = element;
    element->next->prev = element;
}

最后,你可以像这样使用你的实现:

// Initialize empty list
struct list mylist;
mylist.next = &mylist;
mylist.prev = &mylist;

// Initialize element
struct element e;
e.value = 42;

// Add element to list
list_insert(&mylist, &e.link);

这基本上工作得很好,但对于无意中将e 插入mylist 两次 的可能性似乎并不可靠。什么是处理这个问题的计算效率高的方法,理想情况下是 O(1) 时间?

参考:列表设计来自Wayland项目。请参阅wayland-util.hwayland-util.c

【问题讨论】:

  • 初始化你的元素 s.t. prev = next = 0,检查要插入的元素,如果它们实际上是未链接的元素。
  • 将静态变量添加到链表中似乎是不明智的。当您添加副本时,您的所有疑虑都会消失吗?
  • struct element 中嵌入struct list 指针似乎很可疑;您必须为 element 和它使用的 list 分配内存。如果您保留struct list,那么您应该在struct element 中拥有struct list link;,以使您保持理智。不过,大多数情况下,人们只是将struct element *next; struct element *prev; 直接嵌入struct element,完全没有struct list。在其他问题中,struct list 的元素被定义为指向 struct list 项目,而不是 struct element 项目——这很难解决。
  • list_insert() 的第二个参数被声明为list*,但您使用&e 调用它,即element*
  • 我看不出这个实现是如何工作的。当您按照nextprev 链接访问另一个列表项时,您如何获得它的值?您没有从 listelement 的任何指针。

标签: c linked-list


【解决方案1】:

您已经组合和/或混淆了列表和列表元素结构。列表通常具有 headnext 元素,而不是 nextprev。后者通常用于元素 [而不是列表]。

而且,除非你真的想要一个“列表列表”,否则列表结构中的指针必须是指向元素的指针,而 不是 列表。而且,您不希望将该列表结构中的 head/tail 指针初始化为指向同一个列表。

另外,仅仅因为一个列表有两个指针,使它成为一个双重链表。如果元素only 有一个next 指针,它可能是一个singly 链表。为了使它成为一个双重链表,每个元素都需要一个nextprev指针。

还有更多内容。我已经为您的程序创建了一个经过清理和注释的版本。请注意其他更改:

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

// element within list
typedef struct element {
    int value;                          // data value
    struct element *next;               // pointer to next element in list
    struct element *prev;               // pointer to previous element in list
} element_t;

// list of elements
typedef struct list {
    element_t *head;                    // pointer to first element in list
    element_t *tail;                    // pointer to last element in list
} list_t;

// new_element -- get new element
element_t *
new_element(int value)
{
    element_t *newptr;

    newptr = calloc(1,sizeof(element_t));

    if (newptr == NULL) {
        printf("new_element: malloc failure\n");
        exit(1);
    }

    newptr->value = value;

    return newptr;
}

// list_insert -- insert before list head
void
list_insert(list_t *list,element_t *newptr)
{
    element_t *head;

    head = list->head;

    newptr->prev = NULL;
    newptr->next = head;

    // insert at head of empty list
    if (head == NULL)
        list->tail = newptr;

    // insert at head of non-empty list
    else {
        newptr->next = head;
        head->prev = newptr;
    }

    // make new element the head of the list
    list->head = newptr;
}

// list_find_value -- find existing list element by value
element_t *
list_find_value(list_t *list,int value)
{
    element_t *curptr;

    // search for pre-existing element with the same value
    for (curptr = list->head;  curptr != NULL;  curptr = curptr->next) {
        if (curptr->value == value)
            break;
    }

    return curptr;
}

// list_insert_unique -- insert before list head
void
list_insert_unique(list_t *list,element_t *newptr)
{
    element_t *curptr;

    // search for pre-existing element with the same value
    curptr = list_find_value(list,newptr->value);

    // only insert if we did _not_ find one
    if (curptr == NULL)
        list_insert(list,newptr);
    else
        free(newptr);
}

// list_print -- print the list
void
list_print(list_t *list)
{
    element_t *curptr;

    printf("list_print:");

    // search for pre-existing element with the same value
    for (curptr = list->head;  curptr != NULL;  curptr = curptr->next)
        printf(" %d",curptr->value);

    printf("\n");
}

// list_print -- print the list in reverse order
void
list_rprint(list_t *list)
{
    element_t *curptr;

    printf("list_rprint:");

    // search for pre-existing element with the same value
    for (curptr = list->tail;  curptr != NULL;  curptr = curptr->prev)
        printf(" %d",curptr->value);

    printf("\n");
}

// main -- main program
int
main(void)
{
    list_t mylist;
    element_t *e;

    // initialize list
    mylist.head = NULL;
    mylist.tail = NULL;

    // Add unique element to list
    e = new_element(42);
    list_insert_unique(&mylist,e);

    e = new_element(23);
    list_insert_unique(&mylist,e);

    e = new_element(17);
    list_insert_unique(&mylist,e);

    // try to insert a duplicate
    e = new_element(17);
    list_insert_unique(&mylist,e);

    // try to insert a duplicate
    e = new_element(42);
    list_insert_unique(&mylist,e);

    // print list in order [checks the head/next pointers]
    list_print(&mylist);

    // print list in reverse order [checks the tail/prev pointers]
    list_rprint(&mylist);

    return 0;
}

更新:

但问题是要求改进给定列表实现的插入逻辑。

你已经做了足够长的时间知道做一些你在这个问题中做过的事情。

您发布的代码已损坏,甚至无法编译。如果它没有错误,则应该改为在 CodeReview 上询问。

因此,其余代码变得可疑。现在,在查看它之后,我猜你想要一个双向链接的循环列表。但是,对双向链表指针使用list 具有欺骗性。如果将其命名为link 会更清楚。

您询问了关于防止重复的问题 [in O(1)]:

对于无意中将e 插入mylist 两次的可能性似乎并不可靠。

这对于标准(即指针匹配、值匹配或两者)并不完全清楚。

您没有提供任何代码来检查重复项,只是 list_insert 在列表头部插入。

什么是处理这个问题的计算有效的方法,最好在 O(1) 时间内?

在列表头部插入 O(1),但任何检查重复项都需要完整的列表扫描,即 O(n)强>。这在我看来是基本的。不一定要弄清楚O(n),但至少知道它可能O(1)

因此,鉴于所有这些,如果您的代码被重构,您不应该感到惊讶。

即使 [现在] 知道链接的循环列表,我也不建议您按照您的方式进行操作,因此我进行了重构。使用您的方法,您“失去”对 value 的访问权限,因此基于值的重复扫描变得更加成问题。

我所做的是将链接嵌入到元素中。这使得几乎所有的代码都更简单。按照你的做法,element_t 是一个从link_t(nee list_t)“继承”的子类。这对于 C++ 来说是可以的,但它会使 C 中的代码有点混乱。我在下面创建了一个这样做的版本,只是为了显示代码变得多么复杂。

也许最令人震惊的是,您编辑了原始代码以“修复”损坏的部分。您应该知道这样做,但将修复附加到您的帖子底部,因为它会使答案不再与问题匹配。

此外,Wayland 代码/链接似乎与您发布的代码几乎没有匹配。您甚至没有尝试以任何特定方式将两者联系起来。


这里有一个更好的list_insert_unique函数:

// list_insert_unique -- insert before list head
void
list_insert_unique(list_t *list,element_t *newptr)
{
    int dupflg;
    element_t *curptr;

    // search for pre-existing element
    dupflg = 0;
    for (curptr = list->head;  curptr != NULL;  curptr = curptr->next) {
        // prevent list corruption
        if (curptr == newptr) {
            dupflg = -1;
            break;
        }

        // don't add duplicate values (we must continue to search for a
        // pointer match)
        if (curptr->value == value)
            dupflg = 1;
    }

    do {
        // we're trying to re-add the _same_ node
        if (dupflg < 0)
            break;

        // we're trying to add a different node but with an existing value
        if (dupflg) {
            free(newptr);
            break;
        }

        // only insert if we did _not_ find one
        list_insert(list,newptr);
    } while (0);
}

这是一个保留更多原始结构的变体。它是-循环的,但这种差异相对较小。请注意,它需要在超类和子类之间进行更多的“向上转型”和/或“向下转型”。

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

// element within list
typedef struct link link_t;
struct link {
    link_t *next;                   // pointer to next link in list
    link_t *prev;                   // pointer to previous link in list
};

// list of elements
typedef struct list {
    link_t *head;                   // pointer to first element in list
    link_t *tail;                   // pointer to last element in list
} list_t;

// value types
typedef enum {
    VTYPE_INT = 1,
    VTYPE_FLT = 2
} vtype_t;

// value within list
typedef struct value {
    link_t link;                    // list linkage
    vtype_t type;                   // value type
    union {
        int int_value;              // data value
        double flt_value;           // data value
    };
} element_t;

#define ELEMENTOF(_eptr)    ((element_t *) (_eptr))
#define LINKOF(_vptr)       ((link_t *) (_vptr))

// new_element -- get new element
element_t *
new_element(vtype_t type,...)
{
    va_list ap;
    element_t *newptr;

    newptr = calloc(1,sizeof(element_t));

    if (newptr == NULL) {
        printf("new_element: malloc failure\n");
        exit(1);
    }

    newptr->type = type;

    va_start(ap,type);

    switch (type) {
    case VTYPE_INT:
        newptr->int_value = va_arg(ap,int);
        break;
    case VTYPE_FLT:
        newptr->flt_value = va_arg(ap,double);
        break;
    }

    va_end(ap);

    return newptr;
}

// list_insert -- insert before list head [O(1)]
void
list_insert(list_t *list,link_t *newptr)
{
    link_t *head;

    head = list->head;

    newptr->prev = NULL;
    newptr->next = head;

    // insert at head of empty list
    if (head == NULL)
        list->tail = newptr;

    // insert at head of non-empty list
    else {
        newptr->next = head;
        head->prev = newptr;
    }

    // make new element the head of the list
    list->head = newptr;
}

// list_find_value -- find existing list element by value [O(n)]
link_t *
list_find_value(list_t *list,element_t *newval,int *matchptr)
{
    int dupflg;
    element_t *curval;
    link_t *curptr;

    // search for pre-existing element with the same value
    dupflg = 0;
    for (curptr = list->head;  curptr != NULL;  curptr = curptr->next) {
        curval = ELEMENTOF(curptr);

        // don't allow the list to become corrupted
        if (curval == newval) {
            dupflg = -1;
            break;
        }

        // only compare values of similar type
        if (curval->type != newval->type)
            continue;

        switch (newval->type) {
        case VTYPE_INT:
            if (curval->int_value == newval->int_value)
                dupflg = 1;
            break;
        case VTYPE_FLT:
            if (curval->flt_value == newval->flt_value);
                dupflg = 1;
            break;
        }
    }

    if (matchptr != NULL)
        *matchptr = dupflg;

    return curptr;
}

// list_insert_unique -- insert before list head
void
list_insert_unique(list_t *list,element_t *newval)
{
    link_t *curptr;
    int dupflg;

    // search for pre-existing element with the same value
    curptr = list_find_value(list,newval,&dupflg);

    // only insert if we did _not_ find one
    do {
        // prevent double free
        if (dupflg < 0)
            break;

        if (dupflg) {
            free(newval);
            break;
        }

        list_insert(list,LINKOF(newval));
    } while (0);
}

// value_print -- print a single value
void
value_print(link_t *curptr)
{
    element_t *valptr;

    valptr = ELEMENTOF(curptr);

    switch (valptr->type) {
    case VTYPE_INT:
        printf(" I:%d",valptr->int_value);
        break;
    case VTYPE_FLT:
        printf(" F:%g",valptr->flt_value);
        break;
    }
}

// list_print -- print the list
void
list_print(list_t *list)
{
    link_t *curptr;

    printf("list_print:");

    for (curptr = list->head;  curptr != NULL;  curptr = curptr->next)
        value_print(curptr);

    printf("\n");
}

// list_rprint -- print the list in reverse order
void
list_rprint(list_t *list)
{
    link_t *curptr;

    printf("list_rprint:");

    for (curptr = list->tail;  curptr != NULL;  curptr = curptr->prev)
        value_print(curptr);

    printf("\n");
}

// main -- main program
int
main(void)
{
    list_t mylist;
    element_t *e;

    // initialize list
    mylist.head = NULL;
    mylist.tail = NULL;

    // Add unique element to list
    e = new_element(VTYPE_INT,42);
    list_insert_unique(&mylist,e);

    e = new_element(VTYPE_INT,23);
    list_insert_unique(&mylist,e);

    e = new_element(VTYPE_INT,17);
    list_insert_unique(&mylist,e);

    // try to insert a duplicate
    e = new_element(VTYPE_INT,17);
    list_insert_unique(&mylist,e);

    // try to insert a duplicate
    e = new_element(VTYPE_INT,42);
    list_insert_unique(&mylist,e);

    e = new_element(VTYPE_INT,67);
    list_insert_unique(&mylist,e);

    list_insert_unique(&mylist,e);

    // change the value [which also changes the list's value] and try to
    // insert
    e->int_value = 66;
    list_insert_unique(&mylist,e);

    // this is _not_ a duplicate because the type is different
    e = new_element(VTYPE_FLT,42.0);
    list_insert_unique(&mylist,e);

    // print list in order [checks the head/next pointers]
    list_print(&mylist);

    // print list in reverse order [checks the tail/prev pointers]
    list_rprint(&mylist);

    return 0;
}

更新 #2:

超级跟进,谢谢。是的,我知道原始帖子的错误并对其进行了编辑以确保正确性;以及所提供实现的缺陷。

好吧,我认为我已经找到了 [我们的 :-)] 两个世界中最好的。

首先,这取决于重复的含义。最初,我认为这是一个重复的。我的 updated list_insert_unique 必须完整扫描到最后,即使它在中途找到 value 匹配,因为它必须完成扫描才能找到重复的 指针匹配。这迫使 O(n)

但是,如果您不/不介意重复值,而只是想检查列表中是否已有新节点(即允许重复值但防止列表损坏),则 可以 em> 使用不同的检测方法在 O(1) 内完成。

我已将我的 更新 list_insert_unique 拆分为两个函数:

list_insert_unique_value 非常相似。这是 O(n) 最坏 的情况,但平均为 O(n/2)。这是因为值扫描可以现在在找到值匹配后立即停止。

list_insert_unique_pointer 只检查新节点是否已经在列表中,并且总是 O(1)

这是可行的,因为它假设类似于我的 new_element 函数,它执行 calloc 将链接指针设置为 NULL。在调用list_insert 之前,如果不执行e.next = NULL; e.prev = NULL;,它不会在您原来的main 中工作。

以下是包含这些更改的原始版本。我仍然建议将链接嵌入到element_t 中的简单性,但这可以[轻松]适应我的第二个示例的超类/派生类模型[类似于您的原始]。

旁注:这些年来我已经完成了很多双向链接的实现,使用 both 样式,并且我更喜欢尽可能嵌入。实际上,我有多个独立的代码库,并且在一个中使用了“继承”模型,在另一个中使用了“嵌入”模型。我这样做是为了试验速度、易用性、检测列表损坏或链接损坏等。虽然 linux 内核使用循环列表,但我从来没有,主要是因为我找不到使它们循环的用例在我自己的代码中——YMMV。

下面的代码[仍然]是非循环的,但也可以适应循环列表。 for 循环必须更改为:

for (curptr = list->head->next;  curptr != head;  curptr = curptr->next)

对于我自己的代码,我 [would] 通常会在 CPP 宏中隐藏这样的丑陋,例如:

for (FORALL_FORWARD(list,curptr))

不管怎样,这里是:

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

// element within list
typedef struct element {
    int value;                          // data value
    struct element *next;               // pointer to next element in list
    struct element *prev;               // pointer to previous element in list
} element_t;

// list of elements
typedef struct list {
    element_t *head;                    // pointer to first element in list
    element_t *tail;                    // pointer to last element in list
} list_t;

// new_element -- get new element
element_t *
new_element(int value)
{
    element_t *newptr;

    newptr = calloc(1,sizeof(element_t));

    if (newptr == NULL) {
        printf("new_element: malloc failure\n");
        exit(1);
    }

    newptr->value = value;

    return newptr;
}

// list_insert -- insert before list head
void
list_insert(list_t *list,element_t *newptr)
{
    element_t *head;

    head = list->head;

    newptr->prev = NULL;
    newptr->next = head;

    // insert at head of empty list
    if (head == NULL)
        list->tail = newptr;

    // insert at head of non-empty list
    else {
        newptr->next = head;
        head->prev = newptr;
    }

    // make new element the head of the list
    list->head = newptr;
}

// list_insert_unique_pointer -- insert before list head [O(1)]
void
list_insert_unique_pointer(list_t *list,element_t *newptr)
{
    int dupflg;

    // check for being on the list -- works for both circular/non-circular
    // NOTE: for circular, this can be reduced to:
    //   dupflg = (newptr->prev != NULL)
    dupflg = (newptr == list->head) || (newptr->prev != NULL);
    //dupflg = -dupflg;

    do {
        // we're trying to re-add the _same_ node -- we'd corrupt the list
        if (dupflg)
            break;

        // only insert if we did _not_ find one
        list_insert(list,newptr);
    } while (0);
}

// list_insert_unique_value -- insert before list head [O(n)]
void
list_insert_unique_value(list_t *list,element_t *newptr)
{
    int dupflg;
    element_t *curptr;
    int value;

    curptr = list->head;

    // check for being on the list -- works for both circular/non-circular
    dupflg = (newptr == curptr) || (newptr->prev != NULL);
    dupflg = -dupflg;

    // search for pre-existing element with the same value
    value = newptr->value;
    for (;  curptr != NULL;  curptr = curptr->next) {
        if (dupflg)
            break;
        dupflg = (curptr->value == value);
    }

    do {
        // we're trying to re-add the _same_ node -- we'd corrupt the list
        if (dupflg < 0)
            break;

        // we're trying to add a different node but with an existing value
        if (dupflg) {
            free(newptr);
            break;
        }

        // only insert if we did _not_ find one
        list_insert(list,newptr);
    } while (0);
}

// list_print -- print the list
void
list_print(list_t *list)
{
    element_t *curptr;

    printf("list_print:");

    for (curptr = list->head;  curptr != NULL;  curptr = curptr->next)
        printf(" %d",curptr->value);

    printf("\n");
}

// list_print -- print the list in reverse order
void
list_rprint(list_t *list)
{
    element_t *curptr;

    printf("list_rprint:");

    for (curptr = list->tail;  curptr != NULL;  curptr = curptr->prev)
        printf(" %d",curptr->value);

    printf("\n");
}

// main -- main program
int
main(void)
{
    list_t mylist;
    element_t *e;

    // initialize list
    mylist.head = NULL;
    mylist.tail = NULL;

    // Add unique element to list
    e = new_element(42);
    list_insert_unique_value(&mylist,e);

    // try to corrupt the list
    list_insert_unique_pointer(&mylist,e);

    e = new_element(23);
    list_insert_unique_value(&mylist,e);

    // try to corrupt the list
    list_insert_unique_pointer(&mylist,e);

    e = new_element(17);
    list_insert_unique_value(&mylist,e);

    // try to insert a duplicate
    e = new_element(17);
    list_insert_unique_value(&mylist,e);

    // try to insert a duplicate
    e = new_element(42);
    list_insert_unique_value(&mylist,e);

    // print list in order [checks the head/next pointers]
    list_print(&mylist);

    // print list in reverse order [checks the tail/prev pointers]
    list_rprint(&mylist);

    return 0;
}

【讨论】:

  • 我知道列表的实现可能会有所不同,感谢您的回答!但问题是要求改进给定列表实现的插入逻辑。
  • 超级跟进,谢谢。是的,我知道原始帖子的错误并对其进行了编辑以确保正确性;以及所提供实现的缺陷。
  • 有必要解释一下 head/tail 列表和 circular 列表之间的区别(这似乎是 nextprev 指向第一个节点。)它们本质上是两种不同的方法来处理 circular 列表具有的列表的开头和结尾能够从任何节点遍历到任何节点而不需要在不同方向上迭代单独的指针的好处。
  • @DavidC.Rankin 是的,同意。我会补充一点。我知道其中的机制,但试图想出一个有益的实际用例(即一个实际工作的实际示例——也许是内核)。此外,一个空终止列表可以使用两个 for 循环作为循环遍历,或者如果一个设置了一个迭代器函数 [并将一个“当前”指针放入 list_t,我之前已经做过]。但是,明天,因为这里已经过了我的就寝时间了:-)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-11-16
  • 2012-05-10
  • 1970-01-01
  • 2012-02-18
  • 2021-11-18
  • 2011-06-03
相关资源
最近更新 更多