问题 找到未知大小列表的中间位置


在最近的一次采访中,我被问到:

从第一个位置开始查找未知长度的排序列表的中间元素。

我回答这个问题:

有2个位置计数器:

计数器1 计数器2

将counter1递增1,将counter2递增2.当计数器2到达列表的末尾时,计数器1将位于中间。我觉得这样做效率不高,因为我正在重新审视我已经看过的节点。无论哪种方式,是否有更高效的算法?


2739
2017-10-21 00:06


起源

这是 O(N) 时间和 O(1) 空间;对我来说似乎很好。 - PengOne
如果长度未知,允许哪些操作?是一个 链表? - Vlad
我以为你可以做类似的但是相同的计数器,但不是跳2,跳得更多。跳跃计数器的跳跃距离1,然后当你达到你的边界计数一。这更好,无效还是有缺陷? - segFault
当时面试官的回答是什么? - cyber_raj
@segFault:需要共享列表类型(或至少是允许的操作)以提供最佳答案。 Strilanc为链表(+1)给出了很好的答案。我给出了我认为是阵列支持列表的最佳答案(您之前的评论似乎意味着)。 - Eric J.


答案:


假设有一个链表,你可以在任意接近N个项目时进行。

做5/4 N:

  • 迭代列表直到你结束,计算项目。
  • 在第2个元素的每个幂上放下一个锚点。跟踪最后2个锚点。
  • 迭代前一个锚点,直到它到达列表的中间位置。

当您点击列表的末尾时,前一个锚点位于中点之前,但至少已经有一半。所以N为完整迭代+最多1/4 N为锚= 5/4 N.

更频繁地丢弃锚点,例如在第1.5项的每个上限功率下,根据需要使你接近N(以跟踪更多锚点为代价;但对于任何给定的X作为功率步长,渐近记忆是恒定的) 。


9
2017-10-21 00:37



很好的答案。不知道二进制搜索的用途是什么;如果它是一个数组,我们可以从一开始就做得更好。 - davin
我的意思是一个你不知道确切长度的数组,但可以检查一个给定的位置是否超过了长度。也许它是一个没有误报并且插入了0..N的布隆过滤器。我会删除那个案子,因为它是如此奇怪和微不足道。 - Craig Gidney
@Strilanc如果你想办法制作一个没有误报的布隆过滤器,请告诉我们。 - Nick Johnson


我假设你正在讨论一个链表。确实,您的解决方案非常出色。另一种方法是简单地遍历列出元素数量的列表,然后从头开始,遍历计算量的一半。两种方法最终都会遍历 3n/2 节点,所以没有太大区别。

根据架构的不同,任何一种方法都可能存在轻微的缓存优势;第一种方法可能具有缓存节点的优势,如果缓存足够大,则在两个指针相距很远之前,这可能意味着更快的检索。或者,如果我们一次遍历列表,缓存可能会获得更好的块,而不是保持两个指针活着。


3
2017-10-21 00:18





假设你可以检测到你已超出列表的末尾并在列表中有效地寻找任意位置,你可以保持列表的假定长度加倍(猜测长度为1,然后是2,然后是4,...... )直到你超过列表的末尾,然后在最后尝试的值小于列表长度和超出列表末尾的第一个值之间使用二进制搜索来查找列表的实际结束。

然后,寻找位置END_OF_LIST / 2。

这样,您就不必访问每个节点。


2
2017-10-21 00:18



阵列的好解决方案! - kyun


从技术上讲,如果您使用O(N)内存(假设您正在使用链接列表),则可以在一次遍历中执行此操作。

  1. 遍历列表并将其转换为数组(如果这是一个链表,则为指针)。
  2. 返回[N / 2]

编辑:我确实喜欢你的答案!


0
2017-10-21 08:40





假设常规的内存中链表允许读取下一个元素,给出当前多次引用(免责声明,未经测试,但该想法应该有效):

// assume non-empty list
slow = fast = first;
count = 0;

while (fast)
{
  fast = fast.next;

  if (!fast)
    break;

  count++;
  fast = fast.next;

  if (fast)
    count++;

  slow = slow.next;
}

if (count % 2)
  return slow.data;
else
  return (slow.data + slow.next.data)/2.0;

更难的情况是“列表”不是内存中的链接列表,而是一个可以按排序顺序读取的流以及只读取一次的每个元素的内容,我没有一个很好的解决方案。


0
2018-04-08 23:51