二、707.设计链表
题解参考:代码随想录 (programmercarl.com)
1. 题目描述
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:
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
的节点。
示例:
输入
["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 。
2. 解题思路
链表操作的两种方式:
- 直接使用原来的链表来进行操作。
- 设置一个虚拟头结点在进行操作。
下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)。这样的话,我们删除头结点和在头结点之前插入结点的操作就和其他结点一致了。大家如果对虚拟头结点的使用不太清楚的话,可以看一下链表篇:一、203. 移除链表元素这篇博客,里面有对虚拟头结点的使用说明。
删除单链表结点:
新增单链表结点:
3. 虚拟头结点
使用虚拟头结点之后,头结点处的处理方法和其他结点一样。
这道题目设计链表的五个接口:
- 获取链表第index个节点的数值
- 在链表的最前面插入一个节点
- 在链表的最后面插入一个节点
- 在链表第index个节点前面插入一个节点
- 删除链表的第index个节点
具体代码实现的过程中有以下几点我们需要注意:
- 获取值的时候,我们可以令
cur=head->next
,因为我们不需要对该结点进行操作,只需要获取当前的值,所以可以直接令cur=head->next
,简化代码。 - 新增和删除结点的时候,由于我们是要操作第
index
个结点,而要想新增或删除该结点,根据上面的图示,我们首先需要知道第index-1
个结点的指针,通过(index-1)->next
对第index
个结点进行操作。因此,我们可以令cur=head
,然后遍历数组,对数组新增或删除 - 因为题目指定结构体只有
val
和next
,没有链表的大小size
,所以在判断index
是否合法的时候,我们需要通过cur != NULL
来确定循环终止条件。当然,我们也可以定义一个链表长度size
,这样代码会简洁一点。我这里偷了个懒,就没有使用size
了。
代码如下所示
typedef struct MyLinkedList {
int val;
struct MyLinkedList* next;
}MyLinkedList;
/**初始化 MyLinkedList 对象*/
MyLinkedList* myLinkedListCreate() {
//这个题必须用虚拟头指针
MyLinkedList* head = (MyLinkedList *)malloc(sizeof (MyLinkedList));
head->next = NULL;
return head;
}
/**获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1*/
int myLinkedListGet(MyLinkedList* obj, int index) {
// 这里是获取第index个元素,cur指向obj->next
MyLinkedList *cur = obj->next;
// cur != NULL,防止index大于链表长度
for (int i = 0; cur != NULL; i++){
if (i == index){
return cur->val;
}
else{
cur = cur->next;
}
}
return -1;
}
/** 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。 */
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newNode->val = val;
// 注意顺序
newNode->next = obj->next;
obj->next = newNode;
}
/** 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。 */
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
// 新增,cur指向obj,即cur->next才是要操作的节点
MyLinkedList* cur = obj;
// 注意是cur->next不为空
while(cur->next != NULL){
cur = cur->next;
}
MyLinkedList* newNode = (MyLinkedList*)malloc(sizeof(MyLinkedList));
newNode->val = val;
newNode->next = NULL;
cur->next = newNode;
}
/**将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。 */
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
// index < 0 不操作
// 新增,cur指向obj,cur->next才是指向要操作的节点
MyLinkedList *cur = obj;
// cur != NULL,防止index大于链表长度
for (int i = 0 ;cur != NULL; i++){
if (i == index){
MyLinkedList* newnode = (MyLinkedList *)malloc(sizeof (MyLinkedList));
newnode->val = val;
newnode->next = cur->next;
cur->next = newnode;
}
else{
cur = cur->next;
}
}
}
/** 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。 */
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
MyLinkedList *cur = obj;
// cur != NULL用来判断index是否大于链表长度
for (int i = 0; cur != NULL ; i++){
if (i == index){
MyLinkedList *tmp = cur->next;
if (tmp != NULL) {
cur->next = tmp->next;
free(tmp);
}
}
else{
cur = cur->next;
}
}
}
/**清空链表 */
void myLinkedListFree(MyLinkedList* obj) {
while(obj != NULL){
MyLinkedList *tmp = obj;
obj = obj->next;
free(tmp);
}
}
/**
* Your MyLinkedList struct will be instantiated and called as such:
* MyLinkedList* obj = myLinkedListCreate();
* int param_1 = myLinkedListGet(obj, index);
* myLinkedListAddAtHead(obj, val);
* myLinkedListAddAtTail(obj, val);
* myLinkedListAddAtIndex(obj, index, val);
* myLinkedListDeleteAtIndex(obj, index);
* myLinkedListFree(obj);
*/
4. Java版
这里我们使用Java替换上面的代码,由于Java有自己的内存回收机制,所以这里不需要我们手动管理内存空间。
这里我做了两点修改
- 定义了链表长度
size
,简化了非法index的判断。 addAtHead
和addAtTail
,我直接调用addAtIndex
,因为前者是后者的特殊情况。这样的话代码可以简化代码。
代码如下所示
//单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
}
class MyLinkedList {
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head.next;
while (index-- > 0) {
currentNode = currentNode.next;
}
return currentNode.val;
}
//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode currentNode = head;
while (index-- > 0) {
currentNode = currentNode.next;
}
ListNode toAdd = new ListNode(val);
toAdd.next = currentNode.next;
currentNode.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
ListNode currentNode = head;
while (index-- > 0) {
currentNode = currentNode.next;
}
currentNode.next = currentNode.next.next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
可以看到,相比于C语言的写法,加上size
之后,代码简化了很多。
5. 总结
链表类题目无脑用虚拟头结点,虚拟头结点的解法比直接在原链表上修改简化很多。
这道的易错点主要有以下几点:
- 内存分配与释放(C/C++)
- 链表的遍历
- 第0个和最后一个节点怎么处理
- 怎么遍历链表才不会出现空指针异常
- 链表构造,怎么挂链
因此,我们要弄清楚cur
结点到底指向哪里,循环终止条件又是什么,这道题就很容易了。如果对循环终止条件不清楚的话,可以直接带极端情况进行判断。