这不是问题的答案,而是 cmets 的答案,因为我要说的内容需要图表,因此在 cmets 中不起作用。
锦写道:
但是我的双链表在 O(1) 中运行,如果我们假设只有数据而不是“指针”是不可变的,那么你仍然可以在 O(1) 时间内复制整个列表,因为您在添加或删除时所做的唯一事情是更改或创建指向旧列表的指针(参考单元格),然后我们仍然拥有旧列表的副本,而无需再次复制每个元素。
如果您尝试这样做,您会发现对于双向链表,实际上您不能保留旧的列表指针。原因如下。
使用单链表,您可以在 O(1) 时间内预先添加到列表中,同时保持指向旧列表的所有指针不变。这是一个例子:
包含三个项目的旧列表:
添加新头后的新列表:
注意其他代码的引用是如何保持不变的。其他代码引用的旧列表是["Item 1"; "Item 2"; "Item 3"]。新列表是["New head item"; "Item 1"; "Item 2"; "Item 3"]。但是代码的不同部分所持有的引用仍然指向一个格式良好的列表。正如您即将看到的那样,“格式良好”的部分很重要。
使用双向链表,事情会变得更加复杂——事实证明,不可能保持不变性并且 O(1) 时间。首先让我们看一下包含三个项目的旧列表:
这是一个结构良好的双向链表。它遵循所有格式良好的双向链表都应遵循的以下属性:
- 所有节点都有一个 Fwd 和 Back 指针。
- 除头节点外的所有节点都有一个有效的(非空)反向指针。只有头节点的 Back 指针是
null。
- 除尾节点外的所有节点都有一个有效的(非空)Fwd 指针。只有尾节点的 Fwd 指针是
null。
- 从不是尾部的任何节点,前进然后后退应该会带您回到您开始的同一节点。
- 从不是头部的任何节点,后退然后前进应该会将您带回到 您开始的同一节点。
现在,我们如何添加一个新的头项,同时仍然确保来自其他代码的引用继续指向一个格式良好的双向链表?
这是第一次尝试。我们添加新的头项,调整它的 Fwd 指针指向“旧”头节点,并重写该节点的 Back 指针指向新的头节点:
这仍然是一个格式正确的列表,您可以轻松验证。所有五个属性仍然适用于每个节点。但是等等!来自代码其他部分的引用已经从下面的列表中更改了!之前它指向三个项目的列表,现在它指向 四个 项目列表的第二个项目!如果其他代码只是向前迭代,它不会注意到变化。但是当它试图向后迭代时,它会注意到有一个以前不存在的新头部项目! 我们已经打破了不变性承诺。不变性是对使用我们数据结构的其他代码的保证,即“如果你引用了这个数据结构,你看到的数据永远不会从你下面改变出来。” 我们刚刚打破了这个承诺:旧代码用于查看列表["Item 1"; "Item 2"; "Item 3"],现在它看到列表["New head item"; "Item 1"; "Item 2"; "Item 3"]。
好的,那么。有没有办法保持这个承诺,而不改变其他代码看到的内容?好吧,我们可以尝试不重写那个旧的头节点;这样旧代码仍然会看到一个包含三个项目的双向链表,每个人都很高兴,对吧?好吧,让我们看看如果我们这样做会是什么样子:
太棒了:其他代码仍然看到与以前完全相同的双向链表,并且无法从旧列表到新的头节点。因此,其他代码的任何部分如果试图从列表的头部向后移动,就会发现头部仍然是null,就像它应该的那样。但是等等:一个格式良好的列表的五个属性呢?好吧,事实证明我们已经违反了属性 #4:从头节点开始,向前然后向后返回以空指针结束,不是我们开始的节点。所以我们不再有一个格式正确的列表:太糟糕了。
好的,所以这种方法行不通。我们还能尝试什么。嗯……嘿!我有个主意!让我们复制旧的头节点,并在我们不理会旧的头节点的同时调整副本!这仍然是 O(1),因为我们知道我们只复制一个节点。然后其他代码看到的正是它以前看到的内容,一个包含三个项目的列表,但新列表有四个项目。杰出的!正是我们想要的,对吧?好吧,我们来看看:
好的,这行得通吗?好吧,其他代码引用了旧的、未更改的头节点,所以这很好:它永远不会意外地看到新数据,所以它仍然可以继续看到它曾经拥有的东西,一个包含三个项目的列表。好的。从新的头节点开始,我们可以前进和后退,最终回到我们开始的地方,这很好……但是等等……不,还是有问题。从 item 1 的副本 节点,向前然后返回将我们带到 old“item 1”节点,而不是“item 1 的副本”节点。所以我们还是违反了良构列表的性质,而这个列表也不是良构的。
对此也有一个答案:复制其中包含项目 2 的节点。我已经厌倦了绘制图表并且这个答案越来越长,所以我会让你自己解决这个问题 - 但你很快就会看到,带有第 2 项副本的节点有同样的问题之前:前进和后退会将您带到“旧”项目 2。(否则您已经调整了“旧”项目 2 节点,从而破坏了不变性承诺,因为其他代码现在可以通过以下方式查看“新”数据一些 Fwd 和/或 Back 操作)。
但是那个也有一个解决方案:也复制第 3 项。我也不会画那个图,但你可以自己画出来。您会发现,一旦将项目 1、2、和 3 复制到新列表中,您就成功地满足了 both 不变性承诺,和格式良好的列表的所有属性。其他代码仍然看到未修改的旧列表,而新列表中有四个项目。唯一的问题是,您必须复制 列表中的每个项目——根据定义,这是一个 O(N) 操作——才能获得此结果。
总结:单链表具有三个属性:
- 您可以在 O(1) 时间内预先添加项目。
- 引用旧列表的其他代码在您执行前置操作后仍会看到相同的数据。
- 旧列表和新列表都是格式正确的。
但是,使用双向链表,您只能拥有这三个属性中的 两个。您可以进行 O(1) 前置操作并维护格式正确的列表,但随后任何其他代码都会看到列表数据发生变化。或者您可以在前面添加 O(1) 并且仍然让其他代码看到与以前相同的数据,但是您的列表将不再是格式正确的。或者,通过复制列表中的每个节点,您可以让其他代码仍然看到与以前相同的数据,并且您的新列表将是格式良好的——但您必须执行 O(N) 操作才能实现这个结果。
这就是为什么我说不可能有一个具有 O(1) 操作的不可变双向链表。