扑朔迷离的环形链表:JS算法题解(141. 环形链表和142. 环形链表 II)
2024-02-18 07:17:46
在计算机科学领域,链表作为一种基础的数据结构,其重要性不言而喻。它由一系列节点组成,每个节点都存储着数据以及指向下一个节点的指针。但是,在某些特定情况下,链表的尾节点指针可能会指向链表中的某个节点,而不是指向 null,这就形成了所谓的环形链表。环形链表的出现会给链表的遍历和操作带来一系列挑战,因此,如何检测和处理环形链表就成了一个关键问题。
为了更好地理解环形链表,我们先来回顾一下链表的基本概念。链表就像一条链子,由多个节点连接而成。每个节点都包含两部分:数据和指针。数据部分用于存储实际的数据,而指针部分则指向下一个节点的位置,就像链子上的一个环节连接着下一个环节一样。链表的第一个节点我们称之为头节点,最后一个节点则称之为尾节点。
在 JavaScript 中,我们可以用以下代码来创建一个简单的链表:
class Node {
constructor(data) {
this.data = data;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.tail = null;
}
add(data) {
const node = new Node(data);
if (!this.head) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
this.tail = node;
}
}
}
这段代码定义了两个类:Node
和 LinkedList
。Node
类表示链表中的一个节点,它包含数据 data
和指向下一个节点的指针 next
。LinkedList
类表示链表本身,它包含头节点 head
和尾节点 tail
。add
方法用于向链表中添加新的节点。
接下来,我们来探讨如何检测一个链表是否为环形链表。一种直观的方法是利用哈希表来记录已经遍历过的节点。在遍历链表的过程中,如果遇到一个节点已经在哈希表中存在,那就说明链表中存在环,因为我们又回到了之前访问过的节点。
function hasCycle(head) {
const visited = new Set();
while (head) {
if (visited.has(head)) {
return true;
}
visited.add(head);
head = head.next;
}
return false;
}
这个 hasCycle
函数接收链表的头节点 head
作为参数。它使用一个 Set
来存储已经访问过的节点。在遍历链表的过程中,如果当前节点已经在 Set
中,则返回 true
,表示链表存在环;否则,将当前节点添加到 Set
中,并继续遍历下一个节点。如果遍历完整个链表都没有发现重复的节点,则返回 false
,表示链表不存在环。
除了哈希表方法,还有一种更巧妙的方法可以用来检测环形链表,那就是快慢指针法。顾名思义,这种方法使用两个指针,一个快指针和一个慢指针,快指针每次移动两个节点,慢指针每次移动一个节点。如果链表中存在环,那么快指针最终一定会追上慢指针,就像在操场上跑步一样,跑得快的人最终会超过跑得慢的人一圈。
function hasCycle(head) {
if (!head || !head.next) {
return false;
}
let slow = head;
let fast = head.next;
while (slow !== fast) {
if (!fast || !fast.next) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
这个 hasCycle
函数也接收链表的头节点 head
作为参数。它首先判断链表是否为空或者只有一个节点,如果是,则直接返回 false
,因为这样的链表不可能存在环。然后,它初始化慢指针 slow
和快指针 fast
,分别指向头节点和头节点的下一个节点。在 while
循环中,如果快指针或者快指针的下一个节点为空,则说明链表不存在环,返回 false
;否则,慢指针前进一个节点,快指针前进两个节点。如果慢指针和快指针相遇,则说明链表存在环,返回 true
。
如果我们已经确定链表中存在环,那么接下来就需要找到环的入口节点,也就是环开始的地方。一种方法是再次利用哈希表,记录已经遍历过的节点。当遇到第一个已经在哈希表中存在的节点时,这个节点就是环的入口节点。
function findCycleEntrance(head) {
const visited = new Set();
while (head) {
if (visited.has(head)) {
return head;
}
visited.add(head);
head = head.next;
}
return null;
}
这个 findCycleEntrance
函数接收链表的头节点 head
作为参数。它使用一个 Set
来存储已经访问过的节点。在遍历链表的过程中,如果当前节点已经在 Set
中,则返回当前节点,因为它就是环的入口节点;否则,将当前节点添加到 Set
中,并继续遍历下一个节点。如果遍历完整个链表都没有发现重复的节点,则返回 null
,表示链表不存在环。
另一种找到环入口节点的方法仍然是利用快慢指针。当快慢指针第一次相遇时,我们将慢指针重置到头节点,然后让快慢指针以相同的速度(每次移动一个节点)前进。当它们再次相遇时,相遇的节点就是环的入口节点。
function findCycleEntrance(head) {
if (!head || !head.next) {
return null;
}
let slow = head;
let fast = head.next;
while (slow !== fast) {
if (!fast || !fast.next) {
return null;
}
slow = slow.next;
fast = fast.next.next;
}
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
这个 findCycleEntrance
函数也接收链表的头节点 head
作为参数。它首先使用快慢指针找到第一次相遇的节点。然后,将慢指针重置到头节点,并让快慢指针以相同的速度前进。当它们再次相遇时,返回相遇的节点,它就是环的入口节点。
环形链表在计算机科学中是一个常见的问题,它在很多领域都有应用,例如操作系统、数据库、网络协议等。掌握环形链表的检测和处理方法对于程序员来说是十分重要的。
常见问题及其解答
问题 1:为什么环形链表会导致程序出现问题?
解答:环形链表会导致程序进入无限循环。例如,如果我们尝试遍历一个环形链表,那么程序会一直循环遍历环中的节点,永远无法到达链表的末尾。
问题 2:哈希表方法和快慢指针方法哪种更好?
解答:两种方法各有优缺点。哈希表方法需要额外的空间来存储访问过的节点,而快慢指针方法不需要额外的空间。但是,快慢指针方法的代码实现稍微复杂一些。
问题 3:如何避免出现环形链表?
解答:在编写代码时,要仔细检查指针的操作,确保链表的尾节点指针指向 null,而不是指向链表中的其他节点。
问题 4:环形链表有哪些应用场景?
解答:环形链表可以用来实现循环队列、约瑟夫环问题等。
问题 5:如何删除环形链表中的环?
解答:找到环的入口节点后,将环的入口节点的前一个节点的指针指向 null,即可删除环。