算法题 设计链表
设计链表实现 本文介绍了两种链表实现方法:单链表和双链表。两种方法都使用虚拟头节点简化边界处理,并维护链表长度以优化性能。单链表实现包含基本的增删查操作,时间复杂度为O(n)。双链表实现通过双向指针优化了遍历效率,特别是对于靠近尾部的节点。两种实现都避免了使用内置链表库,符合题目要求。代码示例展示了如何实现get、addAtHead、addAtTail、addAtIndex和deleteAtInd
·
设计链表
问题描述
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。
如果是双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。
实现 MyLinkedList 类:
int get(int index)获取链表中第index个节点的值。如果索引无效,则返回-1。void addAtHead(int val)在链表的第一个元素之前添加一个值为val的节点。插入后,新节点将成为链表的第一个节点。void addAtTail(int val)将值为val的节点追加到链表的最后一个元素。void addAtIndex(int index, int val)在链表中的第index个节点之前添加值为val的节点。如果index等于链表的长度,则该节点将附加到链表的末尾。如果index大于链表长度,则不会插入节点。如果index小于 0,则在头部插入节点。void deleteAtIndex(int index)如果索引index有效,则删除链表中的第index个节点。
注意:
- 所有
val值都在[1, 1000]范围内。 - 操作次数不会超过
2000次。 - 请不要使用内置的 LinkedList 库。
示例:
输入: ["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); // 链表: 1
myLinkedList.addAtTail(3); // 链表: 1->3
myLinkedList.addAtIndex(1,2); // 链表: 1->2->3
myLinkedList.get(1); // 返回 2
myLinkedList.deleteAtIndex(1); // 链表: 1->3
myLinkedList.get(1); // 返回 3
算法思路
-
虚拟头节点(哨兵节点):
- 使用虚拟头节点简化边界处理
- 避免对空链表的特殊处理
- 所有操作都可以统一处理
-
长度维护:
- 维护链表长度,避免每次遍历计算
- 用于快速验证索引有效性
-
操作:
- get(index):遍历到第index个节点
- addAtHead(val):在虚拟头节点后插入
- addAtTail(val):遍历到尾节点后插入
- addAtIndex(index, val):在第index个位置前插入
- deleteAtIndex(index):删除第index个节点
代码实现
方法一:单链表 + 虚拟头节点
class MyLinkedList {
// 链表节点定义
private static class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
this.next = null;
}
}
private ListNode dummyHead; // 虚拟头节点
private int size; // 链表长度
/**
* 初始化链表
*/
public MyLinkedList() {
dummyHead = new ListNode(-1); // 虚拟头节点,值不重要
size = 0;
}
/**
* 获取链表中第index个节点的值
*
* @param index 节点索引 (0-based)
* @return 节点值,如果索引无效返回-1
*/
public int get(int index) {
// 检查索引有效性
if (index < 0 || index >= size) {
return -1;
}
// 从虚拟头节点开始,移动index+1步到达目标节点
ListNode current = dummyHead;
for (int i = 0; i <= index; i++) {
current = current.next;
}
return current.val;
}
/**
* 在链表头部添加节点
*
* @param val 要添加的节点值
*/
public void addAtHead(int val) {
addAtIndex(0, val);
}
/**
* 在链表尾部添加节点
*
* @param val 要添加的节点值
*/
public void addAtTail(int val) {
addAtIndex(size, val);
}
/**
* 在指定索引位置添加节点
*
* @param index 插入位置的索引
* @param val 要添加的节点值
*/
public void addAtIndex(int index, int val) {
// 如果index大于链表长度,不插入
if (index > size) {
return;
}
// 如果index小于0,在头部插入
if (index < 0) {
index = 0;
}
// 找到插入位置的前一个节点
ListNode prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// 创建新节点并插入
ListNode newNode = new ListNode(val);
newNode.next = prev.next;
prev.next = newNode;
size++;
}
/**
* 删除指定索引位置的节点
*
* @param index 要删除的节点索引
*/
public void deleteAtIndex(int index) {
// 检查索引有效性
if (index < 0 || index >= size) {
return;
}
// 找到要删除节点的前一个节点
ListNode prev = dummyHead;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
// 删除节点
prev.next = prev.next.next;
size--;
}
}
方法二:双链表
class MyLinkedList {
private static class ListNode {
int val;
ListNode next;
ListNode prev;
ListNode(int val) {
this.val = val;
}
}
private ListNode head; // 虚拟头节点
private ListNode tail; // 虚拟尾节点
private int size;
public MyLinkedList() {
head = new ListNode(-1);
tail = new ListNode(-1);
head.next = tail;
tail.prev = head;
size = 0;
}
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode current;
// 从较近的一端开始遍历
if (index < size / 2) {
current = head.next;
for (int i = 0; i < index; i++) {
current = current.next;
}
} else {
current = tail.prev;
for (int i = 0; i < size - 1 - index; i++) {
current = current.prev;
}
}
return current.val;
}
public void addAtHead(int val) {
addAtIndex(0, val);
}
public void addAtTail(int val) {
addAtIndex(size, val);
}
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
ListNode prev;
if (index < size / 2) {
prev = head;
for (int i = 0; i < index; i++) {
prev = prev.next;
}
} else {
prev = tail.prev;
for (int i = 0; i < size - index; i++) {
prev = prev.prev;
}
}
ListNode newNode = new ListNode(val);
ListNode next = prev.next;
prev.next = newNode;
newNode.prev = prev;
newNode.next = next;
next.prev = newNode;
size++;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
ListNode current;
if (index < size / 2) {
current = head.next;
for (int i = 0; i < index; i++) {
current = current.next;
}
} else {
current = tail.prev;
for (int i = 0; i < size - 1 - index; i++) {
current = current.prev;
}
}
current.prev.next = current.next;
current.next.prev = current.prev;
size--;
}
}
算法分析
- 时间复杂度:
get(index):O(index),最坏O(n)addAtHead(val):O(1)addAtTail(val):O(n)(单链表),O(1)(双链表)addAtIndex(index, val):O(index),最坏O(n)deleteAtIndex(index):O(index),最坏O(n)
- 空间复杂度:O(n),n为链表长度
算法过程
初始化:
dummyHead → nullsize = 0
addAtHead(1):
- 在dummyHead后插入1
dummyHead → 1 → nullsize = 1
addAtTail(3):
- 在尾部插入3
dummyHead → 1 → 3 → nullsize = 2
addAtIndex(1, 2):
- 在索引1位置插入2
- 找到索引0的节点(值为1)
- 插入2:
dummyHead → 1 → 2 → 3 → null size = 3
get(1):
- 返回索引1的值:2
deleteAtIndex(1):
- 删除索引1的节点
dummyHead → 1 → 3 → nullsize = 2
get(1):
- 返回索引1的值:3
测试用例
public class TestMyLinkedList {
public static void main(String[] args) {
// 测试用例1:标准示例
MyLinkedList myLinkedList1 = new MyLinkedList();
myLinkedList1.addAtHead(1); // [1]
myLinkedList1.addAtTail(3); // [1,3]
myLinkedList1.addAtIndex(1,2); // [1,2,3]
System.out.println("get(1): " + myLinkedList1.get(1)); // 2
myLinkedList1.deleteAtIndex(1); // [1,3]
System.out.println("get(1): " + myLinkedList1.get(1)); // 3
System.out.println();
// 测试用例2:边界索引
MyLinkedList myLinkedList2 = new MyLinkedList();
System.out.println("get(0): " + myLinkedList2.get(0)); // -1 (空链表)
myLinkedList2.addAtHead(1);
System.out.println("get(0): " + myLinkedList2.get(0)); // 1
System.out.println("get(1): " + myLinkedList2.get(1)); // -1 (越界)
myLinkedList2.deleteAtIndex(0); // 删除唯一元素
System.out.println("get(0): " + myLinkedList2.get(0)); // -1
System.out.println();
// 测试用例3:addAtIndex边界情况
MyLinkedList myLinkedList3 = new MyLinkedList();
myLinkedList3.addAtIndex(0, 1); // 等同于addAtHead
myLinkedList3.addAtIndex(1, 2); // 等同于addAtTail
myLinkedList3.addAtIndex(1, 3); // 在中间插入
// 链表: [1,3,2]
System.out.println("get(0): " + myLinkedList3.get(0)); // 1
System.out.println("get(1): " + myLinkedList3.get(1)); // 3
System.out.println("get(2): " + myLinkedList3.get(2)); // 2
// 测试用例4:重复操作
MyLinkedList myLinkedList5 = new MyLinkedList();
myLinkedList5.addAtHead(1);
myLinkedList5.addAtHead(1); // 允许重复值
myLinkedList5.addAtTail(1);
// 链表: [1,1,1]
System.out.println("get(0): " + myLinkedList5.get(0)); // 1
System.out.println("get(1): " + myLinkedList5.get(1)); // 1
System.out.println("get(2): " + myLinkedList5.get(2)); // 1
myLinkedList5.deleteAtIndex(1); // 删除中间的1
// 链表: [1,1]
System.out.println("get(0): " + myLinkedList5.get(0)); // 1
System.out.println("get(1): " + myLinkedList5.get(1)); // 1
}
}
关键点
-
虚拟头节点:
- 统一处理空链表和非空链表的情况
- 避免对head的特殊处理
- 简化插入和删除操作的代码
-
索引有效性:
get和deleteAtIndex:索引范围[0, size-1]addAtIndex:索引范围(-∞, size],超出范围不插入
-
复用:
addAtHead和addAtTail可以复用addAtIndex- 减少代码重复,提高可维护性
-
边界情况处理:
- 空链表的操作
- 单节点链表的删除
- 越界索引
常见问题
-
为什么使用虚拟头节点?
- 避免对空链表的特殊处理
- 所有插入操作都可以统一为"在某节点后插入"
- 删除操作总是有前驱节点,无需特殊处理头节点
-
addAtIndex的索引规则?
index < 0:在头部插入index == size:在尾部插入index > size:不插入0 <= index < size:在指定位置插入
更多推荐

所有评论(0)