一、双链表(Double Linked List)
1. 结点与链表类定义(设计思路)
(1)DLinkNode 结点类设计
- 双向指针:
- prior 指针指向前驱,支持反向遍历
- next 指针指向后继,支持正向遍历
- 应用场景:需要频繁前后移动的场景(如文本编辑器的光标移动)
(2)DLinkListClass 链表类设计
- 头结点 dhead:
- prior 初始为 null(链表头部标识)
- next 初始为 null(链表尾部标识)
- 双向操作基础:每个结点可通过 prior 和 next 双向访问
// 双链表结点泛型类(逻辑:双向指针实现双向遍历)
class DLinkNode<E> {
E data;
DLinkNode<E> prior; // 前驱指针
DLinkNode<E> next; // 后继指针
public DLinkNode() {
prior = null;
next = null;
}
public DLinkNode(E d) {
data = d;
prior = null;
next = null;
}
}
// 双链表泛型类(逻辑:头结点管理双向链表)
public class DLinkListClass<E> {
DLinkNode<E> dhead; // 头结点
public DLinkListClass() {
dhead = new DLinkNode<E>();
dhead.prior = null;
dhead.next = null;
}
// 基本运算算法...
}
2. 核心操作实现(逻辑解析)
(1)插入操作(在 p 结点后插入 s 结点)
- 四步指针修改:
- s.next = p.next (新结点后继指向原后继)
- if (p.next!=null) p.next.prior = s (原后继前驱指向新结点)
- p.next = s (原结点后继指向新结点)
- s.prior = p (新结点前驱指向原结点)
- 顺序原则:先修改新结点的后继和原后继的前驱,再修改原结点的后继和新结点的前驱
s.next = p.next; // 新结点后继指向原后继
if (p.next != null) {
p.next.prior = s; // 原后继前驱指向新结点(非空时)
}
p.next = s; // 原结点后继指向新结点
s.prior = p; // 新结点前驱指向原结点
(2)删除操作(删除 p 结点)
- 两步指针修改:
- if (p.next!=null) p.next.prior = p.prior (后继前驱指向原前驱)
- p.prior.next = p.next (前驱后继指向原后继)
- 内存处理:Java 自动回收被删除结点
if (p.next != null) {
p.next.prior = p.prior; // 后继前驱指向原前驱
}
p.prior.next = p.next; // 前驱后继指向原后继
二、循环链表(Circular Linked List)
1. 循环单链表(约瑟夫问题解决方案)
(1)问题建模思路
- 数据结构选择:
- 循环单链表天然适合环形结构
- 不带头结点设计,简化首尾相连逻辑
- 报数逻辑:
- 从 first 开始遍历,计数到 m-1 时找到出列结点前驱
- 出列后将 first 指向出列结点的后继,继续报数
(2)代码实现(逻辑步骤)
构建 n 个结点的循环单链表
循环 n 次执行出列操作:
- 找到第 m-1 个结点 p
- 出列结点 q=p.next
- 删除 q 并更新 first 指针
class Child {
int no;
Child next;
public Child(int no1) {
no = no1;
next = null;
}
}
class Joseph {
int n, m;
Child first; // 指向开始报数的结点
// 构造方法:构建循环单链表
public Joseph(int n1, int m1) {
Child p, t;
n = n1; m = m1;
first = new Child(1); // 首结点编号1
t = first;
for (int i = 2; i <= n; i++) {
p = new Child(i);
t.next = p; t = p; // 尾插法建表
}
t.next = first; // 尾结点指向首结点,形成环
}
// 求解约瑟夫序列
public String Jsequence() {
String ans = "";
int i, j;
Child p, q;
for (i = 1; i <= n; i++) { // 出列n次
p = first;
j = 1;
while (j < m - 1) { // 找到第m-1个结点
j++;
p = p.next;
}
q = p.next; // 第m个结点(出列结点)
ans += q.no + " ";
p.next = q.next; // 删除q结点
first = p.next; // 从下一个结点开始报数
}
return ans;
}
}
三、顺序表与链表的比较(应用决策逻辑)
1. 空间效率决策树
是否已知数据量大小?
├─ 是 → 数据量小:顺序表(存储密度1)
└─ 否 → 动态变化:链表(动态分配)
2. 时间效率决策树
主要操作类型?
├─ 查找为主 → 顺序表(O(1)随机访问)
└─ 插入删除为主 → 链表(O(1)指针修改)
3. 综合对比表
场景特征 |
顺序表更适合 |
链表更适合 |
数据量固定 |
√(预先分配空间无浪费) |
×(动态分配反而效率低) |
频繁随机访问 |
√(下标直接访问) |
×(需从头遍历) |
频繁插入删除 |
×(大量元素移动) |
√(仅修改指针) |
内存碎片化严重 |
×(需要连续空间) |
√(离散空间可用) |
数据量动态增长 |
×(扩容代价高) |
√(逐个结点分配) |
要点
- 指针操作三原则:
- 先保存后继指针再修改(防止断链)
- 插入时 "先连后断",删除时 "先断后连"
- 头插法逆序,尾插法顺序