707. 设计链表
题目
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
- get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
- addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
- addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
- addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
- deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
示例:
MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2); //链表变为1-> 2-> 3
linkedList.get(1); //返回2
linkedList.deleteAtIndex(1); //现在链表是1-> 3
linkedList.get(1); //返回3
解法
链表是一个包含零个或多个元素的数据结构。每个元素都包含一个值和到另一个元素的链接。根据链接数的不同,可以分为单链表,双链表和多重链表。
单链表是最简单的一种,它提供了在常数时间内的 addAtHead 操作和在线性时间内的 addAtTail 的操作。
双链表是最常用的一种,因为它提供了在常数时间内的 addAtHead 和 addAtTail 操作,并且优化的插入和删除。
双链表在 Java 中的实现为 LinkedList,在 Python 中为 list。这些结构都比较常用,有两个要点:
-
哨兵节点:哨兵节点在树和链表中被广泛用作伪头、伪尾等,通常不保存任何数据。我们将使用伪头来简化我们简化插入和删除。在接下来的两种方法中应用此方法。
-
双链表的双向搜索:我们可以从头部或尾部进行搜索。
单链表
哨兵节点dummyHead被用作伪头始终存在,这样结构中永远不为空,它将至少包含一个伪头。MyLinkedList 中所有节点均包含:值 + 链接到下一个元素的指针。
因为伪头的关系 addAtHead 和 addAtTail 可以使用 addAtIndex 来完成。
这个想法很简单:
- 找到要插入位置节点的前驱节点。如果要在头部插入,则它的前驱节点就是伪头。如果要在尾部插入节点,则前驱节点就是尾节点。
- 通过改变 next 来插入节点。
public class MyLinkedList {
int size;
ListNode dummyHead;
public MyLinkedList() {
size = 0;
dummyHead = new ListNode(-1);
}
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode cur = dummyHead;
for (int i = 0; i < index + 1; i++) {
cur = cur.next;
}
return cur.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;
}
index = index < 0 ? 0 : index;
++size;
ListNode pre = dummyHead;
for (int i = 0; i < index; i++) {
pre = pre.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = pre.next;
pre.next = toAdd;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
ListNode pre = dummyHead;
for (int i = 0; i < index; i++) {
pre = pre.next;
}
pre.next = pre.next.next;
}
}
双链表
双链表比单链表快得多,测试用例花费的时间比单链表快了两倍。但是它更加复杂,它包含了 size,记录链表元素个数,和伪头伪尾。
- 找到要插入节点的前驱节点和后继节点。如果要在头部插入节点,则它的前驱结点是伪头。如果要在尾部插入节点,则它的后继节点是伪尾。
- 通过改变前驱结点和后继节点的链接关系添加元素。
toAdd.prev = pred
toAdd.next = succ
pred.next = toAdd
succ.prev = toAdd
get:
- 通过比较 index 和 size - index 的大小判断从头开始较快还是从尾巴开始较快。
- 从较快的方向开始。
public class ListNode {
int val;
ListNode next;
ListNode prev;
ListNode(int x) {
val = x; }
}
class MyLinkedList {
int size;
ListNode dummyHead;
ListNode dummyTail;
public MyLinkedList() {
size = 0;
dummyHead = new ListNode(-1);
dummyTail = new ListNode(-1);
dummyHead.next = dummyTail;
dummyTail.prev = dummyHead;
}
public int get(int index) {
if (index < 0 || index >= size) {
return -1;
}
ListNode curr = dummyHead;
// 判断从头部开始比较快还是从尾部比较快
if (index + 1 < size - index) {
for (int i = 0; i < index + 1; i++) {
curr = curr.next;
}
} else {
curr = dummyTail;
for (int i = 0; i < size - index; i++) {
curr = curr.prev;
}
}
return curr.val;
}
public void addAtHead(int val) {
++size;
ListNode pre = dummyHead;
ListNode succ = dummyHead.next;
ListNode toAdd = new ListNode(val);
toAdd.prev = pre;
toAdd.next = succ;
pre.next = toAdd;
succ.prev = toAdd;
}
public void addAtTail(int val) {
++size;
ListNode succ = dummyTail;
ListNode pre = dummyTail.prev;
ListNode toAdd = new ListNode(val);
toAdd.prev = pre;
toAdd.next = succ;
pre.next = toAdd;
succ.prev = toAdd;
}
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
ListNode pre;
ListNode succ;
// 判断从头部开始比较快还是从尾部比较快
if (index < size - index) {
pre = dummyHead;
for (int i = 0; i < index; ++i) {
pre = pre.next;
}
succ = pre.next;
} else {
succ = dummyTail;
for (int i = 0; i < size - index; i++) {
succ = succ.prev;
}
pre = succ.prev;
}
++size;
ListNode toAdd = new ListNode(val);
toAdd.prev = pre;
toAdd.next = succ;
pre.next = toAdd;
succ.prev = toAdd;
}
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
ListNode pre;
ListNode succ;
// 判断从头部开始比较快还是从尾部比较快
if (index < size - index) {
pre = dummyHead;
for (int i = 0; i < index; ++i) {
pre = pre.next;
}
// 注意这里的succ是删除之后的后继节点
succ = pre.next.next;
} else {
succ = dummyTail;
// 注意这里是size - index - 1,前面add是size - index
for (int i = 0; i < size - index - 1; i++) {
succ = succ.prev;
}
// 注意这里的pre是删除之后的前序节点
pre = succ.prev.prev;
}
--size;
pre.next = succ;
succ.prev = pre;
}
}