有没有更有效的方法来找到单链表的中间? (任何语言)

Is there a more efficient way to find the middle of a singly-linked list? (Any language)

让我们设置 context/limitations:

  1. 一个链表由 Node 对象组成。
  2. 节点仅具有对其下一个节点的引用。
  3. 对列表的引用只是对头节点对象的引用。
  4. 除了构建之外,链表没有进行任何预处理或索引(没有其他对内部节点的引用或收集的统计信息,即长度)。
  5. 列表中的最后一个节点对其下一个节点有空引用。

下面是我提出的解决方案的一些代码。

Node cursor = head;
Node middle = head;

while (cursor != null) {
    cursor = cursor.next;
    if (cursor != null) {
        cursor = cursor.next;
        middle = middle.next;
    }
}
return middle;

在不改变链表架构(不切换到双向链表或存储长度变量)的情况下,是否有更有效的方法来找到单链表的中间元素?


注意:当此方法找到偶数个节点的中间时,它总是找到左中间。这是理想的,因为它使您可以同时访问两者,但如果更有效的方法总能找到正确的中间点,那也很好。

不,鉴于您掌握的信息,没有比这更有效的方法了。

考虑从一个节点到下一个节点的转换。您 执行N 转换来算出列表长度。然后你 执行 N/2 转换以找到中间点。

无论您是先进行全扫描,然后根据发现的长度进行半扫描,还是 运行 cursor(以两倍速度)和 middle (在正常速度下)并行指针在这里不相关,转换总数保持不变。

加快速度的唯一方法是向数据结构中引入 额外的 信息,但为了完整起见,我将其包括在内这里。例子是:

  • 使它成为一个带有 headtail 指针的双向链表,所以你可以在 N transitions by "squeezing" 中找到它从两端到中间。这使指针的存储要求加倍,但可能不合适。

  • 有一个跳跃列表,每个节点都指向它的 "child" 和它的 "grandchild"。这将加速 cursor 转换,导致总共只有大约 N(即 cursormiddle 中的每一个的 N/2)。与前一点一样,每个节点都需要一个额外的指针。

  • 单独维护列表的长度,以便您可以在 N/2 转换中找到中间部分。

  • 与上一点相同,但在某些情况下缓存中间节点以提高速度。


最后一点需要额外检查。与许多优化一样,您可以用 space 换取时间,缓存显示了一种方法。

首先,维护链表的长度一个指向中间节点的指针。长度初始为零,中间指针初始设置为 null.

如果在长度为零时要求您提供中间节点,只需 return null。这是有道理的,因为列表是空的。

否则,如果你要求中间节点,指针是null,那肯定是因为你还没有缓存值

在这种情况下,使用长度(N/2 转换)计算它,然后 存储 该指针以备后用,在 return 之前 return。

顺便说一句,添加到列表的 end 时有一种特殊情况,这种情况很常见,需要特殊代码。

当长度从偶数变为奇数时添加到末尾时,只需将 middle 设置为 middle->next 而不是将其设置回 null

这将节省重新计算并起作用,因为您 (a) 有 next 指针,并且 (b) 您可以算出中间 "index"(基于一并选择根据你原来的问题一对)改变给定的长度:

Length   Middle(one-based)
------   -----------------
     0      none
     1         1
     2         1
     3         2
     4         2
     5         3
     :         :

这种缓存意味着,如果列表不变(或仅在末尾发生变化),下次您需要中间元素时,它几乎是瞬时的。

如果您曾经从列表中删除一个节点(或在末尾以外的地方插入),请将中间指针设置回空。下次需要时将重新计算(并重新缓存)它。

因此,对于最小的额外存储要求,您可以获得相当多的速度,尤其是在需要中间元素的频率高于列表更改频率的情况下。