版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43571807/article/details/100538865
wiki定义
链接列表
在计算机科学中,链表是数据元素的线性集合,其顺序不是由它们在内存中的物理位置给出的。相反,每个元素指向下一个元素。它是一个由节点集合组成的数据结构,这些节点一起表示一个序列。在其最基本的形式中,每个节点包含:数据和引用(换句话说,链接)到序列中的下一个节点。该结构允许在迭代期间从序列中的任何位置有效地插入或移除元素。更复杂的变体添加了额外的链接,允许在任意位置更有效地插入或移除节点。链表的缺点是访问时间是线性的(并且难以管道化)。更快的访问,例如随机访问,是不可行的。与链表相比,数组具有更好的缓存局部性。
链接列表,其节点包含两个字段:整数值和指向下一个节点的链接。最后一个节点链接到用于表示列表末尾的终止符。
链表是最简单和最常见的数据结构之一。它们可用于实现其他几种常见的抽象数据类型,包括列表,堆栈,队列,关联数组和S表达式,但如果不使用链表作为基础直接实现这些数据结构并不罕见。
链接列表相对于传统阵列的主要好处是可以轻松插入或删除列表元素,而无需重新分配或重新组织整个结构,因为数据项不需要连续存储在内存或磁盘上,同时重构数组运行时是一个更昂贵的操作。链接列表允许在列表中的任何位置插入和删除节点,并允许通过在列表遍历期间保持在内存中添加或删除链接之前的链接来执行常量操作。
另一方面,由于简单的链表本身不允许随机访问数据或任何形式的有效索引,许多基本操作 - 例如获取列表的最后一个节点,查找包含给定数据的节点,或者找到应插入新节点的位置 - 可能需要遍历大多数或所有列表元素。使用链表的优缺点如下。链表是动态的,因此列表的长度可以根据需要增加或减少。每个节点不一定在内存中物理上跟随前一个节点。
内容
缺点
由于指针使用的存储空间,它们使用的内存多于数组。
必须从头开始按顺序读取链接列表中的节点,因为链接列表本质上是顺序访问。
节点存储不明确,大大增加了访问列表中各个元素所需的时间,特别是CPU缓存。
在反向遍历方面,链表中出现了困难。例如,单向链接列表向后导航很麻烦[1],而双向链表更容易阅读,在为后向指针分配空间时会占用内存。
历史
链接列表由1953-1956由RAND公司的Allen Newell,Cliff Shaw和Herbert A. Simon开发,作为其信息处理语言的主要数据结构。作者使用IPL开发了几个早期的人工智能程序,包括逻辑理论机器,通用问题解算器和计算机象棋程序。关于他们工作的报告出现在1956年的IRE信息理论交易中,以及1957年至1959年的几次会议记录,包括1957年和1958年西方联合计算机会议论文集,以及信息处理(第一期会议录)。1959年,教科文组织国际信息处理会议。现在的经典图表由代表列表节点的块组成,箭头指向连续的列表节点,出现在Newell和Shaw的“编程逻辑理论机器”中。WJCC,1957年2月。纽厄尔和西蒙因其“为人工智能,人类认知心理学和名单处理做出基本贡献”而于1975年获得ACM 图灵奖。问题机器翻译的自然语言处理导致维克托·夫在麻省理工学院(麻省理工学院)使用链接列表作为他的COMIT编程语言中的数据结构,用于语言学领域的计算机研究。1958年机械翻译中出现了一种名为“机械翻译的编程语言”的语言报告。[ 引证需要 ]
LISP,代表列表处理器,由John McCarthy于1958年创建,当时他在麻省理工学院,并于1960年在ACM通讯中发表了一篇题为“符号表达式的递归函数及其机器计算,部分”的论文。一世”。LISP的主要数据结构之一是链表。
到20世纪60年代早期,链接列表和使用这些结构作为主要数据表示的语言的效用已得到很好的证实。麻省理工学院林肯实验室的伯特·格林于1961年3月在IRE电子人类因素交易中发表了题为“符号操作的计算机语言”的评论文章,该文章总结了链表方法的优点。后来的评论文章“Bobrow和Raphael的列表处理计算机语言的比较”于1964年4月出现在ACM的通信中。
由Technical Systems Consultants开发的几个操作系统(最初是West Lafayette Indiana,后来是北卡罗来纳州的Chapel Hill)使用单链表作为文件结构。目录条目指向文件的第一个扇区,并通过遍历指针找到文件的后续部分。使用这种技术的系统包括Flex(用于Motorola 6800 CPU),mini-Flex(相同CPU)和Flex9(用于Motorola 6809 CPU)。由TSC开发并由加利福尼亚州的烟雾信号广播公司销售的变体以相同的方式使用双链表。
由IBM为System 360/370计算机开发的TSS / 360操作系统使用双链表进行文件系统目录。目录结构类似于Unix,其中目录可以包含文件和其他目录并扩展到任何深度。
基本概念和术语
链表的每个记录通常称为“元素”或“ 节点 ”。
包含下一个节点地址的每个节点的字段通常称为“下一个链接”或“下一个指针”。其余字段称为“数据”,“信息”,“值”,“货物”或“有效负载”字段。
列表的“头部”是其第一个节点。列表的“尾部”可以指向头部之后的列表的其余部分,也可以指列表中的最后一个节点。在Lisp和一些派生语言中,下一个节点可以被称为列表的' cdr '(发音为can-er),而头节点的有效载荷可以被称为'car'。
单链表
单链表包含具有数据字段和“下一个”字段的节点,这些字段指向节点行中的下一个节点。可以在单链表上执行的操作包括插入,删除和遍历。
单链表,其节点包含两个字段:整数值和指向下一个节点的链接
以下代码演示了如何将具有数据“value”的新节点添加到单个链接列表的末尾:
[code]
node
addNode
(
node
head
,
int
value
)
{
node
temp
,
p
;
//声明两个节点temp和p
temp
=
createNode
();
//假设createNode创建一个data = 0的新节点,然后指向NULL。
temp
- >
data
=
value
;
//将元素的值添加到节点的数据部分
if
(
head
==
NULL
)
{
head
=
temp
;
//当链接列表为空时
}
else
{
p
=
头
;
//将头部分配给p
while
(
p
- >
next
!=
NULL
)
{
p
=
p
- >
next
;
//遍历列表,直到p是最后一个节点。最后一个节点始终指向NULL。
}
p
- >
next
=
temp
;
//将上一个节点指向创建的新节点。
}
返回
头
;
}
双重链表
主要文章:双重链表
在“双链表”中,除了下一节点链接之外,每个节点还包含指向序列中“前一个”节点的第二个链接字段。这两个链接可以称为“向前('s')和'向后',或'下一个'和'上'('上一个')。
一个双向链表,其节点包含三个字段:整数值,前向下一个节点的链接,以及后一个节点的链接
称为XOR链接的技术允许使用每个节点中的单个链接字段来实现双向链表。但是,这种技术需要能够对地址进行位操作,因此可能在某些高级语言中不可用。
许多现代操作系统使用双向链表来维护对活动进程,线程和其他动态对象的引用。[2]rootkit逃避检测的一个共同策略是将自己与这些列表脱钩。[3]
乘以链表
在“多重链表”中,每个节点包含两个或多个链接字段,每个字段用于以相同集合的不同顺序连接同一组数据记录(例如,按名称,按部门,按出生日期,等等。)。虽然双向链表可以被视为多重链表的特殊情况,但是两个或更多个指令彼此相反的事实导致更简单和更有效的算法,因此它们通常被视为单独的情况。
循环链表
在列表的最后一个节点中,链接字段通常包含空引用,特殊值用于指示缺少其他节点。一个不太常见的约定是使它指向列表的第一个节点; 在这种情况下,该名单被称为“循环”或“循环联系”; 否则,它被称为“开放”或“线性”。它是最后一个指针指向第一个节点的列表。
循环链表
在圆形双链表的情况下,第一节点也指向列表的最后一个节点。
Sentinel节点
在一些实现中,可以在第一数据记录之前或在最后一个数据记录之后添加额外的“前哨”或“伪”节点。该约定通过确保可以安全地解除引用所有链接并且每个列表(甚至不包含数据元素的列表)始终具有“第一”和“最后”节点来简化和加速一些列表处理算法。
空列表
空列表是不包含数据记录的列表。这通常与它说零节点相同。如果正在使用标记节点,则当列表仅具有标记节点时,通常称该列表为空。
哈希链接
链接字段不一定是节点的物理部分。如果数据记录存储在数组中并由其索引引用,则链接字段可以存储在具有与数据记录相同索引的单独数组中。
列表句柄
由于对第一个节点的引用允许访问整个列表,因此该引用通常称为列表的“地址”,“指针”或“句柄”。操作链表的算法通常会将这些句柄输入到输入列表中,并将句柄返回到结果列表中。实际上,在这种算法的上下文中,单词“list”通常表示“list handle”。但是,在某些情况下,通过由两个链接组成的句柄引用列表可能很方便,指向其第一个和最后一个节点。
结合替代方案
上面列出的替代方案几乎可以在任何方面任意组合,因此可以有没有哨兵的圆形双向链表,带有哨兵的圆形单链表等。
权衡
与计算机编程和设计中的大多数选择一样,任何方法都不适合所有情况。链表数据结构在一种情况下可能运行良好,但在另一种情况下会导致问题。这是涉及链表结构的一些常见权衡的列表。
链接列表与动态数组
列表数据结构的比较
索引
Θ(n)
Θ(1)
Θ(1)
Θ(log n)
Θ(log n)[4]
Θ(1)
在开头 插入/删除
Θ(1)
N / A
Θ(n)
Θ(log n)
Θ(1)
Θ(n)
插入/删除在结束
Θ(1)当最后一个元素已知时 ; 当最后一个元素未知时
Θ(n)
N / A
Θ(1)摊销
Θ(log n)
Θ(log n)更新
Θ(1)摊销
在中间 插入/删除
搜索时间+ Θ(1)[5][6]
N / A
Θ(n)
Θ(log n)
Θ(log n)更新
Θ(n)
浪费的空间(平均)
Θ(n)
0
Θ(n)[7]
Θ(n)
Θ(n)
Θ(√ Ñ)
甲动态阵列是一种数据结构,其分配的所有元素连续在存储器中,并且保持元件的电流数量的计数。如果超出了为动态数组保留的空间,则会重新分配并(可能)复制该空间,这是一项昂贵的操作。
链接列表与动态数组相比有几个优点。在列表的特定点插入或删除元素,假设我们已经将指针指向节点(在要删除的指针之前或插入点之前),这是一个常量操作(否则没有这个)引用它是O(n)),而在随机位置插入动态数组将需要平均移动一半元素,最坏情况下需要移动所有元素。虽然可以通过某种方式将其插槽标记为“空闲”而在恒定时间内从数组中“删除”元素,但这会导致碎片阻碍迭代的执行。
此外,可以将任意多个元素插入到链表中,仅受可用总存储量的限制; 而动态数组最终将填满其底层数组数据结构,并且必须重新分配 - 一个昂贵的操作,如果内存碎片化甚至可能无法实现,尽管重新分配的成本可以在插入时平均,并且成本由于重新分配而导致的插入仍将按摊销 O(1)进行摊销。这有助于在阵列末端附加元素,但是由于数据移动以保持连续性,插入(或从中间位置移除)仍然会带来高昂的成本。删除了许多元素的数组也可能必须调整大小以避免浪费太多空间。
另一方面,动态数组(以及固定大小的数组数据结构)允许进行恒定时间随机访问,而链表仅允许对元素进行顺序访问。事实上,单链表可以很容易地只在一个方向上进行。这使链接列表不适合于快速查找元素的应用程序,例如heapsort。对阵列和动态数组的顺序访问也比许多机器上的链表快,因为它们具有最佳的引用局部性,因此可以很好地利用数据缓存。
链表的另一个缺点是引用所需的额外存储空间,这常常使得它们对于小数据项(如字符或布尔值)的列表不切实际,因为链接的存储开销可能会超过两倍或更多的大小。数据。相反,动态数组仅需要数据本身的空间(以及非常少量的控制数据)。[注1] 它也可能很慢,并且对于一个天真的分配器来说,浪费,为每个新元素分别分配内存,通常使用内存池解决问题。
一些混合解决方案试图结合两种表示的优点。 展开的链接列表在每个列表节点中存储多个元素,从而提高缓存性能,同时减少引用的内存开销。CDR编码也通过用引用的实际数据替换引用来实现这两者,这些数据延伸到引用记录的末尾。
突出使用动态数组与链表的优缺点的一个很好的例子是通过实现解决Josephus问题的程序。约瑟夫斯问题是一种选举方法,通过让一群人站成一个圈子而起作用。从一个预定的人开始,你会在圆圈周围数n次。一旦你到达第n个人,将他们带出圆圈并让成员关闭圆圈。然后围绕圆圈计算相同的n时间并重复这个过程,直到只剩下一个人。那个人赢得了选举。这显示了链表与动态数组的优缺点,因为如果您将人们视为循环链表中的连接节点,则会显示链表能够轻松删除节点(因为它只需重新排列)到不同节点的链接)。但是,链接列表很难找到要删除的下一个人,并且需要搜索列表,直到找到该人。另一方面,动态数组在删除节点(或元素)方面将很差,因为它无法在不将列表中的所有元素单独移动一个的情况下移除一个节点。但是,找到n非常容易圆圈中的人通过他们在阵列中的位置直接引用它们。
的列表排序问题涉及链表表示的有效转化成一个数组。虽然对于传统计算机来说微不足道,但通过并行算法解决这个问题是复杂的并且已成为许多研究的主题。
一个平衡的树也有类似的内存访问模式和空间开销链表,同时允许更高效的索引,以O(log n)的时间,而不是为O(n)的随机访问。但是,由于树操作的开销以保持平衡,插入和删除操作更加昂贵。树存在自动维持平衡状态的方案:AVL树或红黑树。
单独链接线性列表与其他列表
虽然双链接和循环列表优于单链接线性列表,但线性列表提供了一些优点,使其在某些情况下更可取。
单链接的线性列表是递归数据结构,因为它包含指向相同类型的较小对象的指针。出于这个原因,对单链接线性列表的许多操作(例如合并两个列表,或以相反的顺序枚举元素)通常具有非常简单的递归算法,比使用迭代命令的任何解决方案简单得多。虽然这些递归解决方案可以适用于双向链接和循环链接列表,但这些过程通常需要额外的参数和更复杂的基本情况。
线性单链表还允许尾部共享,使用子列表的公共最后部分作为两个不同列表的终端部分。特别是,如果在列表的开头添加新节点,则前一个列表仍然可用作新节点的尾部 - 一个持久数据结构的简单示例。同样,对于其他变体,情况并非如此:节点可能永远不属于两个不同的循环或双向链接列表。
特别地,末端哨兵节点可以在单个链接的非圆形列表之间共享。相同的末端前哨节点可以用于每个这样的列表。在Lisp中,例如,每一个适当的列表,并附有链接到一个特殊的节点,记结束nil或者(),它CDR链接指向自己。这样一个Lisp程序可以安全地乘坐CAR或CDR的任何名单。
花哨变体的优点通常限于算法的复杂性,而不是效率。特别是,循环列表通常可以通过线性列表以及指向第一个和最后一个节点的两个变量来模拟,而无需额外成本。
双重联系与单一联系
双链表需要每个节点有更多空间(除非使用XOR链接),并且它们的基本操作更昂贵; 但它们通常更容易操作,因为它们允许在两个方向上快速轻松地顺序访问列表。在双向链表中,只有该节点的地址,才能在恒定数量的操作中插入或删除节点。做同样的单链表,一个必须具备的指针的地址给该节点,这无论是对整个列表的句柄(第一个节点的情况下),或在链路领域以前的节点。某些算法需要双向访问。另一方面,双向链表不允许尾部共享,不能用作持久性数据结构
圆形链接与线性链接
循环链表可以是表示自然循环的数组的自然选项,例如多边形的角,以FIFO(“ 先进先出 ”)顺序使用和释放的缓冲池,或者一组过程应该是时间共享在循环顺序。在这些应用程序中,指向任何节点的指针充当整个列表的句柄。
使用循环列表,通过跟随一个链接,指向最后一个节点的指针也可以轻松访问第一个节点。因此,在需要访问列表的两端的应用程序中(例如,在队列的实现中),循环结构允许通过单个指针而不是两个指针来处理该结构。
通过给出每个片段的最后一个节点的地址,循环列表可以在恒定时间内被分成两个循环列表。该操作包括交换这两个节点的链接字段的内容。将相同的操作应用于两个不同列表中的任意两个节点将两个列表连接成一个。此属性极大地简化了一些算法和数据结构,例如四边和面边。
空循环列表的最简单表示(当这样的事情有意义时)是一个空指针,表明该列表没有节点。如果没有这个选择,许多算法必须测试这种特殊情况,并单独处理它。相比之下,使用null来表示空线性列表更自然,并且通常会创建更少的特殊情况。
使用哨兵节点
Sentinel节点可以通过确保每个元素存在下一个或前一个节点来简化某些列表操作,并且甚至空列表至少具有一个节点。也可以使用列表末尾的标记节点和适当的数据字段来消除某些列表末尾测试。例如,当扫描列表寻找具有给定值x的节点时,将sentinel的数据字段设置为x使得不必测试循环内的列表末尾。另一个例子是合并两个排序列表:如果它们的标记将数据字段设置为+∞,则下一个输出节点的选择不需要对空列表进行特殊处理。
但是,前哨节点占用了额外的空间(特别是在使用许多短列表的应用程序中),并且它们可能使其他操作复杂化(例如创建新的空列表)。
然而,如果循环列表仅用于模拟线性列表,则可以通过在最后和第一数据节点之间向每个列表添加单个标记节点来避免这种复杂性中的一些。使用此约定,空列表仅由Sentinel节点组成,通过下一节点链接指向自身。如果列表不为空,则列表句柄应该是指向最后一个数据节点的指针,在标记之前; 如果列表为空,则为哨兵本身。
可以使用相同的技巧来简化双链接线性列表的处理,方法是将其转换为具有单个标记节点的循环双向链表。但是,在这种情况下,句柄应该是指向虚节点本身的单个指针。[8]
链接列表操作
在就地操作链接列表时,必须注意不要使用先前分配中已失效的值。这使得用于插入或删除链表节点的算法有些微妙。本节给出了伪代码,用于在单个,双向和循环链接列表中就地添加或删除节点。纵观我们将使用空指结束列表中的标记或定点,其可以以多种方式来实现。
线性链接列表
单链表
我们的节点数据结构将有两个字段。我们还保留一个变量firstNode,它始终指向列表中的第一个节点,或者对于空列表为null。
[code]记录节点
{
数据; //中的数据被存储在节点
的节点下一//一个参考到下一个节点,为null最后一个节点
}
[code]记录清单
{
Node firstNode //指向列表的第一个节点; null表示空列表
}
遍历单个链接列表很简单,从第一个节点开始,然后跟随每个下一个链接,直到我们结束:
[code]node:= list.firstNode,
而 node不为null
(对node.data执行某些操作)
node:= node.next
以下代码在单个链接列表中的现有节点之后插入节点。该图显示了它的工作原理。在现有节点之前插入节点不能直接完成; 相反,必须跟踪前一个节点并在其后插入一个节点。
[code]function insertAfter(Node node,Node newNode)//在节点后插入newNode
newNode.next:= node.next
node.next:= newNode
在列表的开头插入需要单独的功能。这需要更新firstNode。
[code]function insertBeginning(List list,Node newNode)//在当前第一个节点之前插入节点
newNode.next:= list.firstNode
list.firstNode:= newNode
类似地,我们具有在给定节点之后移除节点以及从列表的开头移除节点的功能。该图演示了前者。要查找和删除特定节点,必须再次跟踪前一个元素。
[code]function removeAfter(节点节点)//删除此节点之后的节点
obsoleteNode:= node.next
node.next:= node.next.next
破坏过时节点
[code]function removeBeginning(List list)//删除第一个节点
obsoleteNode:= list.firstNode
list.firstNode:= list.firstNode.next //指向已删除的节点
破坏过时节点
请注意,在删除列表中的最后一个节点时removeBeginning()设置list.firstNode为null。
由于我们无法向后迭代,因此无法进行高效insertBefore或removeBefore操作。在特定节点之前插入列表需要遍历列表,这将具有O(n)的最坏情况运行时间。
将一个链接列表附加到另一个链接列表可能效率低,除非对尾部的引用保留为List结构的一部分,因为我们必须遍历整个第一个列表才能找到尾部,然后将第二个列表附加到此。因此,如果两个线性链表是每个长度,列表追加有时间复杂的。在Lisp系列语言中,程序提供了列表追加。
通过在列表的前面包含虚拟元素,可以消除链表操作的许多特殊情况。这样可以确保列表的开头没有特殊情况,并且既不需要insertBeginning()又removeBeginning()不必要。在这种情况下,列表中的第一个有用数据将在。 list.firstNode.next
循环链表
在循环链接列表中,所有节点都以连续的圆圈链接,而不使用null。对于具有前端和后端的列表(例如队列),可以存储对列表中最后一个节点的引用。最后一个节点之后的下一个节点是第一个节点。元素可以添加到列表的后面,并在常量时间从前面删除。
循环链表可以单链或双链。
两种类型的循环链表都可以从任何给定节点开始遍历完整列表。这通常允许我们避免存储firstNode和lastNode,尽管如果列表可能是空的,我们需要空列表的特殊表示,例如指向列表中某个节点的lastNode变量,如果它是空的则为null ; 我们在这里使用这样的lastNode。此表示显着简化了使用非空列表添加和删除节点,但空列表是一种特殊情况。
算法
假设someNode是非空循环单链表中的某个节点,此代码以someNode开头迭代该列表:
[code]如果 someNode≠ null,则函数 iterate(someNode)
node:= someNode
做
用node.value做一些事情
node:= node.next
而 node≠someNode
请注意,测试“ while node≠someNode”必须位于循环的末尾。如果测试移动到循环的开头,则只要列表只有一个节点,该过程就会失败。
此函数在给定节点“node”之后将节点“newNode”插入循环链表中。如果“node”为null,则假定列表为空。
[code]function nodeAfter(Node node,Node newNode)
if node = null
newNode.next:= newNode
其他
newNode.next:= node.next
node.next:= newNode
假设“L”是指向循环链表的最后一个节点的变量(如果列表为空,则为null)。要将“newNode”附加到列表的末尾,可以这样做
[code] insertAfter(L,newNode)
L:= newNode
要在列表的开头插入“newNode” ,可以这样做
[code]insertAfter(L,newNode)
如果 L = null
L:= newNode
使用节点数组的链接列表
不支持任何类型的引用的语言仍然可以通过用数组索引替换指针来创建链接。方法是保留一个记录数组,其中每个记录都有整数字段,指示数组中下一个(可能是前一个)节点的索引。并非需要使用阵列中的所有节点。如果也不支持记录,则通常可以使用并行数组。
例如,请考虑以下使用数组而不是指针的链表记录:
[code]记录条目 {
整数下一个; //数组
整数 prev中的下一个条目的索引 ; //上一个条目(如果是双链接的)
字符串名称;
真正的平衡;
}
可以通过创建这些结构的数组来构建链表,并使用整数变量来存储第一个元素的索引。
[code]整数从ListHead
条目记录[1000]
通过将下一个(或前一个)单元的数组索引放入给定元素中的Next或Prev字段来形成元素之间的链接。例如:
指数
下一个
上一页
名称
平衡
0
1
4
琼斯,约翰
123.45
1
-1
0
史密斯,约瑟夫
234.56
2(listHead)
4
-1
亚当斯,亚当
0.00
3
忽略,伊格内修斯
999.99
4
0
2
另一个,安妮塔
876.54
五
6
7
在上面的示例中,ListHead将设置为2,即列表中第一个条目的位置。请注意,条目3和5到7不是列表的一部分。这些单元格可用于列表的任何添加。通过创建ListFree整数变量,可以创建一个空闲列表来跟踪可用的单元格。如果所有条目都在使用中,则必须增加数组的大小,或者必须删除某些元素,然后才能将新条目存储在列表中。
以下代码将遍历列表并显示名称和帐户余额:
[code]i:= listHead
而 i≥0 //遍历列表
打印i,Records [i] .name,Records [i] .balance // print entry
i:=记录[i] .next
面对选择时,这种方法的优点包括:
链表是可重定位的,这意味着它可以随意在内存中移动,也可以快速直接序列化以便存储在磁盘上或通过网络传输。
特别是对于小型列表,数组索引可以占用比许多体系结构上的完整指针少得多的空间。
可以通过将节点保持在存储器中并通过周期性地重新排列它们来改进参考的位置,尽管这也可以在一般商店中完成。
简单的动态内存分配器可以为分配的每个节点产生过量的开销存储空间; 在这种方法中,每个节点几乎不会产生分配开销。
从预分配的阵列中获取条目比为每个节点使用动态内存分配更快,因为动态内存分配通常需要搜索所需大小的空闲内存块。
然而,这种方法有一个主要缺点:它为其节点创建和管理私有内存空间。这导致以下问题:
它增加了实施的复杂性。
在大数组满时生长可能很困难或不可能,而在大型通用内存池中查找新链接列表节点的空间可能更容易。
偶尔(当它已满时)向动态数组添加元素意外地采用线性(O(n))而不是恒定时间(尽管它仍然是一个分摊的常量)。
如果列表小于预期或者释放了许多节点,则使用通用内存池会为其他数据留下更多内存。
由于这些原因,此方法主要用于不支持动态内存分配的语言。如果在创建阵列时已知列表的最大大小,则还可以减轻这些缺点。
语言支持
许多编程语言(如Lisp和Scheme)都内置了单独链接的列表。在许多函数式语言中,这些列表是由节点构成的,每个节点都称为缺点或缺点单元。缺点有两个字段:汽车,对该节点的数据的引用,以及对下一个节点的引用的cdr。尽管cons单元可用于构建其他数据结构,但这是它们的主要目的。
在支持抽象数据类型或模板的语言中,链接列表ADT或模板可用于构建链接列表。在其他语言中,链接列表通常使用引用和记录来构建。
内部和外部存储
在构建链表时,可以选择是将列表数据直接存储在链表节点中,称为内部存储,还是仅存储对数据的引用,称为外部存储。内部存储的优点是可以更有效地访问数据,整体需要更少的存储空间,具有更好的引用局部性,并简化列表的内存管理(其数据与列表节点同时分配和解除分配)。
另一方面,外部存储具有更通用的优点,因为无论数据的大小如何,相同的数据结构和机器代码都可以用于链表。它还可以轻松地将相同的数据放在多个链接列表中。虽然通过在节点数据结构中包含多个下一个引用,通过内部存储可以将相同的数据放置在多个列表中,但是有必要创建单独的例程以基于每个字段添加或删除单元。可以通过使用外部存储创建使用内部存储的元素的其他链接列表,并使附加链接列表的单元格存储对包含数据的链接列表的节点的引用。
通常,如果需要在链表中包含一组数据结构,则外部存储是最佳方法。如果一组数据结构只需要包含在一个链表中,那么内部存储稍微好一点,除非使用外部存储的通用链表包可用。同样,如果可以存储在同一数据结构中的不同数据集要包含在单个链表中,那么内部存储就可以了。
可与某些语言中使用的另一方法涉及具有不同数据结构,但都具有初始字段,包括下一个(和一个先前如果双链表)引用在同一位置。在为每种类型的数据定义单独的结构之后,可以定义通用结构,其包含所有其他结构共享的最小数据量并包含在结构的顶部(开始)。然后可以创建使用最小结构执行链表类型操作的通用例程,但是单独的例程可以处理特定数据。这种方法通常用于消息解析例程,其中接收几种类型的消息,但都以相同的字段集开始,通常包括消息类型的字段。通用例程用于在收到新消息时将新消息添加到队列,并将其从队列中删除以处理消息。
内部和外部存储的示例
假设您要创建一个家庭及其成员的链接列表。使用内部存储,结构可能如下所示:
[code]记录成员 { //下一个家庭
成员 ;
string firstName;
整数年龄;
}
记录 家庭 { //家庭本身
家庭未来;
string lastName;
字符串地址;
成员成员//这个家庭的成员名单的头
}
要使用内部存储打印完整的系列及其成员列表,我们可以写:
[code]aFamily:= Families //从系列列表开始,
而 aFamily≠ null //遍历系列列表
打印有关家庭的信息
aMember:= aFamily.members //获取此系列成员列表的头部,
而 aMember≠ null //遍历成员列表
打印有关会员的信息
aMember:= aMember.next
aFamily:= aFamily.next
使用外部存储,我们将创建以下结构:
[code]记录节点 { //通用链接结构
节点下一个;
指针数据//节点数据的通用指针
}
记录 成员 { //家庭成员
字符串 firstName的结构 ;
整数年龄
}
记录 族 { //结构为族
字符串 lastName;
字符串地址;
节点成员//这个家庭成员列表的负责人
}
要使用外部存储打印完整的系列及其成员列表,我们可以写:
[code]famNode:= Families //从系列列表开始,
而 famNode≠ null //遍历系列列表
aFamily:=(family)famNode.data //从节点中提取系列
打印有关家庭的信息
memNode:= aFamily.members //得到家庭成员列表,
而 memNode≠ null //循环通过成员列表
aMember:=(成员)memNode.data //从节点中提取成员
打印有关会员的信息
memNode:= memNode.next
famNode:= famNode.next
请注意,在使用外部存储时,需要额外的步骤从节点中提取记录并将其转换为正确的数据类型。这是因为系列列表和系列中的成员列表都使用相同的数据结构(节点)存储在两个链接列表中,并且该语言没有参数类型。
只要在编译时知道成员可以属于的族的数量,内部存储就可以正常工作。但是,如果某个成员需要包含在任意数量的系列中,并且只在运行时知道具体数量,则需要外部存储。
加快搜索速度
查找链表中的特定元素,即使它已排序,通常也需要O(n)时间(线性搜索)。这是链表与其他数据结构相比的主要缺点之一。除了上面讨论的变体之外,下面是两种改善搜索时间的简单方法。
在无序列表中,一个用于减少平均搜索时间的简单启发式算法是移动到前端的启发式算法,它只需将元素移动到列表的开头即可。此方案可以方便地创建简单的缓存,确保最近使用的项目也是最快查找的。
另一种常见方法是使用更有效的外部数据结构“ 索引 ”链表。例如,可以构建一个红黑树或哈希表,其元素是对链表节点的引用。可以在单个列表上构建多个这样的索引。缺点是每次添加或删除节点时(或者至少在再次使用该索引之前)可能需要更新这些索引。
随机访问列表
一个随机访问列表是快速随机访问读取或修改列表中的任何元素的支持列表。[9]一种可能的实现是使用偏斜二进制数系统的偏斜二进制随机访问列表,其涉及具有特殊属性的树的列表; 这允许最坏情况的恒定时间头/缺点操作,以及最坏情况下的对数时间随机访问索引的元素。[9]随机访问列表可以被实现为持久数据结构。[9]
随机访问列表可以被视为不可变链表,因为它们同样支持相同的O(1)头尾操作。[9]
随机访问列表的简单扩展是最小列表,它提供了一个额外的操作,在恒定时间内产生整个列表中的最小元素(没有[ 需要澄清 ]突变复杂性)。[9]
相关数据结构
这两个栈和队列使用链表经常执行,只是限制它支持的操作类型。
的跳跃列表是具有指针的层增强快速跳过大量元件,然后下降到下一层的链表。这个过程一直持续到底层,这是实际的列表。
一个二叉树,其中的元素本身的性质相同的链接列表可以被看作是一种类型的链表。结果是每个节点可以包括对一个或两个其他链接列表的第一节点的引用,其与它们的内容一起形成该节点下面的子树。
一个展开链表是一个链表,其中每个节点包含数据值的阵列。这导致改进的高速缓存性能,因为更多列表元素在存储器中是连续的,并且减少了存储器开销,因为需要为列表的每个元素存储更少的元数据。
一个哈希表可以使用链表来存储散列到在哈希表中同一位置的项目链。
一个堆股一些链表的顺序性,但使用数组几乎总是执行
8000
。使用当前数据的索引计算下一个和先前的数据索引,而不是从节点到节点的引用。
一个自组织名单重新排列基于启发式其通过在列表的头部保持经常访问的节点减少了数据检索搜索时间的节点。
笔记
python 实现
单链表:
[code]#!/usr/bin/python
# -*- coding: utf-8 -*-
class Node(object):
def __init__(self,val,p=0):
self.data = val
self.next = p #面向对象语言指针地址以二进制数字存在
class LinkList(object):
def __init__(self):
self.head = 0 #头指针
def __getitem__(self, key):
if self.is_empty():
print ('linklist is empty.')
return
elif key <0 or key > self.getlength():
print ('the given key is error')
return
else:
return self.getitem(key)
def __setitem__(self, key, value):
if self.is_empty():
print ('linklist is empty.')
return
elif key <0 or key > self.getlength():
print ('the given key is error')
return
else:
self.delete(key)
return self.insert(key)
def initlist(self,data):#基本方法 #data为数组参数
self.head = Node(data[0])
p = self.head
for i in data[1:]:
node = Node(i)
p.next = node #将节点地址赋值给next
p = p.next
def getlength(self):#基本方法
p = self.head
length = 0
while p!=0:
length+=1
p = p.next
return length
def is_empty(self):
if self.getlength() ==0:
return True
else:
return False
def clear(self):#释放头指针地址,python自动进行垃圾回收
self.head = 0
def append(self,item):#基本方法
q = Node(item)
if self.head ==0:
self.head = q
else:
p = self.head
while p.next!=0:
p = p.next
p.next = q
def getitem(self,index):#基本方法
if self.is_empty():
print ('Linklist is empty.')
return
j = 0
p = self.head
while p.next!=0 and j
p = p.next
j+=1
if j ==index:
return p.data
else:
print ('target is not exist!')
def insert(self,index,item):#基本方法
if self.is_empty() or index<0 or index >self.getlength():
print ('Linklist is empty.')
return
if index ==0:
q = Node(item,self.head)
self.head = q
p = self.head
post = self.head
j = 0
while p.next!=0 and j
post = p
p = p.next
j+=1
if index ==j:
q = Node(item,p)
post.next = q
q.next = p
def delete(self,index):#基本方法
if self.is_empty() or index<0 or index >self.getlength():
print ('Linklist is empty.')
return
if index ==0:
q=self.head
q=q.next
q=q.next
self.head = q
p = self.head
post = self.head
j = 0
while p.next!=0 and j
post = p
p = p.next
j+=1
if index ==j:
post.next = p.next
def index(self,value):#基本方法
if self.is_empty():
print ('Linklist is empty.')
return
p = self.head
i = 0
while p.next!=0 and not p.data ==value:
p = p.next
i+=1
if p.data == value:
return i
else:
return -1
l = LinkList()
l.initlist([1,2,3,4,5])
print (l.getitem(4))
l.append(6)
print (l.getitem(5))
l.insert(4,40)
print (l.getitem(3))
print (l.getitem(4))
print (l.getitem(5))
l.delete(5)
print (l.getitem(5))
l.index(5)
双链表:
[code]class Node(object):
def __init__(self,val,p=0):
self.data = val
self.next = p
self.prev = p
class LinkList(object):
def __init__(self):
self.head = 0
def __getitem__(self, key):
if self.is_empty():
print ('linklist is empty.')
return
elif key <0 or key > self.getlength():
print ('the given key is error')
return
else:
return self.getitem(key)
def __setitem__(self, key, value):
if self.is_empty():
print ('linklist is empty.')
return
elif key <0 or key > self.getlength():
print ('the given key is error')
return
else:
self.delete(key)
return self.insert(key)
def initlist(self,data):
self.head = Node(data[0])
p = self.head
for i in data[1:]:
node = Node(i)
p.next = node
node.prev = p
p = p.next
def getlength(self):
p = self.head
length = 0
while p!=0:
length+=1
p = p.next
return length
def is_empty(self):
if self.getlength() ==0:
return True
else:
return False
def clear(self):
self.head = 0
def append(self,item):
q = Node(item)
if self.head ==0:
self.head = q
else:
p = self.head
while p.next!=0:
p = p.next
p.next = q
q.prev = p
def getitem(self,index):
if self.is_empty():
print ('Linklist is empty.')
return
j = 0
p = self.head
while p.next!=0 and j
p = p.next
j+=1
if j ==index:
return p.data
else:
print ('target is not exist!')
def insert(self,index,item):
if self.is_empty() or index<0 or index >self.getlength():
print ('Linklist is empty.')
return
if index ==0:
q = Node(item,self.head)
self.head = q
p = self.head
post = self.head
j = 0
while p.next!=0 and j
post = p
p = p.next
j+=1
if index ==j:
q = Node(item,p)
post.next = q
q.prev = post
q.next = p
p.prev = q
def delete(self,index):
if self.is_empty() or index<0 or index >self.getlength():
print ('Linklist is empty.')
return
if index ==0:
q = self.head.next
self.head = q
p = self.head
post = self.head
j = 0
while p.next!=0 and j
post = p
p = p.next
j+=1
if index ==j:
post.next = p.next
p.next.prev = post
def index(self,value):
if self.is_empty():
print ('Linklist is empty.')
return
p = self.head
i = 0
while p.next!=0 and not p.data ==value:
p = p.next
i+=1
if p.data == value:
return i
else:
return -1
l = LinkList()
l.initlist([1,2,3,4,5])
print (l.getitem(4))
l.append(6)
print (l.getitem(5))
l.insert(4,40)
print (l.getitem(3))
print (l.getitem(4))
print (l.getitem(5))
l.delete(5)
print (l.getitem(5))
l.index(5)
单向循环链表:
[code]# 节点, 包括:元素和下一个节点的地址
class Node(object):
def __init__(self, elem):
self.elem = elem
self.next = None
class SingleLoopLinkList(object):
'''单向循环链表'''
def __init__(self, node=None):
self.__head = node
def is_empty(self):
'''链表是否为空'''
return self.__head == None
def length(self):
'''链表长度'''
# 如果链表为空返回0
if self.is_empty():
return 0
count = 1
cur = self.__head
while cur.next != self.__head:
count += 1
cur = cur.next
return count
def travel(self):
'''遍历整个链表'''
# 如果链表为空,即返回为空
if self.is_empty():
return
cur = self.__head
while cur.next != self.__head:
print(cur.elem, end=' ')
cur = cur.next
# 打印最后一个元素
print(cur.elem, end=' ')
def add(self, item):
'''链表头部添加元素'''
node = Node(item)
if self.is_empty():
self.__head = node
node.next = self.__head
else:
# 添加的节点指向__head
node.next = self.__head
# 移到链表尾部,将尾部节点的next指向node
cur = self.__head
while cur.next != self.__head:
cur = cur.next
cur.next = node
# __head指向添加node的
self.__head = node
def append(self, item):
'''链表尾部添加元素'''
node = Node(item)
if self.is_empty():
self.__head = node
node.next = self.__head
# 如果不为空,则找到最后一个节点添加
else:
cur = self.__head
while cur.next != self.__head:
cur = cur.next
# 将尾节点next指向node
cur.next = node
# 将添加的节点next指向头节点
node.next = self.__head
def insert(self, pos, item):
'''指定位置添加元素'''
# 如果位置在第一个元素之前,就往头添加
if pos <= 0:
self.add(item)
# 如果位置在最后一个元素之后,就往尾添加
elif pos > self.length() - 1:
self.append(item)
else:
cur = self.__head
count = 0
while count < pos -1:
count += 1
cur = cur.next
node = Node(item)
# 先将新节点node的next指向插入位置的节点
node.next = cur.next
# 将插入位置的前一个节点的next指向新节点
cur.next = node
def remove(self, item):
'''删除节点'''
if self.is_empty():
return
cur = self.__head
pre = None
while cur.next != self.__head:
if cur.elem == item:
# 如果是头节点
if not pre:
# 需要改变尾节点的指向,所以先要找到尾节点
rear = self.__head
while rear.next != self.__head:
rear = rear.next
self.__head = cur.next
rear.next = self.__head
# 如果为中间节点
else:
# 将删除位置的上一个节点next指向删除位置的下一个节点
pre.next = cur.next
return
else:
pre = cur
cur = cur.next
# 退出循环
# 如果为尾节点
if cur.elem == item:
# 如果就只有一个节点
if cur == self.__head:
self.__head = None
else:
# 尾节点的上一个节点next指向头节点
pre.next = self.__head
def search(self, item):
'''查找节点是否存在'''
# 如果为空,就直接返回-1
if self.is_empty():
return -1
else:
cur = self.__head
count = 0
while cur.next != self.__head:
if cur.elem == item:
return count
else:
cur = cur.next
count += 1
# 判断最后一个节点是否为item
if cur.elem == item:
return count
return -1
# 测试
if __name__ == '__main__':
sll = SingleLoopLinkList()
# 判断是否为空
print(sll.is_empty())
# 获取链表长度
print(sll.length())
# 遍历链表
sll.travel()
# 头添加元素
sll.add(1)
sll.add(100)
print(sll.is_empty())
print(sll.length())
sll.travel()
print()
# 尾添加元素
sll.append(300)
sll.append(400)
sll.travel()
print()
# 指定位置添加元素
sll.insert(3, 200)
sll.insert(6, 500)
sll.travel()
print()
# 删除节点
sll.remove(1)
sll.travel()
print()
# 查找节点
print(sll.search(200))
双向循环链表:
[code]#链表的节点
class Node(object):
def __init__(self , item ):
self.item = item #节点数值
self.prev = None #用于指向前一个元素
self.next = None #用于指向后一个元素
#双向循环链表
class DoubleCircleLinkList(object):
def __init__(self):
self.__head = None #初始化的时候头节点设为空、
#判断链表是否为空,head为None 的话则链表是空的
def is_empty(self):
return self.__head is None
#头部添加元素的方法
def add(self,item):
node = Node(item) #新建一个节点node 里面的值是item
# 如果链表是空的,则node的next和prev都指向自己(因为是双向循环),head指向node
if self.is_empty():
self.__head = node
node.next = node
node.prev = node
# 否则链表不空
else:
node.next = self.__head #node的next设为现在的head
node.prev = self.__head.prev #node的prev 设为现在head的prev
self.__head.prev.next = node #现在head的前一个元素的next设为node
self.__head.prev = node #现在head的前驱 改为node
self.__head = node #更改头部指针
#尾部添加元素方法
def append(self , item):
#如果当前链表是空的 那就调用头部插入方法
if self.is_empty():
self.add(item)
#否则链表不为空
else :
node = Node(item) #新建一个节点node
#因为是双向循环链表,所以head的prev其实就是链表的尾部
node.next = self.__head #node的下一个设为头
node.prev = self.__head.prev #node的前驱设为现在头部的前驱
self.__head.prev.next = node #头部前驱的后继设为node
self.__head.prev = node #头部自己的前驱改为node
#获得链表长度 节点个数
def length(self):
#如果链表是空的 就返回0
if self.is_empty():
return 0
#如果不是空的
else:
cur = self.__head #临时变量cur表示当前位置 初始化设为头head
count = 1 #设一个计数器count,cur每指向一个节点,count就自增1 目前cur指向头,所以count初始化为1
#如果cur.next不是head,说明cur目前不是最后一个元素,那么count就1,再让cur后移一位
while cur.next is not self.__head:
count += 1
cur = cur.next
#跳出循环说明所有元素都被累加了一次 返回count就是一共有多少个元素
return count
#遍历链表的功能
def travel(self):
#如果当前自己是空的,那就不遍历
if self.is_empty():
return
#链表不空
else :
cur = self.__head #临时变量cur表示当前位置,初始化为链表的头部
#只要cur的后继不是头说明cur不是最后一个节点,我们就输出当前值,并让cur后移一个节点
while cur.next is not self.__head:
print( cur.item,end=" " )
cur = cur.next
#当cur的后继是head的时候跳出循环了,最后一个节点还没有打印值 在这里打印出来
print( cur.item )
#置顶位置插入节点
def insert(self, pos , item ):
#如果位置<=0 则调用头部插入方法
if pos <= 0:
self.add(item)
#如果位置是最后一个或者更大 就调用尾部插入方法
elif pos > self.length() - 1 :
self.append(item)
#否则插入位置就是链表中间
else :
index = 0 #设置计数器,用于标记我们后移了多少步
cur = self.__head #cur标记当前所在位置
#让index每次自增1 ,cur后移,当index=pos-1的时候说明cur在要插入位置的前一个元素,这时候停下
while index < pos - 1 :
index += 1
cur = cur.next
#跳出循环,cur在要插入位置的前一个元素,将node插入到cur的后面
node = Node(item) #新建一个节点
node.next = cur.next #node的后继设为cur的后继
node.prev = cur #node的前驱设为cur
cur.next.prev = node #cur后继的前驱改为node
cur.next = node #cur后继改为node
#删除节点操作
def remove(self,item):
#如果链表为空 直接不操作
if self.is_empty():
return
#链表不为空
else:
cur = self.__head #临时变量标记位置,从头开始
#如果头结点就是 要删除的元素
if cur.item == item:
#如果只有一个节点 链表就空了 head设为None
if self.length() == 1:
self.__head = None
#如果多个元素
else:#链表长度不为1
self.__head = cur.next #头指针指向cur的下一个
cur.next.prev= cur.prev #cur后继的前驱改为cur的前驱
cur.prev.next = cur.next #cur前驱的后继改为cur的后继
#否则 头节点不是要删除的节点 我们要向下遍历
else:
cur = cur.next #把cur后移一个节点
#循环让cur后移一直到链表尾元素位置,期间如果找得到就删除节点,找不到就跳出循环,
while cur is not self.__head:
#找到了元素cur就是要删除的
if cur.item == item:
cur.prev.next = cur.next #cur的前驱的后继改为cur的后继
cur.next.prev = cur.prev #cur的后继的前驱改为cur的前驱
cur = cur.next
#搜索节点是否存在
def search(self , item):
#如果链表是空的一定不存在
if self.is_empty():
return False
#否则链表不空
else:
cur = self.__head #设置临时cur从头开始
# cur不断后移,一直到尾节点为止
while cur.next is not self.__head:
#如果期间找到了就返回一个True 结束运行
if cur.item == item:
return True
cur = cur.next
# 从循环跳出来cur就指向了尾元素 看一下为元素是不是要找的 是就返回True
if cur.item ==item:
return True
#所有元素都不是 就返回False 没找到
return False
if __name__ == "__main__":
dlcl = DoubleCircleLinkList()
print(dlcl.search(7))
dlcl.travel()
dlcl.remove(1)
print(dlcl.length())
print(dlcl.is_empty())
dlcl.append(55)
print(dlcl.search(55))
dlcl.travel()
dlcl.remove(55)
dlcl.travel()
print(dlcl.length())
dlcl.add(3)
print(dlcl.is_empty())
dlcl.travel()
dlcl.add(4)
dlcl.add(5)
dlcl.append(6)
dlcl.insert(-10,1)
dlcl.travel()
print(dlcl.length())
dlcl.remove(6)
dlcl.travel()
print(dlcl.search(7) )
dlcl.append(55)
dlcl.travel()