Question Code it now!
Given a linked list, determine if it has a cycle in it.
Follow up:
Can you solve it without using extra space?
Solution
Hash table – O(n) time, O(n) space
To detect if a list is cyclic, we can check whether a node had been visited before. A natural way is to use a hash table.
We go through each node one by one and record each node's reference (or memory address) in a hash table. If the current node is null
, we have reached the end of the list and it must not be cyclic. If current node’s reference is in the hash table, then return true.
public class Solution { public boolean hasCycle(ListNode head) { Set<ListNode> nodesSeen = new HashSet<>(); while (head != null) { if (nodesSeen.contains(head)) { return true; } else { nodesSeen.add(head); } head = head.next; } return false; } }
Complexity analysis:
Assume that n is the length of list, traversing the list costs O(n) time. Adding a node in the hash table costs only O(1) time.
Two Pointers – O(n) time, O(1) space
The space complexity can be reduced to O(1) by considering two pointers at different speed - a slow pointer and a fast pointer. The slow pointer moves one step at a time while the fast pointer moves two steps at a time.
If there is no cycle in the list, the fast pointer will eventually reach the end and we can return false in this case.
Now consider a cyclic list and imagine the slow and fast pointers are two runners racing around a circle track. The fast runner will eventually meet the slow runner. Why? Consider this case (we name it case A) - The fast runner is just one step behind the slow runner. In the next iteration, they both increment one and two steps respectively and meet each other.
How about other cases? For example, we have not considered cases where the fast runner is two or three steps behind the slow runner yet. This is simple, because in the next or next's next iteration, this case will be reduced to case A mentioned above.
public class Solution { public boolean hasCycle(ListNode head) { if (head == null || head.next == null) { return false; } ListNode slow = head; ListNode fast = head.next; while (slow != fast) { if (fast == null || fast.next == null) { return false; } slow = slow.next; fast = fast.next.next; } return true; } }
Complexity analysis:
We only use two nodes (slow and fast) so the space complexity is O(1).
Let us denote n as the total number of nodes in the linked list. To analyze its time complexity, we consider the following two cases separately.
List has no cycle:
The fast pointer reaches the end first and the run time depends on the list's length, which is O(n).
List has a cycle:
We break down the movement of the slow pointer into two steps, the non-cyclic part and the cyclic part:
The slow pointer takes "non-cyclic length" steps to enter the cycle. At this point, the fast pointer has already reached the cycle. Number of iterations=non-cyclic length=N
Both pointers are now in the cycle. Consider two runners running in a cycle - the fast runner moves 2 steps while the slow runner moves 1 steps at a time. Since the speed difference is 1, it takes difference of speeddistance between the 2 runners loops for the fast runner to catch up with the slow runner. As the distance is at most "cyclic length K" and the speed difference is 1, we conclude that Number of iterations=almost "cyclic length K".
Therefore, the worst case time complexity is O(N+K), which is O(n).
Analysis written by: @tianyi8, revised by @1337c0d3r.
问题 点我链接至原问题!
给定一个单链表,判断其是否存在环。
提高:
你能不使用额外存储空间来解决它吗?
解法如下
哈希表 – O(n) 时间复杂度, O(n) 空间复杂度
我们可以通过检查一个节点是否被访问过来判断其是否存在环。普遍的方式是使用哈希表。
我们遍历所有节点并在哈希表中存储每个节点的引用(或内存地址)。如果当前节点为空节点(即已检测到链表尾部的下一个节点),那么我们已经遍历完整个链表,并且该链表不存在环。如果当前节点的引用已经存在于哈希表中,那么返回真(即链表中存在环)。
public class Solution { public boolean hasCycle(ListNode head) { Set<ListNode> nodesSeen = new HashSet<>(); while (head != null) { if (nodesSeen.contains(head)) { return true; } else { nodesSeen.add(head); } head = head.next; } return false; } }复杂度分析:
假定链表中有n个节点,便利一遍需要O(n)时间复杂度,而将节点插入哈希表中只需要O(1)时间复杂度。
两个指针 – O(n) 时间复杂度, O(1) 空间复杂度
如果分别利用两个速度不同的指针 - 快速的和慢速的 - 来遍历整个链表,那么空间复杂度就能降到O(1)。慢速指针每次移动一个节点,而快速指针每次移动两个。
如果链表中不存在环,那么快速指针最终会先到达尾部,此时我们就可以返回假了(即不存在环)。
现在考虑一个带环的链表,把慢速指针和快速指针想象成两个在环形赛道上跑步的赛跑者(以下将称之为快跑者与慢跑者)。而快跑者一定会追上慢跑者。为什么呢?考虑如下情况 - 假如快跑者只在慢跑者之后一步,在下一次迭代中,它们各自跑了一步或两步并相遇。
其他情况又会怎样呢?例如,我们没有考虑快跑者在慢跑者之后两步或三步等情况。但这其实不难想到,因为在下一次迭代中,又会变成上面提到的这种情况。
public class Solution { public boolean hasCycle(ListNode head) { if (head == null || head.next == null) { return false; } ListNode slow = head; ListNode fast = head.next; while (slow != fast) { if (fast == null || fast.next == null) { return false; } slow = slow.next; fast = fast.next.next; } return true; } }
复杂度分析:
我们仅用了两个指针(慢速和快速),因此时间复杂度为O(1)。
假定链表中一共有n个节点,为了分析时间复杂度,我们将分别考虑两种情况:
链表不存在环:
快速指针将会首先到达尾部,其时间取决于链表长度,即O(n)。
链表存在环:
我们将慢速指针的移动划分为两个部分:非环部分与环部分。
慢速指针在走完所有"非环部分"之后将进入环,此时快速指针已经进入到环中了。迭代次数 = 非环部分长度=N
现在两个指针都在环中。考虑两个在环形赛道上的赛跑者 - 快跑者每次移动两步而慢跑者每次只移动一步。其速度的差值为1,因此需要经过 速度差值 两人之间的距离 圈后快跑者才能追上慢跑者。 因为这个距离几乎就是链表周期部分的长度K,而速度差值又为1,我们可以得到 迭代次数 ≈ 环的长度K
因此,最差情况下时间复杂度为O(n+k),即O(n)。
撰写者:@tianyi8
修订者:@1337c0d3r
翻译员:judadeshu
终于写完啦~今天感冒头痛还在这里苦逼的翻译官方答案也是没sei了。。。这道题属于难度为Medium中与链表有关的一道,然而我并没能做出来。。。正好看到LeetCode官博推送了官方解答,顺手就拿过来翻译了一下,安慰安慰做不出题的自己。。。