环形链表

67 阅读5分钟

1.问题描述

给定一个链表link,头指针head指向链表的第一个元素。判断链表是否存在环,若存在环,返回开始入环的第一个节点,否则返回null。

2.问题分析

2.1 哈希表

使用哈希表的解题思路比较简单直接,具体过程为:

  1. 获取链表的头节点node;
  2. 如果该节点为空,则链表不存在环形,返回
  3. 如果该节点node尚未加入到哈希表中,则将该节点插入到哈希表中
  4. 如果该节点node已存在于哈希表中,则链表存在环形,并且该节点即为入环的第一个节点,返回;
  5. 取该节点的下一跳重复步骤2-4进行判断

节点数据结构

public class Node {
    // 节点数据
    public int value;
    // 下一个节点
    public Node next;

    public Node(int value) {
        this.value = value;
    }
}

哈希法

import java.util.HashSet;
import java.util.Set;

/**
 * 用哈希表返回循环链表的第一个入环节点,否则返回NULL
 */
public class LinkCycle_Hash {

    /**
     * 探测首个入环节点
     */
    public Node detect(Node head){
        Set<Node> nodes = new HashSet<>();
        var node = head;
        while (node!=null){
            if (nodes.contains(node)){
                return node;
            }
            nodes.add(node);
            node = node.next;
        }
        return null;
    }
}

2.2 快慢指针

使用快慢指针来解决环形链表问题。关于快慢指针,概述为两个移动步长不一致的指针。这里设定fast=node.next.next,slow=node.next,即快指针步长为2,慢指针步长为1. 快慢指针在解决环形链表中用到的几个性质:(后文补充性质的证明)

  • 性质1:如果链表不存在环形,则fast指针率先到达终点,为null,否则快慢指针在环内经过N次移动后会相遇
  • 性质2:如果链表存在环形,慢指针进入环形后,最多经过一个循环就会和快指针相遇
  • 性质3:如果链表存在环形,快慢指针在环内首次相遇后,快指针停止移动,慢指针继续移动,同时引入索引指针从头节点开始和慢指针同步移动,两者相遇点即为入环的首个节点。

根据以上特性,使用快慢指针解决环形链路问题需要的步骤为:

  1. 获取链表的头节点node,并设定fast与slow;
  2. 如果快指针的下一跳存在空指针(跑得快),则链表不存在环形,返回;
  3. 设定快指针fast=fast.next.next,慢指针slow=slow.next;
  4. 如果快慢指针指向同一个节点,则链表存在环形;
  5. 如果快慢指针不指向同一个节点,则移动快慢指针到下一个节点,重复步骤2-4进行判断;
  6. 设定索引指针index=head;
  7. 索引指针和慢指针同步移动,当两者相遇时,返回索引指针。
/**
 * 用快慢返回循环链表的第一个入环节点,否则返回NULL
 */
public class LinkCycle_FsPoint {
    /**
     * 探测首个入环节点
     */
    public Node detect(Node head){
        if (head == null || head.next == null){
            return null;
        }
        // 是否存在环形
        Node fast = head, slow = head;
        boolean hasCycle = false;
        while(fast.next!=null && fast.next.next !=null){
            slow = slow.next;
            fast = fast.next.next;
            if (fast == slow){
                hasCycle = true;
                break;
            }
        }
        if (!hasCycle){
            return null;
        }
        // 索引指针
        Node index = head;
        while (slow!=index){
            slow = slow.next;
            index = index.next;
        }
        return index;
    }
}

2.3 快慢指针性质2

如果链表存在环形,慢指针进入环形后,最多经过一个循环就会和快指针相遇 假定:

  1. 环形结构的总长度len
  2. 慢指针进入环形后,快指针的位置落后慢指针节点数x(0<x<=len,0转化为落后一圈,长度len)
  3. 快慢指针相遇用的移动次数m

分析快慢指针当前的相对位置:

  1. x==1: 快指针落后慢指针1个节点,快慢指针同步移动1次,快慢指针相遇,m =1;
  2. x==2: 快指针落后慢指针2个节点,快慢指针同步移动1次, 则转换为情况1, m=1 + 1;
  3. x==3: 快指针落后慢指针3个节点,快慢指针同步移动2次,则转换为情况1, m=2 + 1;
  4. x==4: 快指针落后慢指针4个节点,快慢指针同步移动3次,则转换为情况1, m=3 + 1;
  5. 以此类推
  6. 快指针落后慢指针x个节点,快慢指针同步移动x-1次,则转换为情况1, m=x-1 + 1 =x <=len;
  7. 因此慢指针在进入环形后,最多经过一个循环就会和快指针相遇。

2.4 快慢指针性质3

如果链表存在环形,快慢指针在环内首次相遇后,快指针停止移动,慢指针继续移动,同时引入索引指针从头节点开始和慢指针同步移动,两者相遇点即为入环的首个节点。 假定:

  1. 环形结构外的总节点数为 x
  2. 环形结构内的总节点数为 y
  3. 快慢指针在环内第m个节点相遇
  4. 快慢指针相遇时,快指针在环内已循环移动了c圈

分析快慢指针移动的总距离

  1. 快指针移动的距离为 x + m + cy
  2. 慢指针移动的距离为 x + m
  3. 快指针移动速度是慢指针移动速度的2倍,则 2(x + m) = x + m + cy
  4. 简化以上的公式 x + m = cy
  5. 以上公式可以得到结论为: 头指针经过环外到达环内快慢指针相遇点的距离刚好是环内距离的整数倍
  6. 慢指针再移动x个节点数刚好到达入环的首节点,因此我们引入索引指针来度量x。因为索引指针以步长1从头指针移动x个节点,也刚好到达入环的首节点,这样两个指针相遇点即是入环的首节点。