假设您在 Java 中有一个链表结构。它由节点组成:
class Node {
Node next;
// some user data
}
每个 Node 都指向下一个节点,除了最后一个 Node,它的 next 为 null。假设列表有可能包含一个循环 - 即最终节点,而不是空值,具有对列表中在它之前的节点之一的引用。
最好的写作方式是什么
boolean hasLoop(Node first)
如果给定节点是带有循环的列表的第一个,则返回 true
,否则返回 false
?你怎么能写出来,让它占用恒定的空间和合理的时间?
这是带有循环的列表的图片:
https://i.stack.imgur.com/irw1S.jpg
finite amount of space and a reasonable amount of time?
:)
您可以使用 Floyd's cycle-finding algorithm,也称为 龟兔算法。
这个想法是有两个对列表的引用并以不同的速度移动它们< /强>。一个向前移动 1
个节点,另一个向前移动 2
个节点。
如果链表有循环,它们肯定会相遇。
否则,两个引用中的任何一个(或它们的下一个)都将变为空。
实现算法的Java函数:
boolean hasLoop(Node first) {
if(first == null) // list does not exist..so no loop either
return false;
Node slow, fast; // create two references.
slow = fast = first; // make both refer to the start of the list
while(true) {
slow = slow.next; // 1 hop
if(fast.next != null)
fast = fast.next.next; // 2 hops
else
return false; // next node null => no loop
if(slow == null || fast == null) // if either hits null..no loop
return false;
if(slow == fast) // if the two ever meet...we must have a loop
return true;
}
}
这是对 Fast/Slow 解决方案的改进,它可以正确处理奇数长度的列表并提高清晰度。
boolean hasLoop(Node first) {
Node slow = first;
Node fast = first;
while(fast != null && fast.next != null) {
slow = slow.next; // 1 hop
fast = fast.next.next; // 2 hops
if(slow == fast) // fast caught up to slow, so there is a loop
return true;
}
return false; // fast reached null, so the list terminates
}
slow == fast.next
则 slow
将在下一次迭代中等于 fast
;它最多只保存一次迭代,但每次迭代都要进行额外的测试。
slow
不能在 fast
之前变为 null,因为它遵循相同的引用路径(除非您同时修改了列表,在这种情况下所有赌注都关闭)。
优于弗洛伊德的算法
Richard Brent 描述了一个 alternative cycle detection algorithm,它很像兔子和乌龟 [弗洛伊德循环],除了这里的慢节点不移动,而是稍后以固定的时间间隔“传送”到快节点的位置.
说明可在 Brent's Cycle Detection Algorithm (The Teleporting Turtle) 获得。布伦特声称他的算法比弗洛伊德循环算法快 24% 到 36%。 O(n) 时间复杂度,O(1) 空间复杂度。
public static boolean hasLoop(Node root) {
if (root == null) return false;
Node slow = root, fast = root;
int taken = 0, limit = 2;
while (fast.next != null) {
fast = fast.next;
taken++;
if (slow == fast) return true;
if (taken == limit) {
taken = 0;
limit <<= 1; // equivalent to limit *= 2;
slow = fast; // teleporting the turtle (to the hare's position)
}
}
return false;
}
slow.next != null
?据我所知,slow
始终落后于或等于 fast
。
Turtle and Rabbit 的另一种解决方案,不太好,因为我暂时更改了列表:
这个想法是遍历列表,并在进行时将其反转。然后,当你第一次到达一个已经被访问过的节点时,它的下一个指针将指向“向后”,导致迭代再次向 first
前进,并在此处终止。
Node prev = null;
Node cur = first;
while (cur != null) {
Node next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
boolean hasCycle = prev == first && first != null && first.next != null;
// reconstruct the list
cur = prev;
prev = null;
while (cur != null) {
Node next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return hasCycle;
测试代码:
static void assertSameOrder(Node[] nodes) {
for (int i = 0; i < nodes.length - 1; i++) {
assert nodes[i].next == nodes[i + 1];
}
}
public static void main(String[] args) {
Node[] nodes = new Node[100];
for (int i = 0; i < nodes.length; i++) {
nodes[i] = new Node();
}
for (int i = 0; i < nodes.length - 1; i++) {
nodes[i].next = nodes[i + 1];
}
Node first = nodes[0];
Node max = nodes[nodes.length - 1];
max.next = null;
assert !hasCycle(first);
assertSameOrder(nodes);
max.next = first;
assert hasCycle(first);
assertSameOrder(nodes);
max.next = max;
assert hasCycle(first);
assertSameOrder(nodes);
max.next = nodes[50];
assert hasCycle(first);
assertSameOrder(nodes);
}
看看Pollard's rho algorithm。这不是完全相同的问题,但也许您会从中理解逻辑,并将其应用于链表。
(如果你很懒,你可以看看cycle detection——看看关于龟兔赛跑的部分。)
这只需要线性时间和 2 个额外的指针。
在 Java 中:
boolean hasLoop( Node first ) {
if ( first == null ) return false;
Node turtle = first;
Node hare = first;
while ( hare.next != null && hare.next.next != null ) {
turtle = turtle.next;
hare = hare.next.next;
if ( turtle == hare ) return true;
}
return false;
}
(大多数解决方案不检查 next
和 next.next
是否为空。此外,由于海龟总是在后面,您不必检查它是否为空 - 野兔已经这样做了。)
在这种情况下,到处都有大量的文本材料。我只是想发布一个图表表示,它真正帮助我掌握了这个概念。
当快与慢在 p 点相遇时,
快速行驶的距离 = a+b+c+b = a+2b+c
慢速行驶的距离 = a+b
因为快的比慢的快2倍。所以a+2b+c = 2(a+b),那么我们得到a=c。
所以当另一个慢指针再次从head运行到q时,同时快指针也会从p运行到q,所以它们在q点相遇。
https://i.stack.imgur.com/rbtDK.png
public ListNode detectCycle(ListNode head) {
if(head == null || head.next==null)
return null;
ListNode slow = head;
ListNode fast = head;
while (fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
/*
if the 2 pointers meet, then the
dist from the meeting pt to start of loop
equals
dist from head to start of loop
*/
if (fast == slow){ //loop found
slow = head;
while(slow != fast){
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
}
a
大于循环长度,则 fast 将进行多个循环,公式 distance (fast) = a + b + b + c
将更改为 a + (b+c) * k + b
引入额外的参数 k
,该参数计算由 fast one 生成的 lopps 数。
用户 unicornaddict 上面有一个很好的算法,但不幸的是,它包含一个奇数长度 >= 3 的非循环列表的错误。问题是 fast
可能会在列表末尾之前“卡住”, slow
赶上它,并且(错误地)检测到一个循环。
这是修正后的算法。
static boolean hasLoop(Node first) {
if(first == null) // list does not exist..so no loop either.
return false;
Node slow, fast; // create two references.
slow = fast = first; // make both refer to the start of the list.
while(true) {
slow = slow.next; // 1 hop.
if(fast.next == null)
fast = null;
else
fast = fast.next.next; // 2 hops.
if(fast == null) // if fast hits null..no loop.
return false;
if(slow == fast) // if the two ever meet...we must have a loop.
return true;
}
}
算法
public static boolean hasCycle (LinkedList<Node> list)
{
HashSet<Node> visited = new HashSet<Node>();
for (Node n : list)
{
visited.add(n);
if (visited.contains(n.next))
{
return true;
}
}
return false;
}
复杂
Time ~ O(n)
Space ~ O(n)
n
,已修复
equals
和 hashCode
的重复值。这不是一回事。它在最后一个元素上取消引用 null
。并且问题没有说明将节点存储在 LinkedList
中。
以下可能不是最好的方法——它是 O(n^2)。但是,它应该有助于完成工作(最终)。
count_of_elements_so_far = 0;
for (each element in linked list)
{
search for current element in first <count_of_elements_so_far>
if found, then you have a loop
else,count_of_elements_so_far++;
}
public boolean hasLoop(Node start){
TreeSet<Node> set = new TreeSet<Node>();
Node lookingAt = start;
while (lookingAt.peek() != null){
lookingAt = lookingAt.next;
if (set.contains(lookingAt){
return false;
} else {
set.put(lookingAt);
}
return true;
}
// Inside our Node class:
public Node peek(){
return this.next;
}
请原谅我的无知(我对 Java 和编程还很陌生),但为什么上述方法不起作用?
我想这并不能解决恒定空间问题……但至少可以在合理的时间内到达那里,对吗?它只会占用链表的空间加上具有 n 个元素的集合的空间(其中 n 是链表中的元素数,或到达循环之前的元素数)。对于时间,我认为最坏情况分析会建议 O(nlog(n))。 contains() 的 SortedSet 查找是 log(n) (检查 javadoc,但我很确定 TreeSet 的底层结构是 TreeMap,它又是一棵红黑树),并且在最坏的情况下(没有循环,或在最后循环),它必须进行 n 次查找。
如果我们被允许嵌入类 Node
,我会解决这个问题,因为我已经在下面实现了它。 hasLoop()
在 O(n) 时间内运行,并且只占用 counter
的空间。这似乎是一个合适的解决方案?或者有没有办法在不嵌入 Node
的情况下做到这一点? (显然,在实际实现中会有更多方法,如 RemoveNode(Node n)
等)
public class LinkedNodeList {
Node first;
Int count;
LinkedNodeList(){
first = null;
count = 0;
}
LinkedNodeList(Node n){
if (n.next != null){
throw new error("must start with single node!");
} else {
first = n;
count = 1;
}
}
public void addNode(Node n){
Node lookingAt = first;
while(lookingAt.next != null){
lookingAt = lookingAt.next;
}
lookingAt.next = n;
count++;
}
public boolean hasLoop(){
int counter = 0;
Node lookingAt = first;
while(lookingAt.next != null){
counter++;
if (count < counter){
return false;
} else {
lookingAt = lookingAt.next;
}
}
return true;
}
private class Node{
Node next;
....
}
}
您甚至可以在恒定的 O(1) 时间内完成此操作(尽管它不会非常快速或高效):您的计算机内存可以容纳的节点数量有限,例如 N 条记录。如果您遍历 N 条以上的记录,那么您就有了一个循环。
这是我的可运行代码。
我所做的是通过使用三个跟踪链接的临时节点(空间复杂度 O(1)
)来尊重链表。
这样做的一个有趣的事实是帮助检测链表中的循环,因为当你前进时,你不希望回到起点(根节点)并且临时节点之一应该为空,除非你有一个循环,这意味着它指向根节点。
该算法的时间复杂度为O(n)
,空间复杂度为O(1)
。
这是链表的类节点:
public class LinkedNode{
public LinkedNode next;
}
下面是一个简单的三个节点的测试用例的主要代码,最后一个节点指向第二个节点:
public static boolean checkLoopInLinkedList(LinkedNode root){
if (root == null || root.next == null) return false;
LinkedNode current1 = root, current2 = root.next, current3 = root.next.next;
root.next = null;
current2.next = current1;
while(current3 != null){
if(current3 == root) return true;
current1 = current2;
current2 = current3;
current3 = current3.next;
current2.next = current1;
}
return false;
}
这是一个简单的三个节点的测试用例,最后一个节点指向第二个节点:
public class questions{
public static void main(String [] args){
LinkedNode n1 = new LinkedNode();
LinkedNode n2 = new LinkedNode();
LinkedNode n3 = new LinkedNode();
n1.next = n2;
n2.next = n3;
n3.next = n2;
System.out.print(checkLoopInLinkedList(n1));
}
}
// To detect whether a circular loop exists in a linked list
public boolean findCircularLoop() {
Node slower, faster;
slower = head;
faster = head.next; // start faster one node ahead
while (true) {
// if the faster pointer encounters a NULL element
if (faster == null || faster.next == null)
return false;
// if faster pointer ever equals slower or faster's next
// pointer is ever equal to slower then it's a circular list
else if (slower == faster || slower == faster.next)
return true;
else {
// advance the pointers
slower = slower.next;
faster = faster.next.next;
}
}
}
boolean hasCycle(Node head) {
boolean dec = false;
Node first = head;
Node sec = head;
while(first != null && sec != null)
{
first = first.next;
sec = sec.next.next;
if(first == sec )
{
dec = true;
break;
}
}
return dec;
}
使用上述函数检测 java 中链表中的循环。
可以通过一种最简单的方法来检测链表中的循环,这会导致使用 hashmap 的 O(N) 复杂度或使用基于排序的方法的 O(NlogN) 复杂度。
当您从头开始遍历列表时,创建一个排序的地址列表。当您插入一个新地址时,检查该地址是否已经在排序列表中,这需要 O(logN) 复杂度。
我看不出有任何方法可以使这花费固定的时间或空间,两者都会随着列表的大小而增加。
我会使用 IdentityHashMap(假设还没有 IdentityHashSet)并将每个节点存储到地图中。在存储节点之前,您将在其上调用 containsKey。如果节点已经存在,则您有一个循环。
ItentityHashMap 使用 == 而不是 .equals,以便您检查对象在内存中的位置,而不是它是否具有相同的内容。
我可能非常迟到和新来处理这个线程。不过还是..
为什么节点的地址和指向的“下一个”节点不能存储在一个表中
如果我们可以这样制表
node present: (present node addr) (next node address)
node 1: addr1: 0x100 addr2: 0x200 ( no present node address till this point had 0x200)
node 2: addr2: 0x200 addr3: 0x300 ( no present node address till this point had 0x300)
node 3: addr3: 0x300 addr4: 0x400 ( no present node address till this point had 0x400)
node 4: addr4: 0x400 addr5: 0x500 ( no present node address till this point had 0x500)
node 5: addr5: 0x500 addr6: 0x600 ( no present node address till this point had 0x600)
node 6: addr6: 0x600 addr4: 0x400 ( ONE present node address till this point had 0x400)
因此形成了一个循环。
这种方法有空间开销,但实现更简单:
Loop 可以通过在 Map 中存储节点来识别。在放置节点之前;检查节点是否已经存在。如果节点已经存在于地图中,则意味着链表有循环。
public boolean loopDetector(Node<E> first) {
Node<E> t = first;
Map<Node<E>, Node<E>> map = new IdentityHashMap<Node<E>, Node<E>>();
while (t != null) {
if (map.containsKey(t)) {
System.out.println(" duplicate Node is --" + t
+ " having value :" + t.data);
return true;
} else {
map.put(t, t);
}
t = t.next;
}
return false;
}
此代码经过优化,将比选择作为最佳答案的代码更快地产生结果。此代码可以避免进入一个非常漫长的追逐前向和后向节点指针的过程,如果我们遵循“最佳”,该过程将在以下情况下发生答案'方法。通过下面的试运行,你会明白我想说什么。然后通过下面给出的方法看问题并测量没有。为找到答案所采取的步骤。
1->2->9->3 ^--------^
这是代码:
boolean loop(node *head)
{
node *back=head;
node *front=head;
while(front && front->next)
{
front=front->next->next;
if(back==front)
return true;
else
back=back->next;
}
return false
}
boolean hasLoop(Node first)
的最佳方法是什么,否则返回 false?
这是我在java中的解决方案
boolean detectLoop(Node head){
Node fastRunner = head;
Node slowRunner = head;
while(fastRunner != null && slowRunner !=null && fastRunner.next != null){
fastRunner = fastRunner.next.next;
slowRunner = slowRunner.next;
if(fastRunner == slowRunner){
return true;
}
}
return false;
}
您也可以使用上述答案中建议的弗洛伊德乌龟算法。
该算法可以检查单链表是否有闭环。这可以通过使用两个以不同速度移动的指针迭代一个列表来实现。这样,如果有一个循环,两个指针将在未来的某个时间点相遇。
请随时查看我关于链表数据结构的 blog post,其中我还包含了一个代码片段,其中包含上述算法的 java 语言实现。
问候,
安德烈亚斯(@xnorcode)
这是检测周期的解决方案。
public boolean hasCycle(ListNode head) {
ListNode slow =head;
ListNode fast =head;
while(fast!=null && fast.next!=null){
slow = slow.next; // slow pointer only one hop
fast = fast.next.next; // fast pointer two hops
if(slow == fast) return true; // retrun true if fast meet slow pointer
}
return false; // return false if fast pointer stop at end
}
// 链表查找循环函数
int findLoop(struct Node* head)
{
struct Node* slow = head, *fast = head;
while(slow && fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
return 1;
}
return 0;
}
如果链表结构实现了java.util.List。我们可以使用列表大小来跟踪我们在列表中的位置。
我们可以遍历节点,将当前位置与最后一个节点的位置进行比较。如果我们当前的位置超过了最后一个位置,我们检测到列表在某处有一个循环。
此解决方案占用的空间量恒定,但会随着列表大小的增加而线性增加完成时间。
class LinkedList implements List {
Node first;
int listSize;
@Override
int size() {
return listSize;
}
[..]
boolean hasLoop() {
int lastPosition = size();
int currentPosition = 1;
Node next = first;
while(next != null) {
if (currentPosition > lastPosition) return true;
next = next.next;
currentPosition++;
}
return false;
}
}
或作为实用程序:
static boolean hasLoop(int size, Node first) {
int lastPosition = size;
int currentPosition = 1;
Node next = first;
while(next != null) {
if (currentPosition > lastPosition) return true;
next = next.next;
currentPosition++;
}
return false;
}
我不确定这个答案是否适用于 Java,但我仍然认为它属于这里:
每当我们使用现代架构上的指针时,我们都可以期望它们是 CPU word aligned。对于 64 位架构,这意味着指针中的前 3 位始终为零。这让我们可以使用这个内存来标记我们已经看到的指针,通过将 1 写入它们的第一位。
如果我们遇到一个指针的第一位已经写入了 1,那么我们就成功地找到了一个循环,之后我们需要再次遍历该结构并将这些位屏蔽掉。完毕!
这种方法称为 pointer tagging,它在低级编程中被过度使用,例如 Haskell 将它用于某些 optimizations。
public boolean isCircular() {
if (head == null)
return false;
Node temp1 = head;
Node temp2 = head;
try {
while (temp2.next != null) {
temp2 = temp2.next.next.next;
temp1 = temp1.next;
if (temp1 == temp2 || temp1 == temp2.next)
return true;
}
} catch (NullPointerException ex) {
return false;
}
return false;
}
next
之前还需要对fast.next
进行空检查:if(fast.next!=null)fast=fast.next.next;