你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
示例:
scss
输入
["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
[[], [1], [3], [1, 2], [1], [1], [1]]
输出
[null, null, null, null, 2, null, 3]
解释
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addAtHead(1);
myLinkedList.addAtTail(3);
myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
myLinkedList.get(1); // 返回 3
提示:
0 <= index, val <= 1000
- 请不要使用内置的 LinkedList 库。
- 调用
get
、addAtHead
、addAtTail
、addAtIndex
和deleteAtIndex
的次数不超过2000
。
解答
使用双向链表实现 MyLinkedList
类的 Java 代码。代码设计使用虚拟头节点 (dummyHead
) 和虚拟尾节点 (dummyTail
) 来简化边界操作(如头插和尾插),并通过维护 size
变量记录链表长度以提高索引效率。
ini
java
复制
public class MyLinkedList {
// 双向链表节点定义
private class ListNode {
int val;
ListNode prev;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode prev, ListNode next) {
this.val = val;
this.prev = prev;
this.next = next;
}
}
private ListNode dummyHead; // 虚拟头节点
private ListNode dummyTail; // 虚拟尾节点
private int size; // 链表实际长度(不含虚拟节点)
// 初始化链表
public MyLinkedList() {
dummyHead = new ListNode();
dummyTail = new ListNode();
dummyHead.next = dummyTail;
dummyTail.prev = dummyHead;
size = 0;
}
// 获取指定索引的节点值(索引无效时返回 -1)
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode curr = getNode(index); // 获取目标节点
return curr.val;
}
// 在头部插入节点
public void addAtHead(int val) {
addAtIndex(0, val); // 复用 addAtIndex 实现
}
// 在尾部插入节点
public void addAtTail(int val) {
addAtIndex(size, val); // 复用 addAtIndex 实现
}
// 在指定索引插入节点
public void addAtIndex(int index, int val) {
if (index < 0) index = 0; // 负数索引视为头部插入
if (index > size) return; // 超过长度则忽略
// 插入位置的目标节点(新节点将插入在其之前)
ListNode targetNode;
if (index == size) {
targetNode = dummyTail; // 插在尾部:目标节点是虚拟尾节点
} else {
targetNode = getNode(index); // 中间插入:获取当前索引节点
}
// 创建新节点并链接
ListNode newNode = new ListNode(val);
ListNode prevNode = targetNode.prev;
// 连接新节点与前驱
prevNode.next = newNode;
newNode.prev = prevNode;
// 连接新节点与后继
newNode.next = targetNode;
targetNode.prev = newNode;
size++; // 更新链表长度
}
// 删除指定索引节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) return;
ListNode targetNode = getNode(index); // 获取目标节点
// 断开目标节点的连接
ListNode prevNode = targetNode.prev;
ListNode nextNode = targetNode.next;
prevNode.next = nextNode;
nextNode.prev = prevNode;
// 清空引用(帮助GC)
targetNode.prev = null;
targetNode.next = null;
size--; // 更新链表长度
}
// 辅助方法:获取指定索引的节点(优化遍历:根据位置选择方向)
private ListNode getNode(int index) {
if (index < size / 2) {
// 从前向后遍历
ListNode curr = dummyHead.next;
for (int i = 0; i < index; i++) {
curr = curr.next;
}
return curr;
} else {
// 从后向前遍历
ListNode curr = dummyTail.prev;
for (int i = size - 1; i > index; i--) {
curr = curr.prev;
}
return curr;
}
}
}
代码说明:
-
节点定义:
ListNode
类包含值 (val
) 和前驱/后继指针 (prev
,next
)。
-
虚拟头尾节点:
dummyHead
和dummyTail
始终存在但不存储实际数据,确保头/尾插入统一处理。- 初始化时二者互连,形成空链表结构。
-
核心方法:
-
**
get(int index)
**:- 检查索引有效性,无效返回
-1
。 - 调用
getNode(index)
获取目标节点值。
- 检查索引有效性,无效返回
-
**
addAtHead(int val)
/addAtTail(int val)
**:- 复用
addAtIndex(0, val)
和addAtIndex(size, val)
实现。
- 复用
-
**
addAtIndex(int index, int val)
**:- 处理负数索引(视为头插)。
- 若索引超过
size
则忽略。 - 获取目标节点(若索引为
size
则目标为虚拟尾节点)。 - 创建新节点并链接到目标节点和其前驱之间。
-
**
deleteAtIndex(int index)
**:- 检查索引有效性,无效则返回。
- 获取目标节点,断开其与前后节点的连接,并更新链表长度。
-
-
**辅助方法
getNode(int index)
**:-
根据索引位置优化遍历方向:
- 索引在前半部分:从
dummyHead.next
向后遍历。 - 索引在后半部分:从
dummyTail.prev
向前遍历。
- 索引在前半部分:从
-
特点:
-
时间复杂度:
addAtHead
/addAtTail
:O(1)。get
/addAtIndex
/deleteAtIndex
:平均 O(n/2)(通过双向遍历优化)。
-
空间复杂度:O(n)(存储 n 个节点)。
-
简化边界:虚拟头尾节点避免处理空链表、单节点链表等边界情况。