链表(Linked List)是计算机科学中最基础也最灵活的数据结构之一。与数组不同,链表通过指针动态连接节点,无需预先分配连续内存,在插入和删除操作上具有 O(1) 的时间优势。本文将从零实现一个完整的单链表,重点讲解反转链表的经典迭代解法,以及 Floyd 判圈算法——一个用于检测链表中环的数学 elegant 算法。
数据结构定义
链表由一系列节点组成,每个节点包含数据域和指针域。单链表的指针仅指向下一个节点,形成线性结构。
Java 实现
import java.util.HashSet;
import java.util.Set;
/**
* 单链表完整实现,包含基础操作、反转与环检测
*/
public class LinkedList {
/**
* 链表节点定义
*/
static class Node {
int val;
Node next;
Node(int val) {
this.val = val;
}
}
private Node head; // 链表头节点
private int size; // 链表长度
/**
* 在链表尾部添加元素
*
* @param val 待添加的值
*/
public void add(int val) {
Node newNode = new Node(val);
if (head == null) {
head = newNode;
} else {
Node cur = head;
while (cur.next != null) {
cur = cur.next;
}
cur.next = newNode;
}
size++;
}
/**
* 迭代反转链表:三指针法
* prev -> cur -> next 逐步反转指针方向
*
* 时间复杂度 O(n),空间复杂度 O(1)
*/
public void reverse() {
Node prev = null;
Node cur = head;
while (cur != null) {
Node next = cur.next; // 暂存下一个节点
cur.next = prev; // 反转当前指针
prev = cur; // prev 前进一步
cur = next; // cur 前进一步
}
head = prev; // 新的头节点
}
/**
* 递归反转链表
*
* @param node 当前处理的节点
* @return 反转后的新头节点
*/
public Node reverseRecursive(Node node) {
if (node == null || node.next == null) {
return node; // 递归终止条件
}
Node newHead = reverseRecursive(node.next);
node.next.next = node; // 将下一个节点指向当前节点
node.next = null; // 断开原指针,防止环
return newHead;
}
/**
* Floyd 判圈算法(龟兔赛跑算法)
* 使用快慢指针检测链表中是否存在环
*
* 原理:若存在环,快指针终将追上慢指针;若无环,快指针先到达末尾
*
* @return true 若存在环
*/
public boolean hasCycle() {
if (head == null || head.next == null) {
return false;
}
Node slow = head; // 慢指针,每次走一步
Node fast = head; // 快指针,每次走两步
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true; // 两指针相遇,存在环
}
}
return false; // 快指针到达末尾,无环
}
/**
* 若存在环,返回环的入口节点
* 数学证明:设头到入口距离为 a,环长为 b,相遇时慢指针走了 a + mb + x,
* 快指针走了 a + nb + x。由于快指针速度是慢指针两倍,可得从头和相遇点同步走
* 必在入口相遇
*
* @return 环入口节点,若无环返回 null
*/
public Node detectCycleEntry() {
if (head == null || head.next == null) {
return null;
}
Node slow = head;
Node fast = head;
// 第一阶段:寻找相遇点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
break; // 找到相遇点
}
}
// 无环情况
if (fast == null || fast.next == null) {
return null;
}
// 第二阶段:寻找入口
Node ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr; // 入口节点
}
/**
* 哈希表法检测环(辅助理解,非最优)
* 时间 O(n),空间 O(n)
*/
public boolean hasCycleByHash() {
Set<Node> visited = new HashSet<>();
Node cur = head;
while (cur != null) {
if (visited.contains(cur)) {
return true;
}
visited.add(cur);
cur = cur.next;
}
return false;
}
/**
* 打印链表内容
*/
public void printList() {
Node cur = head;
while (cur != null) {
System.out.print(cur.val + " -> ");
cur = cur.next;
}
System.out.println("null");
}
public Node getHead() {
return head;
}
public void setHead(Node head) {
this.head = head;
}
public static void main(String[] args) {
LinkedList list = new LinkedList();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
System.out.print("原始链表: ");
list.printList();
// 反转测试
list.reverse();
System.out.print("反转后: ");
list.printList();
// 环检测测试:构造一个有环链表 1 -> 2 -> 3 -> 4 -> 2
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
Node n4 = new Node(4);
n1.next = n2;
n2.next = n3;
n3.next = n4;
n4.next = n2; // 形成环
LinkedList cycleList = new LinkedList();
cycleList.setHead(n1);
System.out.println("有环链表检测结果: " + cycleList.hasCycle());
Node entry = cycleList.detectCycleEntry();
System.out.println("环入口节点值: " + (entry != null ? entry.val : "无环"));
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 尾部插入 | O(n) | O(1) |
| 迭代反转 | O(n) | O(1) |
| 递归反转 | O(n) | O(n)(递归栈) |
| Floyd 判圈 | O(n) | O(1) |
| 哈希判圈 | O(n) | O(n) |
Floyd 判圈算法的精妙之处在于它仅使用两个指针就解决了环检测问题,空间复杂度为 O(1),远优于哈希表法的 O(n)。
总结
链表是许多高级数据结构和算法的基础。掌握链表的反转技巧(三指针法)和环检测(Floyd 算法),不仅能应对面试高频题,更能深入理解指针操作和数学推理在算法设计中的力量。