.split_off
上的std::collections::LinkedList
方法被描述为具有O(n)时间复杂度。从(docs):
pub fn split_off(&mut self, at: usize) -> LinkedList<T>
在给定的索引处将列表分成两部分。返回给定索引之后的所有内容,包括索引。
此操作应以O(n)时间计算。
为什么不使用O(1)?
我知道链表在Rust中并不简单。 this book和this article的方式和原因有很多资源,但我还没有机会深入探讨这些资源或标准库的源代码。
是否有关于在(安全)Rust中拆分链表时所需的额外工作的简要说明?
这是唯一的方法吗?如果不是,为什么选择这种实现方式?
答案 0 :(得分:8)
也许我对您的问题有误解,但是在链接列表中,必须跟随每个节点的链接才能继续到下一个节点。如果要到达第三个节点,则从第一个节点开始,按照其链接指向第二个节点,然后最终到达第三个节点。
此遍历的复杂度与目标节点索引 n 成正比,因为 n 节点已处理/遍历,因此它是线性O( n )操作,而不是恒定时间O(1)操作。列表被“分割”的部分当然是固定时间,但是整个分割操作的复杂性由到达分离点节点之前产生的主导项O( n )决定。甚至可以进行拆分。
可能存在O(1)的一种方式是,如果存在指向节点的指针,则在该指针之后会拆分列表,但这不同于指定目标节点索引。另外,可以保留索引以将节点索引映射到相应的节点指针,但是在保持索引更新与列表操作同步方面会占用额外的空间和处理开销。
答案 1 :(得分:5)
pub fn split_off(&mut self, at: usize) -> LinkedList<T>
在给定的索引处将列表分成两部分。返回给定索引之后的所有内容,包括索引。
此操作应以O(n)时间计算。
文档可以是:
n
应该是索引,n
是列表的长度(通常的含义)。在实现中可以看到,适当的复杂度是O(min(at,n-at))(以较小者为准)。由于at
必须小于n
,因此文档证明O(n)限制了复杂度(已达到at = n / 2
),但是这样大的限制是无益的。 / p>
也就是说,如果list.split_off(5)
是10或1,000,000,list.len()
花费的时间就很重要!
注意:我鼓励您使用split_off
操作编写链表的伪代码实现;您会意识到,这是在不改变数据结构的前提下可以得到的最好的结果。
答案 2 :(得分:5)
方法LinkedList::split_off(&mut self, at: usize)
首先必须从开始(或结束)到位置at
遍历列表,其位置为O(min({at
,n-{{1 }})) 时间。实际的分割是一个恒定时间的操作(如您所说)。并且由于此 min()表达式令人困惑,因此我们仅将其替换为合法的at
。因此:O(n)。
为什么这样设计方法?问题比这种特定方法还要深:标准库中的大多数n
API并没有真正有用。
由于缓存不友好,因此链表通常是存储顺序数据的错误选择。但是链接列表具有一些不错的属性,这些属性使它们成为少数几种罕见情况下的最佳数据结构。这些不错的属性包括:
有什么事吗?链表设计用于以下情况:您已经有了指向要执行任务的位置的指针。
Rust的LinkedList
与许多其他变量一样,只存储指向起点和终点的指针。要在链接列表中具有指向元素的指针,您需要类似LinkedList
之类的东西。在我们的情况下,该值为IterMut
。集合上的迭代器可以像指向特定元素的指针一样起作用,并且可以小心地进行提前(即,不使用Iterator
循环)。实际上,for
允许您在O(1)中的列表中间插入一个元素。哇!
但是此方法不稳定。缺少删除当前元素或在该位置拆分列表的方法。为什么?由于存在恶性循环:
IterMut::insert_next
几乎缺少使链接列表有用的所有功能LinkedList
请注意,偶尔会有一些勇敢的人试图改善这种状况。在the tracking issue about insert_next
中,人们认为LinkedList
可能是执行这些O(1)操作的错误概念,而我们想要的是“光标”之类的东西。 here有人建议将一堆方法添加到Iterator
中(包括IterMut
!)。
现在,有人只需要编写一个不错的RFC,就需要实现它。也许cut
会几乎不再有用了。
编辑2018-10-25 :某人did write an RFC。让我们期盼最好的结果!
编辑2019-02-21 :RFC已被接受! Tracking issue。