参考
题目一:LeetCode 203.移除链表元素
一、按照自己的想法求解
在第一天中也有移除指定元素的题,只是那个题是数组,这里是链表。链表的插入和删除操作有很大不同,数组和链表的相关知识可以参考之前的一篇笔记:线性表。
在链表的操作中,很多时候都会在头节点之前再插入一个虚拟头节点,这样做的好处是对头节点的操作和对其他节点的操作一样,否则头节点要作为特殊节点处理。本题内容是链表的删除操作,链表的删除无非就是将要删除节点的上一个节点指向被删除节点的下一个节点,在C/C++中还需要将被删除的节点释放,否则会造成内存泄漏。本题的代码如下:
/**
* 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) {
if(head == nullptr) return nullptr;
ListNode* node = new ListNode(0,head); //虚拟头节点
ListNode* cur = node; //遍历链表的指针
ListNode* tmp;
while(cur->next != nullptr)
{
if(cur->next->val == val)
{
tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}
else
cur = cur->next;//指向下一个节点
}
tmp = node->next;
delete node;
return tmp;
}
};
出错记录
最初while循环中少了else,并不是遗漏了,就是认为不要else分支(代码如下),结果提交出错,注意不是结果错误,是直接出错,但又能通过部分用例。那为什么少了else在某些情况下会直接出错呢?
//错误代码
while(cur->next != nullptr)
{
if(cur->next->val == val)
{
tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}
cur = cur->next;//指向下一个节点
}
原因在于定义的cur指针指向的是与val比较的节点的上一个节点,例如下图中要比较节点3的值是否与给定的val相等,那么此时cur指针是指向节点3的上一个节点的,即指向节点2。那为什么要指向上一个节点,直接指向要比较的节点不行吗?这和单链表的特点有关,单链表的每一个节点包含一个值和一个指向下一个节点的指针,也就是说只有节点2知道节点3在哪里,如果要删除节点3,那么只需要将节点2的下一个节点设置为节点4然后释放节点3占用的内存就可以了,改变节点2的下一个节点的代码为:
cur->next = cur->next->next;
如果非要使cur指针指向待比较的节点,那么必须使用额外的一个变量保存cur的指向节点的上一个节点,这样在需要的时候才能删除cur指向的节点。
回到缺少else出错的问题,现在已经知道为什么cur要指向上一个节点了。cur = cur->next;
这条语句的作用是让cur指向下一个节点,这没什么问题,以上图进行说明,此时cur指向节点2,如果我们要删除节点3,那么会执行if语句块中的内容,其作用是将节点2的下一个节点设置为节点4,然后释放节点3占用的内存,在这里面没有改变cur的指向,如果没有else分支,那么cur会指向下一个节点,即节点4,这样下一次就会用cur->next->val和val进行比较,这样问题就出现了,节点4被遗漏了。
while循环中的循环条件也需要注意一下,当最后一个节点进行比较时,cur指针指向倒数第二个节点,比较完成之后cur指向最后一个节点,判断指向最后一个节点的方法就是-判断下一个节点是否为空,因为循环的条件就是cur->next != nullptr
。
二、参考代码随想录的求解
代码随想录给出代码如下:
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;
}
};
因为这个题之前看过代码随想录的解法,所以自己的做法就是参照这种做法,但具体的代码实现上自己的代码还可以优化。
小结
- 要明确指针的指针,例如在本题中cur指针是指向上一个节点的,用cur->next->val和val进行比较
- 在链表操作中经常会引入虚拟头节点,这样对头节点的操作就和其他节点一致了,否则需要最头节点做特殊处理
题目二:LeetCode 707.设计链表
一、按照自己的想法求解
虽然这个题之前做过,但是今天做的时候又出现了很多问题,这里记录一下。注意下面的代码是错误的。
class MyLinkedList {
public:
MyLinkedList() {
}
int get(int index) {
ListNode* cur = pHead;
while(index--)
{
cur = cur->next;
if(cur == nullptr) return -1;
}
return cur->val;
}
void addAtHead(int val) {
ListNode* dummyNode = new ListNode(0,pHead);
ListNode* node = new ListNode(val,dummyNode->next); //新节点
dummyNode->next = node;
//pHead = node;
delete dummyNode;
}
void addAtTail(int val) {
ListNode* cur = pHead;
ListNode* node = new ListNode(val,nullptr);
//找到最后节点
while(cur->next != nullptr) cur = cur->next;
cur->next = node;
}
void addAtIndex(int index, int val) {
if(index < 0) addAtHead(val);
else
{
ListNode* dummyNode = new ListNode(0,pHead);
ListNode* cur = dummyNode;
while(index--)
{
cur = cur->next;
if(cur == nullptr) return;
}
ListNode* node = new ListNode(val,cur->next);
cur->next = node;
delete dummyNode;
}
}
void deleteAtIndex(int index) {
if(index < 0) return;
ListNode* dummyNode = new ListNode(0,pHead);
ListNode* cur = dummyNode;
while(index--)
{
cur = cur->next;
if(cur == nullptr) return;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
delete dummyNode;
}
private:
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){}
};
ListNode* pHead = new ListNode();
};
问题出在定义的这个头节点上,写的时候把它当作了真正的头节点,这样无论如何这个链表都不会空,至少有一个头节点存在,这样在后面实现相关函数的时候就非常混乱。这样写之后出问题,由于我使用的是ubuntu系统,在ubuntu下找不到合适的ide,只能硬着头皮用GD调试B,花了很长时间,好在最后也找到了问题。解决思路是把上面定义的头节点作为虚拟头节点,该节点仅在内部对链表进行操作时使用,该不对外表现出来,也就是说,在这个类的外部你是不知道这个节点的存在的。修正后的代码如下:
class MyLinkedList {
public:
MyLinkedList() {
}
int get(int index) {
if(index < 0) return -1;
else
{
ListNode* cur = dummyNode->next;
if(cur == nullptr && index == 0) return -1;
while(index--)
{
cur = cur->next;
if(cur == nullptr) return -1;
}
return cur->val;
}
}
void addAtHead(int val) {
ListNode* node = new ListNode(val,dummyNode->next);
dummyNode->next = node;
}
void addAtTail(int val) {
ListNode* cur = dummyNode;
while(cur->next != nullptr) cur = cur->next;
ListNode* node = new ListNode(val,nullptr);
cur->next = node;
}
void addAtIndex(int index, int val) {
if(index < 0 ) addAtHead(val);
else
{
ListNode* cur = dummyNode;
while(index--)
{
cur = cur->next;
if(cur == nullptr) return;
}
ListNode* node = new ListNode(val,cur->next);
cur->next = node;
}
}
void deleteAtIndex(int index) {
if(index < 0) return;
ListNode* cur = dummyNode;
while(index--)
{
cur = cur->next;
if(cur->next == nullptr) return;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}
private:
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){}
};
ListNode* dummyNode = new ListNode(); //虚拟头节点
};
/**
* 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);
*/
二、参考代码随想录的实现
看了代码随想录给出的代码之后,觉得自己的代码还有很多优化的地方,而且自己对链表还不是很熟悉,所以决定再写一边。给出的代码里有两个值的学习的地方,一是增加了一个_size
变量来表示链表的长度,这在链表的插入、删除和读取的时候非常有帮助,自己在写的时候在这三个函数里需要边遍历边判断(因为不能直接得到链表的长度),引入这个变量之后直接用index与size进行比较就行了,简化了代码设计,而且还不容易出错;二是增加了打印链表的函数,这在后面调试的时候非常有用,特别是在不能用ide调试的时候。
class MyLinkedList {
public:
MyLinkedList() {
size = 0;
dummyNode = new ListNode(0,nullptr);
}
int get(int index) {
if(index < 0 || index > size -1) return -1;
ListNode* cur = dummyNode->next;
while(index--) cur = cur->next;
return cur->val;
}
void addAtHead(int val) {
ListNode* node = new ListNode(val,dummyNode->next);
dummyNode->next = node;
size++;
}
void addAtTail(int val) {
ListNode* cur = dummyNode;
while(cur->next != nullptr) cur = cur->next;
ListNode* node = new ListNode(val,nullptr);
cur->next = node;
size++;
}
void addAtIndex(int index, int val) {
if(index < 0) addAtHead(val);
else if(index > size) return;
else
{
ListNode* cur = dummyNode;
while(index--) cur = cur->next;
ListNode* node = new ListNode(val,cur->next);
cur->next = node;
}
size++;
}
void deleteAtIndex(int index) {
if(index < 0 || index > size - 1) return;
else{
ListNode* cur = dummyNode;
while(index--) cur = cur->next;
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}
size--;
}
//打印链表
void printLinkedList(void)
{
ListNode* cur = dummyNode->next;
while(cur != nullptr)
{
cout << cur->val << " ";
cur = cur->next;
}
cout << endl;
}
private:
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){}
};
int size;
ListNode* dummyNode;
};
/**
* 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);
*/
小结
- 对链表的操作很多时候都会涉及遍历,遍历的时候一定一定要注意遍历指针cur的指向,在删除和插入的时候指向上一个节点(这由链表本身的特点决定),在获取某个节点的值的时候遍历指针指向该节点,这一点需要注意,否则写代码的时候很容易出错。
- 自己设计链表的时候要定义一个虚拟头节点,该结点只在内部使用,不对外表现出来。另外,用一个变量size来记录链表的长度,在编程的时候会方便很多。
题目三:LeetCode 206.反转链表
一、按照自己的想法实现
这个题之前做过,感觉是这三个题中最容易的那个题了,一次就写出来了,没有难点。
/**
* 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* next = nullptr;
ListNode* cur = head;
while(cur != nullptr)
{
ListNode* tmp = cur->next;//反转之前先记录下一个节点,否则之后就找不到下个节点了
cur->next = next; //反转
/* 向右移动三个指针,注意顺序 */
next = cur;
cur = tmp;
}
return next;
}
};
需要用到三个指针,一个指针cur用于遍历链表;指针tmp用于指向cur指向的下一个节点,因为如果不保存,反转之后就找不到反转之前cur的下一个节点了;指针next指向反转之后的下一个节点。
二、参考代码随想录的方法实现
代码随想录提供了两种方法:双指针法和递归法,其中双指针法就是上面自己实现的方法,只是变量的命名有些不同。
方法一:双指针法
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp; // 保存cur的下一个节点
ListNode* cur = head;
ListNode* pre = NULL;
while(cur) {
temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next
cur->next = pre; // 翻转操作
// 更新pre 和 cur指针
pre = cur;
cur = temp;
}
return pre;
}
};
方法二:递归法
递归的方法代码更简洁一些,但也相对要难理解一些,reverse()函数的作用是使得cur->next = pre
,直到cur == nullptr
为之,此时返回pre指针就是反转之后的头节点。
/**
* 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* reverse(ListNode* pre,ListNode* cur)
{
if(cur == nullptr) return pre;
ListNode* tmp = cur->next;
cur->next = pre;
return reverse(cur,tmp);
}
ListNode* reverseList(ListNode* head) {
return reverse(nullptr,head);
}
};
小结
这题相比起前面两个题要容易很多,做的时候没有遇到什么难点,重点理解一下递归法就可以了。
今日小结
今天在设计链表这个题上花了很多时间,主要在思路上出了问题,好在最后自己解决了,看了代码随想录给的代码之后进一步优化。链表的注意点:
- 注意遍历指针cur的指向,在删除和插入中指向上一个节点,在获取某个节点的值中指向当前节点
- 在设计链表(包括删除、插入操作)中注意虚拟头节点的使用,可简化代码设计
- 在设计链表中在类内部引入一个变量size可以简化设计,另外额外增加一个函数来打印链表可以帮助调试,在出现问题之后可以快速定位问题
- 在遍历链表时,循环条件与遍历指针的指向有关,特别注意遍历指针的指向