
1. 双向链表基础概念
双向链表是一种数据结构,其中的每个节点不仅包含数据,还包含指向下一个节点(next)和指向上一个节点(previous)的引用。这种结构允许我们从两个方向遍历链表,使得某些操作(如在给定节点前插入或删除节点)比单向链表更高效。
在Java中实现双向链表,通常需要定义两个核心类:
- Node类:表示链表中的一个节点,包含数据、next和previous引用。
- DoublyLinkedList类:表示整个链表,包含head(头节点)、tail(尾节点)和size(链表大小)等属性,以及各种操作方法。
为了提高代码的复用性和类型安全性,我们通常会使用泛型来定义这些类。
1.1 节点(Node)类的设计
Node类应是泛型的,以便存储任何类型的数据。它将包含一个数据字段以及指向前一个和后一个节点的引用。
立即学习“Java免费学习笔记(深入)”;
class Node{ T data; // 节点存储的数据 Node next; // 指向下一个节点的引用 Node previous; // 指向上一个节点的引用 public Node(T data) { this.data = data; this.next = null; this.previous = null; } @Override public String toString() { return data.toString(); } }
1.2 双向链表(DoublyLinkedList)类的设计
DoublyLinkedList类同样应是泛型的,并且其泛型类型通常会要求实现Comparable接口,以便进行排序或其他比较操作(如果需要)。它将管理链表的head、tail和size。
public class DoublyLinkedList> { protected Node head; // 链表头节点 protected Node tail; // 链表尾节点 int size = 0; // 链表当前大小 public DoublyLinkedList() { this.head = null; this.tail = null; } /** * 向链表末尾添加一个新节点。 * @param data 要添加的数据 * @return 新创建的节点 */ public Node append(T data) { Node newNode = new Node<>(data); if (head == null) { // 链表为空,新节点既是头也是尾 head = newNode; tail = newNode; } else { // 链表不为空,新节点添加到尾部 newNode.previous = tail; tail.next = newNode; tail = newNode; // 更新尾节点 } size++; return newNode; } /** * 辅助方法:将链表内容转换为字符串,便于调试。 */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("Size[%d]: ", size)); Node current = head; while (current != null) { sb.append(current.data); if (current.next != null) { sb.append(" <-> "); } current = current.next; } return sb.toString(); } // 删除方法将在下一节详细实现 // public void delete(int location) { ... } }
2. 实现指定索引节点删除(delete)方法
删除双向链表中的节点比单向链表稍微复杂,因为需要维护previous和next两个方向的引用。我们需要考虑多种边界情况:链表为空、删除头节点、删除尾节点以及删除中间节点。
2.1 方法签名与初步验证
delete方法接收一个整数location作为参数,表示要删除节点的索引(0-based)。在执行任何删除操作之前,必须对location进行严格的验证。
public void delete(int location) throws IllegalArgumentException {
// 1. 验证链表状态和索引有效性
if (head == null || location < 0 || location >= size) {
throw new IllegalArgumentException("Invalid deletion location or empty list.");
}
// 根据删除位置分为三种情况处理
if (location == 0) {
// 情况一:删除头节点
deleteHead();
} else if (location == size - 1) {
// 情况二:删除尾节点
deleteTail();
} else {
// 情况三:删除中间节点
deleteIntermediate(location);
}
size--; // 成功删除后,链表大小减一
}为了使delete方法更清晰,我们可以将其拆分为几个私有辅助方法来处理不同的删除情况。
2.2 情况一:删除头节点(location == 0)
当删除头节点时,head引用需要指向原头节点的下一个节点。同时,新头节点的previous引用必须设置为null。特别地,如果链表中只有一个节点,删除后链表将变为空,此时head和tail都应设置为null。
private void deleteHead() {
head = head.next; // 头节点指向下一个节点
if (head != null) {
head.previous = null; // 新头节点的previous为null
} else {
// 如果head变为null,说明原链表只有一个节点,删除后链表为空
tail = null; // 尾节点也必须设为null
}
}2.3 情况二:删除尾节点(location == size - 1)
当删除尾节点时,tail引用需要指向原尾节点的上一个节点。同时,新尾节点的next引用必须设置为null。
private void deleteTail() {
tail = tail.previous; // 尾节点指向上一个节点
if (tail != null) {
tail.next = null; // 新尾节点的next为null
} else {
// 如果tail变为null,说明原链表只有一个节点,删除后链表为空
// 此分支在delete(int location)的逻辑下不会被触发,因为location == size - 1 且 size > 1
// 但作为独立的辅助方法,考虑此情况是好的。
head = null; // 头节点也必须设为null
}
}2.4 情况三:删除中间节点(0
删除中间节点需要先遍历到待删除节点的前一个节点。然后,调整前一个节点的next引用和后一个节点的previous引用,使其跳过待删除节点。
private void deleteIntermediate(int location) {
Node current = head;
// 遍历到待删除节点的前一个节点
for (int i = 0; i < location - 1; i++) {
current = current.next;
}
// current 现在是待删除节点的前一个节点
Node nodeToDelete = current.next; // 待删除节点
Node nodeAfter = nodeToDelete.next; // 待删除节点后的节点
current.next = nodeAfter; // 前一个节点的next指向待删除节点后的节点
if (nodeAfter != null) {
nodeAfter.previous = current; // 待删除节点后的节点的previous指向前一个节点
}
// 注意:如果nodeAfter为null,这意味着我们删除了原链表的倒数第二个节点,
// 并且current成为了新的尾节点。然而,在这种情况下,deleteIntermediate
// 不会直接更新tail。由于deleteIntermediate只处理中间节点,
// 而删除倒数第二个节点严格来说不属于“中间节点”范畴,
// 而是特殊情况,应该由deleteTail处理。
// 但是,如果location == size - 2 (倒数第二个节点),则它仍会进入此分支。
// 在此情况下,current是新的tail,但tail变量没有更新。
// 修正:如果nodeAfter为null,且此节点是倒数第二个节点,那么current就是新的tail。
if (nodeAfter == null) {
tail = current; // 更新tail为新的尾节点
}
} 将上述辅助方法整合到DoublyLinkedList类中,并修正`deleteIntermediate










