203.移除链表元素
题目描述:
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
示例一:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例二:
输入:head = [7,7,7,7], val = 7
输出:[]
提示:
列表中的节点数目在范围 [0, 104] 内
1 <= Node.val <= 50
0 <= val <= 50
解题思路:
- 关键词提取:链表、头节点、删除指定节点
- 暴力解法:
- 新建一个链表
- 遍历头节点,不符合条件,就将值赋值给新链表的新节点,符合条件就忽略,不做处理
代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 新建一个链表,申请一个虚拟的头节点
ListNode* dummyHead = new ListNode(0);
dummyHead->next = NULL;
// 从虚拟头节点开始遍历,可以将需要遍历的链表的头节点包含在内
ListNode* curNode = dummyHead;
while(head != NULL)
{
// 不符合条件,可以存放到新的链表中
if(head->val != val)
{
// 申请一个新的节点
ListNode* tmp = new ListNode(0);
// 对新节点进行赋值
tmp->val = head->val;
tmp->next = NULL;
// 将新节点挂载到新的链表中
curNode->next = tmp;
// 更新新链表的遍历节点指针
curNode = curNode->next;
}
head = head->next;
}
// 返回新链表的头节点
return dummyHead->next;
}
};
总结:
-
第一次做的时候,仅仅会暴力解法,不按照题目的建议去做,而是采取一种新的方式,缺点是空间复杂度为o(n)。
1)第一次接触链表的知识,采取的是新建链表的方式,还是容易弄错的,如怎样将新的节点挂载到新的头节点后方。
2)虚拟头节点的方式是题解中会提到的,第一次做题时不理解,只是记住模板。 -
跟着代码随想录刷题的时候,找到需要特别注意的细节。
1)虚拟头节点的含义和用法。carl哥都讲解得很清晰。详见视频[https://www.bilibili.com/video/BV18B4y1s7R9/]:
2)不得不说,每次做链表的题目,都把自己绕晕了,暂时还没找到一个很清晰的路径。经常是比对代码,发现代码基本上一模一样,但是就是无法AC,就很离谱,多次折磨之下,还是没找到原因,只知道,按照题解代码,重新码一遍就好了。
3)其实,还是得去打印出来这个链表,才能更清晰一些,不能说放弃就放弃。最终发现,是自己对头节点的删除考虑不充分,这个头节点删除以后,会再次更新,还是头节点,因此,不能只判断一次,而是得找到下一个不需要删除的节点才算结束。
707.设计链表
题目描述:
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性: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
解题思路:
- 关键词提取:
- 解析:从零构建一个链表,需要对链表的各个操作都要做一个基础性掌握,一点点尝试。
代码如下:
class MyLinkedList {
public:
// 结构体,对链表节点做一个声明
struct LinkedList{
int val;
LinkedList* next;
// new 节点的时候,可以方便的做初始化
LinkedList(int val):val(val),next(nullptr){}
};
// 对声明做初始化
MyLinkedList() {
dummyHead = new LinkedList(0);
size = 0;
}
// 获取指定下标的值
int get(int index) {
// 初始化时,对链表大小,有做记录,不需要层层遍历,直接可以比对出大小,解决临界情况
if(index > size - 1 || index < 0){
return -1;
}
// 指针指向虚拟头节点的下一个值
LinkedList* curNode = dummyHead->next;
// 检索下标,当下标指向0时,即是要找的那个元素
while(index != 0)
{
index--;
curNode = curNode->next;
}
return curNode->val;
}
// 增加头节点
void addAtHead(int val) {
// 申请一个新的节点,并且插入到虚拟头节点后
LinkedList* tmp = new LinkedList(val);
tmp->next = dummyHead->next;
dummyHead->next = tmp;
size++;
}
// 增加尾节点
void addAtTail(int val) {
LinkedList* tmp = new LinkedList(val);
LinkedList* curNode = dummyHead;
// 从虚拟节点开始,看下一个节点是否是空节点,解决临界状态(头节点为空)
while(curNode->next != NULL)
{
curNode = curNode->next;
}
curNode->next = tmp;
size++;
}
// 指定下标插入节点
void addAtIndex(int index, int val) {
if(index > size){
return;
}
if(index < 0){
index = 0;
}
// 指向虚拟头节点
LinkedList* curNode = dummyHead;
// 找到需要插入的下标的前一个元素
while(index != 0)
{
index--;
curNode = curNode->next;
}
LinkedList* tmp = new LinkedList(val);
tmp->next = curNode->next;
// 在下标的前一个元素后面,插入新节点
curNode->next = tmp;
size++;
}
void deleteAtIndex(int index) {
if(index < 0 || index >= size){
return;
}
// 指向虚拟头节点
LinkedList* curNode = dummyHead;
// 找到需要插入的下标的前一个元素
while(index != 0)
{
index--;
curNode = curNode->next;
}
// 删除指定的节点
LinkedList* tmp = curNode->next;
curNode->next = curNode->next->next;
delete(tmp);
tmp = nullptr;
size--;
}
// 声明
private:
int size;
LinkedList* dummyHead;
};
/**
* 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);
*/
总结:
-
第一次做的时候,会采用暴力解法。
1)按照定义,无论是头插还是尾插,都需要对链表进行遍历。2)对于头节点是否为空,需要操作的下标是否超出链表长度,都需要做特殊处理。
-
跟着代码随想录刷题的时候,发现特殊处理都可以统一处理,不用搞得代码那么复杂。
1)采用虚拟头节点的方式。
2)在链表定义的时候,同时申请一个链表长度的变量,每次新增节点的时候,都做计数,因此,可以不需要遍历就完成与链表下标有关的临界条件处理,是时间复杂度的优化。
206.反转链表
题目描述:
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
示例一:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
示例二:
输入:head = [1,2]
输出:[2,1]
提示:
链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000
解题思路:
- 关键词提取:头节点、反转
- 解法一:新建一个链表,可以用头插法,达到链表反转的目标。
- 解法二:递归法,改变next指针的方向
代码如下:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 建立一个虚拟头节点
ListNode* dummyHead = new ListNode(0);
ListNode* curNode = dummyHead;
// 开始遍历原有链表,由前往后遍历
while(head != nullptr) {
// 新建节点
ListNode* newNode = new ListNode(head->val);
// 采用头插法,将新节点插入到虚拟头节点后面
newNode->next = curNode->next;
curNode->next = newNode;
// 原有链表头指针移动
head = head->next;
}
return dummyHead->next;
}
};
总结:
-
第一次做的时候,和上一题一样,新建一个链表的方式是最容易想到的,就跟数组一样。
1)其实,这也是一种懒惰,不愿意在原链表上进行操作,而且对于链表的遍历和指针敬而远之,经常会报错指针指向空,然后花大力气去查哪个位置条件没写对。2)链表的操作相对来说难以理解一些,即使画图也很难理解,跟数组的区别还是比较大的。
3)最终还是会采取打印出链表的方式去立体化自己的想法,看得到的会让人印象深刻一些,在不确定是否正确的时候,就增加打印日志吧。
-
跟着代码随想录刷题的时候,会发现还有两种解法,一种是双指针法,一种是从后往前的递归法,膜拜。
1)双指针法相对而言,容易理解一些,跟递归法一致,就是在反转链表的时候,先将前后节点的地址都保存起来,再按照指定的顺序重新分配next地址。
2)从后往前的解法,我一开始想到过,第一次做的时候,我是新建一个链表,然后将原链表进行倒序遍历(实际上就是遍历n!次,每次遍历到最后一个节点),然后将对应的值,顺序放到新的链表中。也是反复折腾,多次打印链表,多次调试才实现。这个思路用递归去实现,还是比较模糊,不够清晰,可能要三刷才能有感觉。