如何通过一次查找链表的中间元素?


12

数据结构和算法中最受欢迎的问题之一,通常是电话面试中提出的问题。


5
如果此主题中给出的答案是面试官所期望的,那么此问题并不考验技术能力,而是考生能像律师一样躲避的程度。
G. Bach

2
这是一个可怕的采访问题,因为它严格取决于模糊,模棱两可,主观的“ 通过 ” 一词。这个问题几乎所有好的答案都涉及滥用定义,以便您可以有效地忽略它。
RBarryYoung '16

2
好吧,这个问题在这里引起了很多讨论。这意味着从一个方面来说这是一个很好的面试问题:它使您开始思考。
亨德里克

3
我想回答“将所有元素读入向量,然后在位置size()/ 2处访问元素”
Cort Ammon

1
@HendrikJan实际上,我认为这是一个非常糟糕的面试问题。首先,它很可能引发关于“一口气”确切含义的争论,而不是富有成效的讨论。其次,候选人可以找出“正确”的答案,然后拒绝该答案,因为他们认为该答案违反了“一次性通过”标准。第三,因为这是一个众所周知的问题,所以可以更好地测试“您知道流行的面试问题吗?”。比“您胜任这份工作吗?” 其中任何一个都应该足以解决这个问题。这三个同时都是灾难。
David Richerby '16

Answers:


24

通过作弊,并同时并行进行两遍。但是我不知道招聘人员是否会喜欢这样。

可以在一个链接列表上完成,这是一个不错的技巧。列表上有两个指针,其中一个指针是双倍速度。当一个快结束时,另一个快到一半。


1
是的,目前尚不清楚这是否是一次通行证。在这一点上,这个问题尚不清楚。
David Richerby '16

2
顺便说一句,这与难题有关,您有两支蜡烛,每支蜡烛燃烧一个小时,然后要求您测量45分钟。
亨德里克

2
然后迭代列表,计数元素,然后第二次迭代到中间点,这真的有什么不同吗?所有不同之处在于您迭代多余的一半。正如@RBarryYoung在另一个类似的答案中提到的那样,这确实不是一次通过,而是一次通过。

2
如果该列表很长,则与第二次开始迭代相比,“并行”移动两个指针将导致更少的缓存未命中。
zwol

2
它使用与草龟和野兔算法相同的原理进行周期检测。
约书亚·泰勒

7

如果它不是双向链接列表,则可以只计算并使用一个列表,但这在最坏的情况下需要将内存加倍,并且如果列表太大而无法存储在内存中,它将根本无法工作。

一个简单,几乎愚蠢的解决方案是每两个节点增加中间节点

function middle(start) {
    var middle = start
    var nextnode = start
    var do_increment = false;
    while (nextnode.next != null) {
        if (do_increment) {
             middle = middle.next;
        }
        do_increment = !do_increment;
        nextnode = nextnode.next;
    }
    return middle;
}

第二种选择是正确答案(当然是恕我直言)。
马修·克鲁姆利

1
实际上是在链表上进行了1 1/2次传球。
RBarryYoung '16

6

阐述亨德里克的答案

如果是双链表,则从两端进行迭代

function middle(start, end) {
  do_advance_start = false;
  while(start !== end && start && end) {
     if (do_advance_start) {
        start = start.next
     }
     else {
        end = end.prev
     }
     do_advance_start = !do_advance_start
  }
  return (start === end) ? start : null;
}

给定 [1, 2, 3] => 2

1, 3
1, 2
2, 2

给定 [1, 2] => 1

1, 2
1, 1

给定 [1] => 1

给定 [] => null


效率如何?您也在迭代n次而不是n / 2。
卡兰·汉娜

3

创建一个结构,该结构的指针能够指向链接列表的节点,并带有一个整数变量,该变量保留列表中节点的数量。

struct LL{
    struct node *ptr;
    int         count;
}start;

现在,将链接列表的第一个节点的地址存储在并初始化。在链表中成功创建新节点后,请 确保的值增加一。同样,每当从链接列表中删除节点时,将其递减1。小号一个ř Ç Ò ù Ñ = 1 小号一个ř Ç Ò ù Ñ start.ptrstart.count=1
start.count

使用即可找到中间元素。start.count


-2

创建一个动态数组,其中数组的每个元素都是从头开始以遍历顺序指向列表中每个节点的指针。创建一个初始化为1的整数,该整数跟踪您访问过的节点数(每次访问新节点时该节点递增)。到达最后时,您知道列表的大小,并且每个节点都有一个有序的指针数组。最后,将列表的大小除以2(对于基于0的索引,将其减去1)并获取保存在该数组索引中的指针;如果列表的大小为奇数,则可以选择要返回的元素(我仍将返回第一个元素)。

这是一些可以理解这一点的Java代码(即使动态数组的想法有点奇怪)。我会提供C / C ++,但在那方面我很生疏。

public Node getMiddleNode(List<Node> nodes){

    int size = 1;
    //add code to dynamically increase size if at capacity after adding
    Node[] pointers = new Node[10];

    for (int i = 0; i < nodes.size(); i++){
        //remember to dynamically allocate more space if needed
        pointers[i] = nodes.get(i);
        size++;
    }

    return pointers[(size - 1)/2];

}

-3

递归是否被视为不只一次通过?

将列表遍历到末尾,并通过引用传递一个整数计数。在每个级别上对该值进行本地复制以供以后参考,并增加进入下一个调用的参考计数。

在最后一个节点上,将计数除以二,然后将结果截断/ floor()(如果您希望只有两个元素的情况下第一个节点为“中间”)或舍入(如果希望第二个节点为中间”)。适当使用基于零或一的索引。

展开,将引用计数与本地副本(这是节点的编号)匹配。如果相等,则返回该节点。否则,返回从递归调用返回的节点。

还有其他方法可以做到这一点。其中一些可能不那么麻烦(我以为我看到有人说将其读入数组并使用数组长度来确定中间值-荣誉)。但坦率地说,没有很好的答案,因为这是一个愚蠢的面试问题。第一,仍然使用链表(支持意见);第二,找到中间节点是一个随意的,学术性的练习,在现实生活中没有价值。第三,如果我真的需要知道中间节点,那么我的链表将公开一个节点数。与每次我想要中间节点时浪费时间遍历整个列表相比,维护该属性要容易得多。最后,四个,每个面试官都会喜欢或拒绝不同的答案-一位面试官认为这很狡猾,另一位面试官则认为这很荒谬。

我几乎总是用更多的问题回答面试问题。如果我遇到这样的问题(我从未遇到过),我会问(1)您在此链表中存储了什么,并且如果确实有需要,是否有更合适的结构可以有效地访问中间节点; (2)我的约束是什么?如果内存不是问题(例如数组答案),我可以使速度更快,但是如果访问员认为增加内存浪费,我会感到沮丧。(3)我将使用哪种语言开发?我所了解的几乎每一种现代语言都有内置的类来处理链接列表,而这些链接列表使遍历列表不再必要-为什么要重新发明一些已被语言开发人员调整以提高效率的东西?


6
您不知道是谁对某事投了反对票,因此您得出的结论是一个人对所有事都投了赞成票,这可能是正确的,也可能是不正确的,但是,这一结论当然没有事实依据。但是我拒绝您的回答,因为我们正在寻找解释,而不是大量的代码。
David Richerby '16

这可能是一种假设,但是当我看片刻且少于30秒后,每则帖子的精确度都为-1,这并不是不合理的。即使是5个或6个不同的人,也没有一个人留下评论为什么。但是,谢谢您至少说明了一个原因。我不明白为什么一个冗长的解释比代码更好-是的,这是电话采访,但是我没有给OP一个反驳的罐头答案,而是向他展示了一种解决方法。IE浏览器我认为不赞成对帖子进行投票,因为它包含代码,这是没有必要的,但是至少要感谢您说出您这样做的理由。
James K

公平地说-我没有想到您可以知道投票的时间(我认为,在发生投票时加载页面是像我们这样的普通用户唯一的发现方式)。关于我们为什么要在此处避免使用实际代码,我们有一些中继文章:(1) (2)
David Richerby '16

感谢您的元链接。我确实阅读了常见问题解答,但那里没有看到任何内容-并不是我会注意到类似的情况,很可能不是我所期望的。但是,当我叮叮当当再检查时,我也找不到任何东西。我可能仍然忽略了它,但是我确实看到了。元帖子中的推理是有道理的;感谢您的回应。
James K

-5

通过使用2个指针。在每个迭代中增加一个,在第二个迭代中增加另一个。当第一个指针指向链表的末尾时,第二个指针将指向链表的中间模式。


这只是复制亨德里克的答案。除非您有其他要说的内容,否则请不要回答。
David Richerby '16
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.