数组与链表


200

为什么有人要在数组上使用链表?

毫无疑问,对链接列表进行编码比使用数组要花费更多的工作,并且您可能会想知道什么可以证明需要付出额外的努力。

我认为在链表中插入新元素很简单,但这是数组中的一项主要工作。与使用数组存储在数组中相比,使用链表存储还有其他优势吗?

这个问题不是一个重复这个问题,因为在这个问题涉及的一般数据结构的另一个问题是专门关于特定Java类要求。


1
相关- 何时在ArrayList <>上使用LinkedList <>?-它是Java,但是数组(ArrayList)和链表在任何语言下的性能都可能相同。
伯恩哈德·巴克


1
@rootTraveller实际上,该问题与该问题重复,因为我的问题被首先发布。
Onorio Catenacci

Answers:


147
  • 将不同大小的数据存储在链接列表中更加容易。数组假定每个元素的大小完全相同。
  • 正如您所提到的,链接列表的有机增长更容易。阵列的大小需要提前知道,或者在需要增长时重新创建。
  • 改组链表只是将什么指向什么更改的问题。改组数组更复杂和/或占用更多内存。
  • 只要所有迭代都在“ foreach”上下文中进行,您就不会在迭代中损失任何性能。

19
不同大小的物品有何不同之处?链表要么使用带有下一个字段的固定结构(需要固定大小),要么将指向汽车中数据的指针存储(可变大小确定)。使用向量,两种方法都一样容易。改组也一样。
布赖恩

32
我会说改组数组不那么复杂。
休·艾伦

23
这个答案是不正确的并且具有误导性。(除了需要在声明数组时知道数组的大小)
Robert Paulson

35
由于数据的局部性,迭代链表会不会更慢?
菲拉斯·阿萨德

6
@Rick,向量通常将它们所需的空间总体化,这样,每次增加大小时,它们就不需要分配新的空间。结果是向量通常不那么占用大量内存,而没有链表那么多。
温斯顿·埃韦特

179

另一个很好的理由是,链表很适合高效的多线程实现。这样做的原因是更改往往是局部的-仅影响在数据结构的局部插入和删除的一个或两个指针。因此,您可以有多个线程在同一个链表上工作。甚至可以使用CAS类型的操作创建无锁版本,并完全避免使用笨重的锁。

使用链接列表,迭代器还可以在进行修改时遍历列表。在修改不冲突的乐观情况下,迭代器可以继续进行而不会引起争用。

对于数组,任何修改数组大小的更改都可能需要锁定数组的很大一部分,而且实际上,很少在整个数组上都没有全局锁定的情况下完成此操作,因此修改变得停止了世界事务。


9
亚历克斯(Alex)-这是我永远不会想到的有趣的考虑。很好的答案。如果可以的话,我会两次投票给你。:-)
Onorio Catenacci

5
查看跳过列表(尤其是Java 6中的ConcurrentSkipListMap),以了解在何处可以使用此列表。CSLM是一种排序的并发映射,具有出色的性能。远胜于同步的TreeMap。 tech.puredanger.com/2007/10/03/skip-lists
Alex Miller

......不同的是ConcurrentSkipListMapConcurrentSkipListMap不是列表,即使“目录”在他们的名字出现的地方。两者都需要将被排序且唯一的键。如果您需要一个List数据结构(即允许以任意顺序重复元素的数据结构),那么这是不合适的,那么您就必须花大力气来制作一个数据结构,例如LinkedList可同时更新的事物。如果您只需要并发队列或双端队列,是的,甚至还有现有的示例,但是并发List...我不相信这是可能的。
Holger

128

Wikipedia很好地介绍了这些差异。

链表比数组有几个优点。元素可以无限期地插入到链表中,而数组最终将被填满或需要调整大小,这是一项昂贵的操作,如果内存碎片化,这甚至是不可能的。类似地,从中删除了许多元素的数组可能会浪费很多,或者需要变得更小。

另一方面,数组允许随机访问,而链表仅允许顺序访问元素。实际上,单链列表只能在一个方向上遍历。这使得链接列表不适用于需要通过其索引快速查找元素的应用程序,例如堆排序。由于引用和数据缓存的局部性,与许多机器上的链表相比,对阵列的顺序访问也更快。链接列表几乎不会从缓存中受益。

链接列表的另一个缺点是引用需要额外的存储空间,这通常使它们对于小数据项(例如字符或布尔值)的列表不切实际。这也可能很慢,并且由于使用天真的分配器浪费资源,因此无法为每个新元素分别分配内存,这通常是使用内存池解决的问题。

http://en.wikipedia.org/wiki/Linked_list


4
这是完美的答案。简要描述了两者的优缺点。
里克(Rick)

谢谢))死了很简单,但我没有在Wiki上看到它)
Timur Fayzrakhmanov

58

我将添加另一个-列表可以充当纯功能数据结构。

例如,您可以拥有完全不同的列表,共享同一结束部分

a = (1 2 3 4, ....)
b = (4 3 2 1 1 2 3 4 ...)
c = (3 4 ...)

即:

b = 4 -> 3 -> 2 -> 1 -> a
c = a.next.next  

无需将指向的数据复制ab和中c

这就是为什么它们在使用不可变变量的功能语言中如此受欢迎的原因- prepend并且tail在将数据视为不可变的情况下,操作很容易进行而不必复制原始数据-这是非常重要的功能。


4
我将永远不会想到的另一个非常有趣的考虑。谢谢。
Onorio Catenacci

如何在python中做到这一点?
外汇过户'16

29

除了更容易插入列表的中间之外,从链接列表的中间删除也比数组更容易。

但坦率地说,我从未使用过链表。每当我需要快速插入和删除时,我也需要快速查找,所以我去了HashSet或Dictionary。


2
确实,插入和删除是在大多数时间后进行的,因此也需要考虑时间复杂度的总和。
MA Hossain Tonu 2012年

28

合并两个链表(尤其是两个双链表)比合并两个数组(假定合并是破坏性的)要快得多。前者取O(1),后者取O(n)。

编辑:为澄清起见,我的意思是在此处以无序的方式“合并”,而不是在合并排序中。也许“连接”是一个更好的词。


2
仅当您只是将一个列表追加到另一个列表时。如果您实际上是合并两个排序的列表,那么它将花费比O(1)更多的日志。
爱马仕(Herms)

3
@Herms,但是您可以合并两个排序的链表,而无需分配任何额外的内存,只需遍历两个表并适当设置指针即可。合并两个阵列通常会占用至少一个额外的阵列。
Paul Tomblin

1
是的,合并列表具有更高的内存效率,但这并不是我要评论的内容。如果不明确说明情况,则说合并链表为O(1)极具误导性。
爱马仕(Herms)

@Herms合并列表并不比在任何明智的数据模型下合并数组更有效地利用内存。
阿列克谢·阿维琴科

2
Alexei Averchenko:可以使用O(1)内存就地完成两个列表的连接,甚至两个列表的合并排序。串联两个数组必然会占用O(n)内存,除非数组在内存中已经相邻。我认为您要针对的是n个元素的列表和n个元素的数组都占用O(n)内存,但是链表的系数更高。

17

对于ArrayList和LinkedList而言,一个广为人知的参数是调试时LinkedLists不舒服。维护开发人员花费在理解程序上的时间(例如,发现错误,增加错误),并且IMHO有时无法证明性能提高的纳秒级或企业应用程序中的内存消耗字节。有时(当然,这取决于应用程序的类型),最好浪费一些字节,但是拥有一个更易于维护或易于理解的应用程序。

例如,在Java环境中并使用Eclipse调试器,调试ArrayList将显示一个非常容易理解的结构:

arrayList   ArrayList<String>
  elementData   Object[]
    [0] Object  "Foo"
    [1] Object  "Foo"
    [2] Object  "Foo"
    [3] Object  "Foo"
    [4] Object  "Foo"
    ...

另一方面,观看LinkedList的内容并查找特定对象成为Expand-The-Tree单击的噩梦,更不用说过滤掉LinkedList内部所需的认知开销:

linkedList  LinkedList<String>
    header  LinkedList$Entry<E>
        element E
        next    LinkedList$Entry<E>
            element E   "Foo"
            next    LinkedList$Entry<E>
                element E   "Foo"
                next    LinkedList$Entry<E>
                    element E   "Foo"
                    next    LinkedList$Entry<E>
                    previous    LinkedList$Entry<E>
                    ...
                previous    LinkedList$Entry<E>
            previous    LinkedList$Entry<E>
        previous    LinkedList$Entry<E>

17

首先,在C ++中,使用链表应该不会比数组麻烦得多。您可以将std :: listboost指针列表用于链接列表。链表和数组的关键问题是指针所需的额外空间和可怕的随机访问。如果您要使用链表

  • 您不需要随机访问数据
  • 您将添加/删除元素,尤其是在列表中间

14

对我来说就是这样

  1. 访问

    • 链接列表仅允许顺序访问元素。因此,算法复杂度为O(n)的数量级
    • 数组允许随机访问其元素,因此复杂度为O(1)的顺序
  2. 存储

    • 链接列表需要额外的存储空间以供参考。这使得它们对于诸如字符或布尔值之类的小型数据项列表不切实际。
    • 阵列不需要额外的存储即可指向下一个数据项。可以通过索引访问每个元素。
  3. 尺寸

    • 链接列表的大小本质上是动态的。
    • 数组的大小仅限于声明。
  4. 插入/删除

    • 可以无限期地在链接列表中插入和删除元素。
    • 数组中值的插入/删除非常昂贵。它需要内存重新分配。

您有2个2号和2个3号:)
Hengameh 2015年

我们可以声明一个空数组,然后根据需要继续添加数据。如何使尺寸仍然固定?
HebleV

11

两件事情:

毫无疑问,编码一个链表比使用数组要花费更多的工作,他想知道什么能证明需要更多的努力。

使用C ++时,切勿编写链表。只需使用STL。实施起来有多难,决不应成为选择一个数据结构而不选择另一个数据结构的理由,因为大多数数据结构已经在那里实现。

至于数组和链表之间的实际差异,对我来说最大的事情是您如何计划使用该结构。我将使用术语向量,因为这是C ++中可调整大小的数组的术语。

索引到链接列表很慢,因为必须遍历列表才能到达给定的索引,而向量在内存中是连续的,并且可以使用指针数学到达那里。

将链接附加到链接列表的末尾或开头很容易,因为您只需要更新一个链接即可,在矢量中您可能需要调整大小并复制内容。

从列表中删除项目很容易,因为您只需断开一对链接,然后将它们重新连接在一起即可。从向量中删除项目可能更快或更慢,具体取决于您是否关心订单。在要删除的项目上方交换最后一个项目的速度更快,而向下移动所有内容的速度较慢,但​​会保持顺序。


正如我在上面告诉别人的那样,我只是想将问题的提出方式与我联系起来。无论如何,我永远都不会在C ++中使用数组(或者一个我自己拥有的链表),而是会使用这两个版本的STL版本。
Onorio Catenacci

10

埃里克·利珀特(Eric Lippert)最近发表了一篇关于数组应保守使用的原因之一的文章


2
固然不错,但与链表和数组的讨论无关。
罗伯特·保尔森

2
我建议Eric的大部分文章都是相关的,因为它同时讨论了数组的缺点和List的优点,而不必考虑list的实现。
贝文

8

对于链表,快速插入和删除确实是最好的参数。如果您的结构动态增长,并且不需要对任何元素(例如动态堆栈和队列)进行恒定时间访问,那么链表是一个不错的选择。



7

当集合不断增长和缩小时,链接列表特别有用。例如,很难想象要尝试使用数组实现Queue(添加到末尾,从前面移除)-您将花费所有时间将内容向下移动。另一方面,它与链接列表无关紧要。


4
您可能有一个基于数组的队列,而没有太多的工作仍然是快速/高效的。您只需要跟踪哪个索引是“ head”,哪个索引是“ tail”。如果您需要固定大小的队列(例如,内核中的键盘缓冲区),则此方法效果很好。
爱马仕(Herms)

3
如果要在自己喜欢的算法参考中查找它,则称为“循环缓冲区”。
史蒂夫·杰索普

7

除了从列表的中间添加和删除之外,我更喜欢链接列表,因为它们可以动态增长和收缩。


6
向量(基本上是数组)也可以做到这一点,由于引用的局部性问题,向量的摊销成本通常小于列表的成本。
Konrad Rudolph

7

没有人再编码自己的链表了。那太傻了。使用链表需要更多代码的前提是错误的。

如今,建立链接列表只是学生的一项练习,因此他们可以理解概念。相反,每个人都使用一个预先构建的列表。在C ++中,基于我们问题的描述,这可能意味着一个stl向量(#include <vector>)。

因此,选择一个链表和一个数组完全是权衡每种结构相对于应用程序需求的不同特征。克服额外的编程负担应该对决策产生零影响。


2
Er..umm .. std :: vector是一个数组,而不是一个链表。标准的链表是std :: list。
James Curran

1
是的,但是我认为vector更接近op要求的-动态数组替换。
Joel Coehoorn

@Joel,我只是想提出这个正在学习C ++的同胞提出的问题。我也不会为自己的链表编写代码,但这就是他问我的方式。:-)
Onorio Catenacci

在有自定义编译器的内存受限环境(微控制器)中,并非所有语言(例如,C ++中的容器)都实现。因此,可能您必须编写自己的链接列表。 nongnu.org/avr-libc/user-manual/FAQ.html#faq_cplusplus
Minh Tran

6

这实际上是一个效率问题,在链表中插入,删除或移动(不只是交换)元素的开销是最小的,即操作本身为O(1),而数组的O(n)则相反。如果您要对数据列表进行大量操作,则这可能会产生重大影响。您根据对数据的操作方式来选择数据类型,并为所使用的算法选择最有效的数据类型。


6

在知道确切项目数的地方以及在通过索引进行搜索的地方,数组是有意义的。例如,如果我想在给定的时刻存储视频输出的确切状态而不进行压缩,则可能会使用[1024] [768]大小的数组。这将为我提供所需的确切信息,而获取给定像素值的列表会慢得多。在没有意义的数组中,通常有比列表更好的数据类型可以有效地处理数据。


6

数组与链接列表:

  1. 有时由于内存碎片,阵列内存分配将失败。
  2. 由于为所有元素分配了连续的内存空间,因此在数组中进行缓存更好。
  3. 编码比数组更复杂。
  4. 与数组不同,对链表没有大小限制
  5. 链接列表中的插入/删除速度更快,而数组中的访问/删除速度更快。
  6. 从多线程的角度来看,链表更好。

-1:所有这些都需要得到证实,而不仅仅是列举。
约翰·桑德斯

上面的答案已经解释了每一点。作为后来者,除了列举,我别无选择。顺便说一句,您想向谁解释?
AKS

如果已经对它们进行了解释,那么您为什么要回答?
约翰·桑德斯

2
这样可以使您对讨论有一个概括的看法。我喜欢这种类型的答案,这样我就不必一次又一次地阅读相同的解释。我是为那些与我的思想风格相同的人做的。不同的ppl有不同的样式。没什么新鲜的。
AKS

3

由于数组本质上是静态的,因此所有操作(如内存分配)仅在编译时发生。因此处理器必须在其运行时投入更少的精力。


3

假设您有一个有序集,您还想通过添加和删除元素来对其进行修改。此外,您需要能够以对后面的元素进行引用的方式保留对元素的引用,以便以后可以获取上一个或下一个元素。例如,一本书的待办事项列表或一组段落。

首先,我们应该注意,如果要保留对集合本身之外的对象的引用,则可能最终将指针存储在数组中,而不是存储对象本身。否则,您将无法插入到数组中-如果将对象嵌入到数组中,则它们将在插入过程中移动,并且指向它们的任何指针都将变为无效。数组索引也是如此。

您已经注意到自己的第一个问题是插入-链表允许插入O(1),但是数组通常需要O(n)。这个问题可以部分克服-可以创建一个数据结构,该结构提供类似于数组的常规访问接口,在这种情况下,读写都处于对数状态。

您的第二个也是更严重的问题是,给定一个元素,找到下一个元素是O(n)。如果未修改集合,则可以保留元素的索引作为引用而不是指针,从而使find-next成为O(1)操作,但实际上,您只有一个指向对象本身的指针,而没有办法而不是通过扫描整个“数组”来确定其当前在数组中的索引。对于数组来说,这是一个无法解决的问题-即使您可以优化插入操作,也无法执行优化find-next类型操作的操作。


您能否详细说明一下:“有可能创建一个数据结构,该结构提供类似于数组的按序访问接口,在这种情况下,读和写都处于对数状态。”
亨伽美

1
在Wikipedia上的“动态数组/ Vriants”部分下有一些内容。不过,这并不是我的初衷...想象一下,一个类似b + tree的结构,其中有页面但没有键,而是每个中间页面记住每个子页面中有多少个元素,而叶子页面仅包含小数组中的元素。将元素插入叶页时,您必须将页面移至一半以腾出空间,然后向上移动并更新所有祖先页上的项目计数。当查找元素#N,只需添加了下级页面项目计数,直到你过N,然后下到该子树
DenNukem

3

在数组中,您可以在O(1)时间内访问任何元素。因此,它适合于二进制搜索快速排序等操作。另一方面,链表由于其O(1)时间而适合插入删除。两者都有优点和缺点,并且优先选择一个要归结为您要实现的目标。

-更大的问题是我们可以同时兼顾两者吗?像python和perl实现的列表一样。


3

链表

插入时更可取!基本上,它是处理指针的

1-> 3-> 4

插入(2)

1 ........ 3 ...... 4
..... 2

最后

1-> 2-> 3-> 4

2点中的一个箭头指向3,1点中的1箭头指向2

简单!

但是从数组

| 1 | 3 | 4 |

插入(2)| 1 | 3 | | 4 | | 1 | | 3 | 4 | | 1 | 2 | 3 | 4 |

好吧,任何人都可以看到差异!仅针对4个索引,我们执行3个步骤

如果阵列长度为一百万怎么办?数组效率高吗?答案是不!:)

删除同样的事情!在链接列表中,我们可以简单地使用指针,并在元素类中使元素和下一个元素无效!但是对于数组,我们需要执行shiftLeft()

希望有帮助!:)


3

链表维护比阵列维护更多的开销,同时所有这些要点都需要额外的存储空间。但是数组不能做一些事情。在许多情况下,假设您想要一个长度为10 ^ 9的数组,那么您将无法获得它,因为必须有一个连续的内存位置。链表可能是这里的救星。

假设您想用数据存储多个事物,那么可以在链接列表中轻松扩展它们。

STL容器通常在后台具有链接列表实现。


3

1-链接列表是一种动态数据结构,因此它可以在运行时通过分配和释放内存来增长和收缩。因此,无需给出链表的初始大小。节点的插入和删除确实非常容易。

2-链表的大小可以在运行时增加或减少,因此不会浪费内存。在数组的情况下,会浪费很多内存,例如,如果我们声明一个大小为10的数组并在其中仅存储6个元素,那么就会浪费4个元素的空间。链接列表中没有这种问题,因为仅在需要时才分配内存。

3-使用链接列表可以轻松实现数据结构,例如堆栈和队列。


2

使用链表的唯一原因是插入元素很容易(也可以删除)。

缺点可能是指针占用了大量空间。

而且,这种编码更加困难:通常,您不需要代码链接列表(或仅一次),它们被包含在 STL中, 并且即使您仍然需要这样做,也并不复杂。


2
指针占用很多空间?并不是的。如果要存储布尔值的链接列表,则可以确保按百分比分配指针会占用大量空间。但是,如果要存储复杂的对象(通常是这种情况),则指针可能会忽略不计。
Herms,

忘了微笑:)但是却说“不能”是“。”
user15453


1

根据您的语言,可以考虑以下一些不利条件:

C编程语言:使用链接列表时(通常通过结构指针),必须特别注意以确保您不会泄漏内存。如前所述,由于所有操作都在更改指针,因此链接列表很容易洗牌,但是我们要记住释放所有内容吗?

Java:Java具有自动垃圾收集功能,因此不会出现内存泄漏的问题,但是高级程序员隐藏的是链接列表的实现细节。从列表的中间删除节点之类的方法比该语言的某些用户所期望的过程更复杂。


1

为什么要在数组上建立链表?就像某些人已经说过的那样,插入和删除的速度更快。

但是也许我们不必忍受任何一个的局限性,并同时获得两者的最大优势...是吗?

对于数组删除,您可以使用“已删除”字节来表示已删除一行的事实,因此不再需要重组数组。为了减轻插入或快速更改数据的负担,请使用链表。然后在提及它们时,让您的逻辑首先搜索一个,然后搜索另一个。因此,将它们组合使用可为您带来两者的最佳效果。

如果您有一个非常大的数组,则可以将其与另一个更小的数组或链表组合,其中较小的数组或链表包含20、50、100个最近使用的项。如果所需的列表不在较短的链表或数组中,请转到大型数组。如果在此找到,则可以将其添加到较小的链接列表/数组中,前提是“最近使用过的内容最有可能被重复使用”(是的,可能会从列表中删除最近使用最少的项目)。这在许多情况下是正确的,并且以轻松,优雅和令人印象深刻的速度解决了我必须在.ASP安全权限检查模块中解决的问题。


1

虽然你们中的许多人都谈到了链表与数组的主要关系/缺点,但大多数比较是一个比另一个好/坏的比较。您可以在数组中进行随机访问,但不能在链表和其他列表中进行访问。但是,这是假定链接列表和数组将在类似的应用程序中应用。但是,正确的答案应该是,在特定的应用程序部署中,如何优先选择链表而不是阵列,反之亦然。假设您要实现字典应用程序,您将使用什么?Array:mmm,可以通过二进制搜索和其他搜索算法轻松检索..但是让我们考虑一下如何使链接列表更好。.假设您要在字典中搜索“ Blob”。拥有A-> B-> C-> D ---->的链接列表是否有意义

A -> B -> C -> ...Z
|    |    |
|    |    [Cat, Cave]
|    [Banana, Blob]
[Adam, Apple]

现在上述方法是更好的还是[Adam,Apple,Banana,Blob,Cat,Cave]的统一排列?数组甚至有可能吗?因此,链接列表的主要优点是您可以拥有一个不仅指向下一个元素的元素,而且还具有指向其他链接列表/数组/堆/或任何其他内存位置的元素。数组是一个平坦的连续内存,切成要存储的元素的块大小。另一方面,链接列表是一大块不连续的内存单元(可以是任意大小,可以存储任何内容),并指向每个其他您想要的方式。同样,假设您正在制作USB驱动器。现在,您是否希望将文件保存为任何数组或链接列表?我想您知道我指的是什么:)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.