在链表中找到循环的正常方案是移动指针一次并移动其他指针两次。如果它们相遇,则链表中有一个循环。
但如果我将第二个指针移动三次或四次会发生什么。它会降低复杂性吗?为什么我们只需要移动第二个指针两次。
boolean hasLoop(Node first) {
Node slow, fast;
slow = fast = first;
while(true) {
slow = slow.next;
if(fast.next != null)
fast = fast.next.next;
else
return false;
if(slow == null || fast == null)
return false;
if(slow == fast)
return true;
}
}
而不是fast.next.next,为什么我没有fast.next.next.next或fast.next.next.next.next.next?
答案 0 :(得分:3)
由于快速指针以双倍速度移动而不是慢速移动,因此两个指针之间的距离将始终增加1(最初它们之间的距离为2)。
现在假设循环存在,当慢速指针进入循环时,慢速和快速之间的距离称为“x”,循环的长度为“d”。所以现在下一次慢速和快速将移动,它们之间的距离将变为x + 1,然后在下一次移动时它将是x + 2,然后是x + 3,依此类推。只要它们之间的距离是d的倍数,快速和慢速就会相遇。因此,通过将它们之间的距离增加1,我们会检查每个值。
现在考虑快速移动三倍的情况,然后在它们之间的每一步距离将增加2,即x,x + 2,x + 4,依此类推。因此,这两个指针可能不会相互见面并相互交叉,如果发生这种情况,您的程序将永远不会终止。 如果快速指针的速度是四次,五次等,情况类似。
答案 1 :(得分:1)
当指针移动速度超过较慢指针速度的两倍时,它可以与第一个指针重叠而不会满足它。如果每次两个指针都经过(在同一个循环内)时会发生这种情况,那么算法将1.从不找到循环而且2.永远不会终止。
答案 2 :(得分:0)
复杂性不计入循环步数,而是访问的节点数。 因此,上述算法只会使NullPointerExceptions更加困难。循环也可以是任意长度的节点。
在朴素解决方案中将需要二次算法:检查节点[i]没有作为节点[j]出现所有j<岛
更快的算法使用内存。
通过生成标记(一种引用计数)作为节点的 field 。
// Complexity O(N)
class Node
Node next;
boolean generation;
class List
Node first;
boolean nodesGeneration; // All nodes false or true
boolean isCircular() {
for (Node node = first; node != null; node = node.next) {
if (node.generation != nodesGeneration) {
return false;
}
node.generation = !node.generation;
}
nodesGeneration = !nodesGeneration;
return true;
}
或者如果这不可能/期望:使用将Object引用作为键的IdentityHashMap。就像一组对象引用(==
)。
// Complexity O(N), worse case O(N . log N)
boolean isCircular() {
Map<Node, Boolean> checkedNodes = new IdentityHashMap<>();
for (Node node = first; node != null; node = node.next) {
if (checkedNodes.containsKey(node)) {
return false;
}
checkedNodes.put(node, Boolean.TRUE);
}
return true;
}
答案 3 :(得分:0)
不仅如此。
如果循环在链接列表中,程序将更快地找到循环。例如使用fast.next.next
,while将在10000节点链接列表中完成9998次。
但如果最后一个节点指向节点800,则需要9198次才能找到循环