题目链接:leetcode 203、移除链表元素
文章讲解:代码随想录 203、移除链表元素讲解
视频讲解:手把手带你学会操作链表 | LeetCode:203.移除链表元素
自己看到题目的第一想法
题目: 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
提示:
列表中的节点数目在范围
[
0
,
1
0
4
]
[0, 10^4]
[0,104] 内
1
<
=
N
o
d
e
.
v
a
l
<
=
50
1 <= Node.val <= 50
1<=Node.val<=50
0
<
=
v
a
l
<
=
50
0 <= val <= 50
0<=val<=50
想法: 这个题目比较基础,因为之前自己学习数据结构的时候也了解过,所以写出题目比较顺利。大致记录一下自己的思路。关键点就两个,一个是删除链表节点要哪些操作,另一个就是考虑特殊情况,比如第一个节点就是val值怎么办。
删除链表节点需要delete当前访问的节点,然后把上一个节点链接到下一个节点,所以需要两个节点指针,一个指向当前访问的节点,另一个指向上一个节点,在循环中不停更新,然后删除节点时,要先给上一个节点的next赋值,然后再delete当前节点,否则就找不到下一个节点的地址了。
为了解决特殊情况,就有了虚拟头节点,因为有了虚拟头节点,原本的开头的节点地位就和其他节点一样了,不需要特别关照。我定义虚拟头节点一开始是建了一个节点对象,后来改成了指针,这样做可以与形参统一,防止写出乱子,初始化虚拟头节点时考虑了一下要赋什么val值,看了一下提示信息写了val的范围,不过还是担心出问题,后来发现不需要赋值val,因为根本不会访问,只要给next指针赋值原来的头节点地址就可以了。
最后大循环就是访问当前节点,当前节点不是nullptr就进行访问,判断是否需要删除。
时间复杂度
O
(
n
)
O(n)
O(n)
空间复杂度
O
(
1
)
O(1)
O(1)
自己写的代码:
/**
* 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* HEAD(new ListNode);
HEAD -> next = head;
ListNode* prev = HEAD;
ListNode* curr = head;
while (curr != nullptr) {
if (curr -> val == val) {
prev -> next = curr -> next;
delete curr;
curr = prev -> next;
} else {
curr = curr -> next;
prev = prev -> next;
}
}
return HEAD -> next;
}
};
看完代码随想录和大家探讨之后的想法
讲解给的代码:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
ListNode* cur = dummyHead;
while (cur->next != NULL) {
if(cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
head = dummyHead->next;
delete dummyHead;
return head;
}
};
差不多一个意思,有几个细节:
1、这里访问当前节点只用了一个节点指针,同时访问next和下一个next节点,我自己思考的时候这样想容易写错,所以还是保持自己的写法。
2、最后删除了虚拟头节点的指针,我自己写的没有回收这个内存。
题目链接:leetcode 707、设计链表
文章讲解:代码随想录 707、设计链表讲解
视频讲解:帮你把链表操作学个通透!LeetCode:707.设计链表
自己看到题目的第一想法
题目: 不粘贴了,就是设计一个链表类,实现常用功能。
想法: 这个题目非常常见了,然后自己直接开始写,思路比较清晰,但是由于自己不熟悉,所以还是出现了问题。
这里贴一下自己写的有bug代码:
class MyLinkedList {
public:
int val;
MyLinkedList* next;
int length;
MyLinkedList() {
val = -1;
next = nullptr;
length = 1;
}
MyLinkedList(int n) {
val = n;
next = nullptr;
length = 1;
}
int get(int index) {
MyLinkedList* curr = this;
for (int i = 0; i < index; i++) {
curr = curr -> next;
}
if (curr != nullptr) return curr -> val;
return -1;
}
void addAtHead(int val) {
MyLinkedList* head = new MyLinkedList(val);
head -> next = this;
//可以这样写吗? this不能改怎么办?
this = head;
length++;
}
void addAtTail(int val) {
MyLinkedList* curr = this;
while (curr -> next != nullptr) {
curr = curr -> next;
}
MyLinkedList* tail = new MyLinkedList(val);
curr -> next = tail;
length++;
}
void addAtIndex(int index, int val) {
if (index > length) {
return ;
}
if (index == length) {
addAtTail(val);
return ;
}
MyLinkedList* HEAD = new MyLinkedList();
HEAD -> next = this;
MyLinkedList* addPos = HEAD;
for (int i = 0; i < index; i++) {
addPos = addPos -> next;
}
MyLinkedList* add = new MyLinkedList(val);
MyLinkedList* temp = addPos -> next;
addPos -> next = add;
add -> next = temp;
length++;
return ;
}
void deleteAtIndex(int index) {
if (index >= length) return ;
MyLinkedList* HEAD = new MyLinkedList();
HEAD -> next = this;
MyLinkedList* deletePos = HEAD;
for (int i = 0; i < index; i++) {
deletePos = deletePos -> next;
}
//把deletePos的next删掉
MyLinkedList* temp = deletePos -> next -> next;
delete deletePos -> next;
deletePos -> next = temp;
length--;
return ;
}
};
自己写出bug问题原因有以下几点:
1、没有贯彻使用虚拟头节点,反而造成了实现增删功能的时候考虑的东西过于复杂,以后链表题不思考过多,直接都使用虚拟头节点,让自己养成习惯。
2、和虚拟头节点相关,我自己在实现添加头部节点时产生了一个问题,题目该功能函数返回值是void,我怎么让当前类持有最新的头节点,于是我写的bug里就出现了修改this指针这种情况,其实我知道不对,但是不知道怎么实现,另外就是由于是在做题,就处处想着判题的程序能否识别我添加的其他成员变量,其实在类里添加虚拟头节点,改变虚拟头节点的next指针就可以了,自己写的时候过于畏手畏脚了,这也是平常自己写代码的时候一个不好的习惯。
3、最重要的一点是,我的思维有点僵化了,讲解给的代码是在链表类中又写了节点的结构体,链表类维护成员变量:长度_size和虚拟头节点_dummyHead。这个想法太关键了,我自己没有想多写这些代码,造成了很多困难,这个其实和上面第2个问题是一致的。
看完代码随想录和大家探讨之后的想法
没什么好说的
正确代码:
class MyLinkedList {
public:
// 定义链表节点结构体
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int val):val(val), next(nullptr){}
};
// 初始化链表
MyLinkedList() {
_dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点
_size = 0;
}
// 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点
int get(int index) {
if (index > (_size - 1) || index < 0) {
return -1;
}
LinkedNode* cur = _dummyHead->next;
while(index--){ // 如果--index 就会陷入死循环
cur = cur->next;
}
return cur->val;
}
// 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点
void addAtHead(int val) {
LinkedNode* newNode = new LinkedNode(val);
newNode->next = _dummyHead->next;
_dummyHead->next = newNode;
_size++;
}
// 在链表最后面添加一个节点
void addAtTail(int val) {
LinkedNode* newNode = new LinkedNode(val);
LinkedNode* cur = _dummyHead;
while(cur->next != nullptr){
cur = cur->next;
}
cur->next = newNode;
_size++;
}
// 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果index大于链表的长度,则返回空
// 如果index小于0,则在头部插入节点
void addAtIndex(int index, int val) {
if(index > _size) return;
if(index < 0) index = 0;
LinkedNode* newNode = new LinkedNode(val);
//注意初始值,在index前加节点需要访问上一个节点,所以从虚拟头节点开始
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
_size++;
}
// 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的
void deleteAtIndex(int index) {
if (index >= _size || index < 0) {
return;
}
//注意初始值,删除操作要访问的是上一个节点,所以从虚拟头节点开始
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur ->next;
}
LinkedNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
//delete命令指示释放了tmp指针原本所指的那部分内存,
//被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后,
//如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针
//如果之后的程序不小心使用了tmp,会指向难以预想的内存空间
tmp=nullptr;
_size--;
}
// 打印链表
void printLinkedList() {
LinkedNode* cur = _dummyHead;
while (cur->next != nullptr) {
cout << cur->next->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
int _size;
LinkedNode* _dummyHead;
};
几点细节提醒自己以后注意:
1、注意题目中index的意义,是从0开始的下标还是链表中第几个元素,本题目中是下标。
2、学到一个新的遍历链表的小技巧,就是while循环条件的设置。如果要访问尾节点,就是while(curr -> next !=nullptr),如果是访问某个index,就while(index–),循环体里curr = curr -> next;
3、注意在index处节点前添加节点和删除index处节点时,需要访问上一个节点,因为还需要把链表连上,所以遍历的初始值设置为虚拟头节点。在尾部添加节点也需要访问上一个节点,因为原本尾部没有节点,是访问不到的,所以初始也是虚拟头节点。而获取index处节点的值,方便的想法是直接访问index处节点,所以遍历初始值设置为虚拟头节点的next。
4、注意构造函数的写法,自己写的时候忘了C++11特性:初始化列表,虚拟头节点的val赋值无所谓,不用多想,长度一开始设置为0。
5、注意链表类和节点结构体的关系,记住这样的写法。
6、注意添加节点时,next指针的赋值顺序,我自己写的总用临时变量存,其实没必要。
题目链接:leetcode 206、反转链表
文章讲解:代码随想录 206、反转链表讲解
视频讲解:[帮你拿下反转链表 | LeetCode:206.反转链表(https://www.bilibili.com/video/BV1nB4y1i7eL)
自己看到题目的第一想法
题目: 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
想法: 自己写没写出来,这个题写过,但是忘记了,细节地方还是想不清楚,不浪费时间了,直接看讲解了。
看完代码随想录和大家探讨之后的想法
两种方法,双指针和递归法,递归法似乎不太常见。
自己没写出来的原因就是不熟悉双指针的思想,想不起来用。
/**
* 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* pre = nullptr;
ListNode* curr = head;
ListNode* temp;
while (curr != nullptr) {
temp = curr -> next;
curr -> next = pre;
pre = curr;
curr = temp;
}
return pre;
}
};
// class Solution {
// public:
// ListNode* reverse(ListNode* pre, ListNode* curr) {
// if (curr == NULL) return pre;
// ListNode* temp = curr -> next;
// curr -> next = pre;
// return reverse(curr, temp);
// }
// ListNode* reverseList(ListNode* head) {
// //递归法
// return reverse(NULL, head);
// }
// };
两个一起贴出来了,看完讲解以后自己又写了一遍。