算法概述
环形链表(Circular Linked List)是一种首尾相连的链式数据结构,最后一个节点的next指针指向头节点而非null。这种结构天然适合处理循环队列、轮转调度、圆桌问题等场景。本文用Java实现单向环形链表与双向循环链表,涵盖插入、删除、遍历、约瑟夫问题求解等核心操作,并对比两种结构的时空效率差异。
核心原理
单向环形链表中,尾节点的next指向头节点,形成单向循环。双向循环链表中,头节点的prev指向尾节点,尾节点的next指向头节点,支持双向遍历。核心挑战在于维护循环结构的一致性,尤其是在边界操作(插入到空表、删除唯一节点)时避免断链。
完整Java实现
/**
* 单向环形链表节点
*/
class CircularNode {
int data;
CircularNode next;
CircularNode(int data) { this.data = data; }
}
/**
* 双向循环链表节点
*/
class DlCircularNode {
int data;
DlCircularNode prev, next;
DlCircularNode(int data) { this.data = data; }
}
/**
* 单向环形链表实现
*/
class SinglyCircularList {
CircularNode tail; // 指向尾节点,可O(1)访问首尾
int size;
public void append(int data) {
CircularNode node = new CircularNode(data);
if (tail == null) {
node.next = node; // 自环
tail = node;
} else {
node.next = tail.next; // 新节点指向头
tail.next = node; // 原尾指向新节点
tail = node;
}
size++;
}
public void prepend(int data) {
CircularNode node = new CircularNode(data);
if (tail == null) {
node.next = node;
tail = node;
} else {
node.next = tail.next;
tail.next = node;
}
size++;
}
public boolean remove(int data) {
if (tail == null) return false;
CircularNode curr = tail.next, prev = tail;
do {
if (curr.data == data) {
if (curr == tail && curr.next == tail) {
tail = null; // 唯一节点
} else {
prev.next = curr.next;
if (curr == tail) tail = prev;
}
size--;
return true;
}
prev = curr;
curr = curr.next;
} while (curr != tail.next);
return false;
}
public void print() {
if (tail == null) return;
CircularNode curr = tail.next;
do {
System.out.print(curr.data + " -> ");
curr = curr.next;
} while (curr != tail.next);
System.out.println("(head)");
}
/**
* 约瑟夫问题求解:每数到k的人出列,求最后幸存者
* 使用环形链表模拟,时间O(n*k),空间O(n)
*/
public int josephus(int k) {
if (tail == null || k <= 0) return -1;
CircularNode curr = tail.next;
while (size > 1) {
// 前进k-1步(curr本身算第1个)
for (int i = 1; i < k - 1; i++) {
curr = curr.next;
}
// 删除curr.next
CircularNode toRemove = curr.next;
curr.next = toRemove.next;
if (toRemove == tail) tail = curr;
size--;
curr = curr.next;
}
return curr.data;
}
}
/**
* 双向循环链表实现
*/
class DoublyCircularList {
DlCircularNode head; // 哨兵节点
int size;
public DoublyCircularList() {
head = new DlCircularNode(-1);
head.prev = head.next = head;
}
public void append(int data) {
DlCircularNode node = new DlCircularNode(data);
node.prev = head.prev;
node.next = head;
head.prev.next = node;
head.prev = node;
size++;
}
public void insertAfter(DlCircularNode target, int data) {
DlCircularNode node = new DlCircularNode(data);
node.next = target.next;
node.prev = target;
target.next.prev = node;
target.next = node;
size++;
}
public void remove(DlCircularNode node) {
if (size == 0 || node == head) return;
node.prev.next = node.next;
node.next.prev = node.prev;
size--;
}
public void printForward() {
DlCircularNode curr = head.next;
while (curr != head) {
System.out.print(curr.data + " <-> ");
curr = curr.next;
}
System.out.println("(head)");
}
public void printBackward() {
DlCircularNode curr = head.prev;
while (curr != head) {
System.out.print(curr.data + " <-> ");
curr = curr.prev;
}
System.out.println("(head)");
}
}
public class CircularLinkedList {
public static void main(String[] args) {
// 单向环形链表测试
SinglyCircularList scl = new SinglyCircularList();
for (int i = 1; i <= 7; i++) scl.append(i);
System.out.print("单向环形链表: ");
scl.print();
System.out.println("约瑟夫问题(k=3): 幸存者 = " + scl.josephus(3));
// 双向循环链表测试
DoublyCircularList dcl = new DoublyCircularList();
for (int i = 1; i <= 5; i++) dcl.append(i);
System.out.print("双向循环链表(正向): ");
dcl.printForward();
System.out.print("双向循环链表(反向): ");
dcl.printBackward();
}
}
复杂度分析
| 操作 | 单向环形链表 | 双向循环链表 |
|---|---|---|
| 头部插入 | O(1) | O(1) |
| 尾部插入 | O(1) | O(1) |
| 指定节点后插入 | O(1)* | O(1) |
| 删除指定值 | O(n) | O(n) |
| 删除已知节点 | O(n)* | O(1) |
| 正向遍历 | O(n) | O(n) |
| 反向遍历 | 不支持 | O(n) |
*单向环形链表删除已知节点需先找到其前驱
应用场景
- 操作系统进程调度:轮转调度(Round-Robin)使用时间片轮转的循环队列
- 音乐播放器循环播放:播放列表的上一首/下一首在循环链表中天然支持
- 约瑟夫问题:经典的数学淘汰问题,环形链表是最直观的模拟结构
- 多人游戏轮流机制:环形链表维护玩家顺序,支持顺时针/逆时针轮转
- 缓冲区循环复用:如内核中的环形缓冲区(ring buffer)管理