实现链表的两种方法:哪种更好?

时间:2010-12-01 10:49:00

标签: c linked-list

我通常知道在C中设计通用链表数据结构的两种方法。我想知道哪个更好。在提出问题之前,我将很快介绍这两种方法:

一种方法是围绕如下结构构建函数:

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

显然,数据指针指向有效负载。 list元素struct在有效负载之外。这是例如glib如何设计其双链表设施:http://library.gnome.org/devel/glib/2.26/glib-Doubly-Linked-Lists.html

另一种方法是在Linux内核中完成它的方式:http://isis.poly.edu/kulesh/stuff/src/klist/。 list元素结构中没有指向有效负载的void指针。相反,list元素结构包含在有效负载结构中:

struct list_element {
    struct list_element *prev;
    struct list_element *next;
};

struct person {
    char name[20];
    unsigned int age;
    struct list_element list_entry;
};

一个特殊的宏用于获取指向有效负载结构的指针,给定指向list_entry的指针,其名称包含有效负载结构和有效负载结构的类型(list_entry()宏)。

最后,这里是一个问题:后两种构建链表的方法有什么优势?有几次我听说有人说第二种比第一种更“通用”,但为什么呢?我甚至认为第一种方法更通用,因为有效载荷结构与列表实现无关,而第二种方法则不然。 第二种方法的另一个缺点是,如果要将有效负载放在多个列表中,则应该为有效负载结构中的每个列表使用struct list_element成员。

编辑: 总结到目前为止,我看到了两个对我来说很重要的答案:

  • 使用第一种方法:从列表中删除有效负载涉及循环遍历完整列表,直到找到指向有效负载的列表元素。您不需要使用第二种方法执行此操作。 (Patrick的回答)
  • 使用第一种方法,您必须为每个元素执行两个malloc():一个用于有效负载,另一个用于list元素struct。使用第二种方法,一个malloc()就足够了。 (罗迪的回答)

7 个答案:

答案 0 :(得分:10)

这是课程的马匹。

第一种方法效率较低,因为它通常需要每个列表元素有两个malloc()free(),以及一个额外的指针间接访问它们 - 当然还有指针的存储空间。

但是,它允许不同的列表元素具有不同大小的有效载荷,这对于第二种方法可能更加尴尬。

对于第二种方法,我会重新排序结构,因此列表元素在开始时 - 这样就可以为不同的有效负载大小提供一些灵活性。

struct person {
    struct list_element list_entry;
    unsigned int age;
    char name[20];  // now could be variable length.
};

答案 1 :(得分:8)

第一种方法可能看起来不那么具有侵入性,但在很多情况下并非如此(除非您添加其他数据结构)。

想象一下,您有一千个人的列表,并且您想从列表中删除其中一个人。如果此人不知道列表中的位置,则必须先扫描整个列表以获取该人的确切位置。

你可以通过在人员的相应列表结构中添加一个指针来解决这个问题,但这会破坏解决方案的非侵入性(这个词是否存在?)。

另一种选择是使用哈希映射将人的内存地址映射到列表节点的内存地址。然后在列表中找到节点要快得多(但仍比入侵方式慢)。但是,由于这会占用更多的内存,我建议不要这样做。

因此,最简单和最简单的解决方案是第二个。

答案 2 :(得分:0)

第一个更好,因为你可以拥有完全没有数据的列表节点。

使用第二个选项,无论实际使用情况如何,您总是使用空格(例如名称的20个字符)。

答案 3 :(得分:0)

第二种方法是一个侵入性列表。您必须修改要存储在列表中的结构。由于间接性较低,您可以通过这种方法获得一点性能。如果您需要更灵活的解决方案而不是最后一点性能,则应使用第一种方法。

答案 4 :(得分:0)

第二种方法是'侵入';它需要修改列表中的类型。列表(或列表)上的类型必须知道它在列表中。您必须能够修改结构以将其放在列表中。

第一种方法不是侵入性的。它不需要修改结构。您可以在列表中添加任何类型。你甚至可以在一个列表中使用异构类型,尽管这会遇到问题。但是,即使您无法修改基本类型,也可以将其放在第一种类型的列表中。相反,它需要更多的空间。

因此,如果您可以完全控制要放在列表中的数据类型(并且可以对其进行修改以支持您需要的列表),则第二种类型比第一种类型具有一些优势。在Linux内核的上下文中,满足前提条件并且它是有意义的。否则,第一种类型更灵活,但开销略高。

答案 5 :(得分:0)

我认为这更像是一个概念/分析问题。您正在处理具有列表的实体或您是否拥有实例列表的实体?

换句话说,如果您在数据中管理的内容具有独立存在,则第一个是有意义的,因为任何数据点都将被独立操作。如果数据总是并且必然是列表的一部分,那么第二种方法可能更清楚。

与大多数设计决策一样,最重要的标准应该更清晰,更明显。

答案 6 :(得分:0)

我认为这是一个非常主观的问题,因为没有给出比较两者的标准。

对于简单列表,我倾向于使用两者的组合。

struct list_node {
    struct list_node *  prev;
    struct list_node *  next;
};

struct some_struct {
    struct list_node  node;
    ...
};

虽然这看起来几乎与第二个相同,但请注意链表节点是“some_struct”的第一个元素。这意味着当您前进到下一个节点或倒回到列表中的上一个节点时,指针位于结构的开头。否则我将被迫执行一些指针数学运算以获得“some_struct”的开头。就目前而言,我可以简单地演员。

然而,这种方法确实有其局限性。例如,如果我想要一个具有多个链表的结构,则每个列出的方法都会遇到这样的缺陷:它需要指针算法才能到达至少一个结构的起点。为了解决这个问题,一些实现(例如BSD VFS代码中的实现)使用宏来创建链表元素。在这些中,链接列表始终指向结构的开头,但宏包含用于在结构中自动应用节点偏移的代码(如果您需要它(用于前进到下一个,或者重绕前一个)。) p>

希望这有帮助。

编辑:修正了一些术语。