返回

扑朔迷离的环形链表:JS算法题解(141. 环形链表和142. 环形链表 II)

前端

在计算机科学领域,链表作为一种基础的数据结构,其重要性不言而喻。它由一系列节点组成,每个节点都存储着数据以及指向下一个节点的指针。但是,在某些特定情况下,链表的尾节点指针可能会指向链表中的某个节点,而不是指向 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;
    }
  }
}

这段代码定义了两个类:NodeLinkedListNode 类表示链表中的一个节点,它包含数据 data 和指向下一个节点的指针 nextLinkedList 类表示链表本身,它包含头节点 head 和尾节点 tailadd 方法用于向链表中添加新的节点。

接下来,我们来探讨如何检测一个链表是否为环形链表。一种直观的方法是利用哈希表来记录已经遍历过的节点。在遍历链表的过程中,如果遇到一个节点已经在哈希表中存在,那就说明链表中存在环,因为我们又回到了之前访问过的节点。

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,即可删除环。