1. 链表数据结构基础解析
链表作为计算机科学中最基础的数据结构之一,其核心思想是通过节点间的引用关系实现动态数据存储。与数组不同,链表不需要连续的内存空间,每个节点独立存储,通过指针(引用)连接形成逻辑上的线性序列。
1.1 节点结构设计原理
在Java中实现链表时,我们通常定义两个类:外层容器类和内部节点类。这种设计遵循了封装原则,将节点实现细节隐藏在外层类中。内部类ListNode包含两个关键字段:
val:存储当前节点的数据(示例中使用int类型,实际可替换为泛型)next:引用下一个ListNode对象,形成链式结构
重要设计原则:head指针必须作为外层类的成员变量,而非节点类的属性。若将head放入每个节点,会导致逻辑混乱和内存浪费——相当于每个车厢都自带一个火车头标识。
1.2 链表与数组的对比分析
| 特性 | 链表 | 数组 |
|---|---|---|
| 内存分配 | 动态非连续 | 静态连续 |
| 访问复杂度 | O(n) | O(1) |
| 插入/删除复杂度 | O(1)(已知位置) | O(n) |
| 空间开销 | 每个节点额外存储指针 | 无额外开销 |
| 缓存友好性 | 差 | 好 |
链表特别适合频繁插入/删除的场景,而数组更适合随机访问。理解这一差异对实际工程选型至关重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 链表基本操作实现
2.1 遍历与信息获取
打印链表(display):
java复制public void display() {
ListNode cur = head; // 从头节点开始
while (cur != null) { // 终止条件:当前节点为null
System.out.print(cur.val + " ");
cur = cur.next; // 关键:移动到下一节点
}
System.out.println(); // 打印换行保持输出整洁
}
带起始节点的打印(display2):
java复制public void display2(ListNode Nhead) {
// 实际工程中应增加参数校验
if (Nhead == null) {
System.out.println("Empty list");
return;
}
ListNode cur = Nhead;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
}
计算长度(size):
java复制public int size() {
int count = 0;
ListNode cur = head;
while (cur != null) {
count++;
cur = cur.next; // 遍历指针移动
}
return count;
}
操作注意事项:
- 所有遍历操作必须检查终止条件,避免NPE
- 临时指针cur应保持局部性,不要污染类成员变量
- 打印方法应考虑输出格式,增加适当的换行和分隔符
2.2 节点插入操作
头插法(addFirst):
java复制public void addFirst(int data) {
ListNode node = new ListNode(data);
node.next = head; // 新节点指向原头节点
head = node; // 更新head引用
// 时间复杂度:O(1)
}
尾插法(addLast):
java复制public void addLast(int data) {
ListNode node = new ListNode(data);
if (head == null) { // 空链表特殊处理
head = node;
return;
}
ListNode cur = head;
while (cur.next != null) { // 找到最后一个节点
cur = cur.next;
}
cur.next = node; // 尾节点指向新节点
// 时间复杂度:O(n)
}
指定位置插入(addIndex):
java复制public void addIndex(int index, int data) {
// 参数校验
if (index < 0 || index > size()) {
throw new IndexOutOfBoundsException("Invalid index: " + index);
}
if (index == 0) {
addFirst(data); // 复用头插
return;
}
ListNode node = new ListNode(data);
ListNode prev = head;
// 定位到index前驱节点
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
node.next = prev.next; // 新节点指向原位置节点
prev.next = node; // 前驱节点指向新节点
}
工程实践建议:
- 插入操作必须考虑边界条件(空表、头尾位置)
- 参数校验应抛出明确异常而非静默返回
- 可复用已有方法减少重复代码(如addIndex复用addFirst)
3. 链表高级操作实现
3.1 查询与删除操作
元素存在性检查(contains):
java复制public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true; // 找到立即返回
}
cur = cur.next;
}
return false; // 遍历结束未找到
}
删除首个匹配节点(remove):
java复制public void remove(int key) {
if (head == null) return;
// 处理头节点特殊情况
if (head.val == key) {
head = head.next;
return;
}
ListNode prev = head;
while (prev.next != null) {
if (prev.next.val == key) {
prev.next = prev.next.next; // 跳过待删除节点
return;
}
prev = prev.next;
}
}
删除所有匹配节点(removeAllKey):
java复制public void removeAllKey(int key) {
// 处理非头节点的匹配项
ListNode dummy = new ListNode(-1); // 虚拟头节点技巧
dummy.next = head;
ListNode prev = dummy, curr = head;
while (curr != null) {
if (curr.val == key) {
prev.next = curr.next; // 删除当前节点
} else {
prev = curr; // 只有不匹配时才移动prev
}
curr = curr.next; // 总是移动curr
}
head = dummy.next; // 更新真实头节点
}
关键技巧:
- 虚拟头节点(dummy node)可统一处理头节点特殊情况
- 双指针法(prev和curr)是链表删除的经典模式
- 删除操作必须注意更新引用关系,避免内存泄漏
3.2 链表清空与资源释放
清空链表(clear):
java复制public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode next = cur.next; // 提前保存下一节点引用
cur.next = null; // 断开当前节点连接
cur = next; // 移动到下一节点
}
head = null; // 最后清空head
}
内存管理要点:
- 必
