双指针 - 判断链表是否存在环
题目如下:
Given
head
, the head of a linked list, determine if the linked list has a cycle in it.There is a cycle in a linked list if there is some node in the list that can be reached again by continuously following the
next
pointer. Internally,pos
is used to denote the index of the node that tail’snext
pointer is connected to. Note thatpos
is not passed as a parameter.Return
true
if there is a cycle in the linked list. Otherwise, returnfalse
.
解题思路:
这道题也挺长的,而且想要能够答出这题就必须知道什么是链表。
-
什么是链表
简单的描述一下就是,链表是链式关联的一个个节点,每个节点会存储自身的数据和下一个节点的位置,如图:
-
链表中是否存在一个环
也就是说,顺着第一个节点 head 一直向下找,最终能够找回到链表中的某个节点,这就代表这链表中存在一个闭环,如图:
-
如果存在闭环,则返回
true
,不存在闭环,则返回false
-
寻找到闭环的方法
已知顺着当前节点能够找到下一个节点,而形成闭环的要求就是,链表中的一个节点的下一个节点,是存在于链表内部的节点,这样就会形成无限循环。
所以根据定义就可以知道,如果能找到一个节点,它的下一个节点存在于链表中,那么就能够存在闭环。
-
这道题提示是双指针
所以可以设定两个指针都指向自己,在每次移动时,指针 1 移动 1 个节点,指针 2 移动两个节点。
如果存在闭环,那么在移动过程之中二者一定会相遇;如果不存在闭环,移动数量比较多的那个指针总会遇到空值——尾节点的下一个地址的引用对象必然为 undefined。
根据解题思路解题:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function (head) {
// 边界条件
// 如果多加一个边界条件 head.next.next === null
// 就能提速16ms,就差不多是20%
// 所以说最能提速的方法还是多加边界条件……
if (head === null || head.next === null || head.next.next === null) {
return false;
}
let headNode = head,
tailNode = head.next;
while (
headNode !== null &&
headNode.next !== null &&
tailNode !== null &&
tailNode.next !== null &&
tailNode.next.next !== null
) {
if (headNode === tailNode) return true;
headNode = headNode.next;
tailNode = tailNode.next;
tailNode = tailNode.next;
}
return false;
};
其实如果没有双指针这个提示的话,我大概会用 Set 去解这道题:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function (head) {
if (head === null || head.next === null) {
return false;
}
let curr = head.next;
const nodes = new Set([head]);
while (curr !== null) {
nodes.add(curr);
curr = curr.next;
if (nodes.has(curr)) {
return true;
}
}
return false;
};
如果下一个节点是在 Set 中已有的元素,那么就代表有闭环,可以即时返回。
理论上来说会快一些,不过从实际测试的角度上来说,时间没怎么提速,反倒是空间有些浪费:
第一、二条数据是双指针,第三条数据是使用 set 的解题思路,使用 set 提速还不如多加一个边界条件(笑)
不过正常来说,runtime 能够超过 50%就差不多了,70-80%应该已经算是比较优的解题思路了,想要 90%以上,真的是只能靠加莫名奇妙的边界条件来提前返回。
可以加奇奇怪怪的边界条件,但是没必要。
日常开发还是要以代码可读性为准,如果因为加了特别多的边界条件而降低了代码的可读性,那么就得不偿失了。
最近在用 python 刷题,所以补充一下 python 的解法:
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
现在回头看看,上面用 JS 解的确实太过冗杂了……直接判断 fast
的 truthy value 就可以了,毕竟不可能存在 fast
. 不为空,但是 slow
.为空的情况