一、环形链表Ⅱ(LeetCode 142)
1. 问题描述
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。如果链表无环,则返回 null 。通过跟踪 next 指针能再次到达某个节点时,链表存在环,评测系统用整数 pos 表示链尾连接到链表中的位置(索引从 0 开始 ), pos 不作为参数传递,仅用于标识实际情况,且不允许修改链表。
2. 示例
- 输入: head = [3,2,0,-4] , pos = 1 ;输出:返回索引为 1 的链表节点,链表中有环,尾部连接到索引为 1 的节点。
- 输入: head = [1,2] , pos = 0 ;输出:返回索引为 0 的链表节点,链表中有环,尾部连接到索引为 0 的节点 。
- 输入: head = [1] , pos = -1 ;输出:返回 null ,链表中无环。
3. 解题思路 - 快慢指针法
定义两个指针,慢指针 slow 每次走一步,快指针 fast 每次走两步。
- 判断链表是否有环:若链表无环, fast 会先到达链表末尾;若有环, fast 会在环内追上 slow ,二者相遇。
- 找到环的入口:相遇后,设 cur1 从相遇位置出发, cur2 从链表头部出发,二者每次都走一步,再次相遇的位置就是环的入口。这是因为根据数学推导(设链表进入环前长度为 L ,环长为 R ,相遇点距离环入口距离为 X ,推导得出 L = X )。
4. 代码实现
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
// 寻找快慢指针相遇点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
// 相遇后,重新设置指针找环入口
ListNode cur1 = fast;
ListNode cur2 = head;
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
}
return null; // 无环情况
}
二、判断链表是否有环
1. 简单做法(空间复杂度 O(N))
遍历链表,用一个 List 存储每个遍历过的节点地址(引用) ,每次遍历新节点时,检查该节点是否已在 List 中存在,若存在则说明链表有环。
public boolean hasCycle(ListNode head) {
if (head == null) {
return false;
}
List<ListNode> list = new ArrayList<>();
for (ListNode cur = head; cur != null; cur = cur.next) {
if (list.contains(cur)) {
return true;
} else {
list.add(cur);
}
}
return false;
}
1. 快慢指针法(空间复杂度 O(1))
创建两个指针都指向链表头部, fast 每次走两步, slow 每次走一步。若链表无环,两指针不会重合;若有环, fast 会追上 slow ,二者会重合。
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
三、链表常见笔试题思路
1. 删除链表中等于给定值 val 的所有节点
先处理后续节点,最后处理头结点。遍历链表,跳过值为 val 的节点,调整指针连接。
2. 反转一个单链表
使用三个引用 prev (前驱节点 )、 cur (当前节点 )、 next (后继节点 ) ,遍历链表,执行 cur.next = prev 操作,当 next 为 null 时, cur 为反转后的头结点。
3. 求带头发结点 head 的非空单链表的中间结点
求链表长度后除以 2 得到中间节点的“步数” ,从头结点出发走相应步数。若有两个中间结点,返回第二个。
4. 输入一个链表,输出该链表中倒数第 k 个结点
先求链表长度,通过 size - k 得到“步数” ,从头结点出发走相应步数。
5. 合并两个有序链表
创建两个引用分别指向两个链表头结点,比较值大小,将小的节点插入新链表末尾。创建新头结点 newHead 和新尾结点 newTail (可借助傀儡节点 ),遍历过程中,一个链表遍历完后,将另一个链表剩余部分拼接到结果末尾。
6. 以给定值 x 为基准将链表分割成两部分,小于 x 的结点排在大于或等于 x 的结点之前
遍历链表,比较每个值和 x 的关系,准备两个链表分别保存小于 x 和大于等于 x 的元素,最后连接两个链表。
四、判断链表是否为回文结构
1. 空间复杂度 O(N) 的做法
复制一份链表,将复制链表反转,然后比较原链表和反转后链表是否相同。
2. 空间复杂度 O(1) 的做法
找到链表中间节点,将后半部分链表反转,比较前半部分和反转后的后半部分是否相同,忽略长度比较,只要其中一个子链表结束,比较就结束。
五、判断两个链表是否相交
1. 解题思路
先分别求出两个链表的长度,创建两个引用指向头结点,让较长链表的引用先走,走的步数为两链表长度差值,然后两个引用同时走,每次走一步,若相遇则链表相交。
2. 代码实现
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
int lenA = 0, lenB = 0;
ListNode curA = headA, curB = headB;
// 计算链表 A 的长度
while (curA != null) {
lenA++;
curA = curA.next;
}
// 计算链表 B 的长度
while (curB != null) {
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
// 让长链表指针先走
if (lenA > lenB) {
for (int i = 0; i < lenA - lenB; i++) {
curA = curA.next;
}
} else {
for (int i = 0; i < lenB - lenA; i++) {
curB = curB.next;
}
}
// 同时遍历找相交点
while (curA != null && curB != null) {
if (curA == curB) {
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}