这3题都是链表相关,重点在于虚拟头节点的使用,双指针的初始化和使用,循环中指针相关的条件,以及指针非空的判断。还都是提前看过的题目但只有第2道能1次通过,尤其第1题很不顺利。
链表定义:
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) {} };
时间复杂度:
第1道题(203.移除链表元素)忘记了看过的解法,只记得关键在于建立虚拟头节点,用来统一对头节点和其他节点的删除操作。以为需要用到双指针,于是经过数次报错(主要是空指针)和修改,写出了能AC但不够简洁,也存在一些问题的双指针代码。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode virtHead = ListNode(0, head);
ListNode *left = &virtHead, *right = &virtHead;
while (right) {
while (right->next && right->next->val == val) {
right = right->next;
}
left->next = right->next;
left = right->next;
right = left;
}
return virtHead.next;
}
};
问题或值得注意的地方如下:
- 第4行建立虚拟头节点需要声明的是指针,而非ListNode本身;
- 外层循环条件要设定为right非空,而不是right->next非空,因为末尾节点需要被删除的话,上一轮外层循环中内层循环结束后,right已经指向末尾节点,10~12行right再次右移,指向了null,于是本轮循环直接判断right->next的话就会产生空指针报错;
- 内层循环条件中要判断right->next->val,需要首先判断right->next是否为空,否则也会遇到空指针错误。判断指针所指节点的值时,需首先判断指针是否为空指针,这一经验在其他链表问题中也适用;
- return曾误写为virtHead->next。在我上面的写法中,已经将virtHead定义声明为了节点本身而非节点指针,所以应该用“.”而非“->”。
- 删除节点时没有释放内存(虽然不影响OJ上的结果)。
回顾了正确解法后发现无需用到双指针,用单指针,在循环中每次前进一步即可。
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* virtHead = new ListNode(0, head);
ListNode* cur = virtHead;
while (cur->next) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
}
else {
cur = cur->next;
}
}
head = virtHead->next;
delete virtHead;
return head;
}
};
然而在实现中也遇到了几个问题:
- 第4行定义链表指针时报错,因为少写new。
- 对外层循环条件认识还不清晰。该方法每次操作涉及3个节点,cur为第1个,所以循环条件应该是第2个节点非空,这次才能保证末尾节点是可以被删除的。同时也不需要写第1个节点(即cur非空),因为每次循环最多只前进一步,最开始时条件满足(head非空)且上次循环条件(即cur->next非空)满足的话,本次的cur就一定非空。
- 内层判断条件的认识也不够清晰。因为外层循环条件已经保证了第2个节点非空,所以不需要写这一条件,可以直接判断第2个节点的val。
- 第13行的内容未加else。以为不论哪种情况都需要将cur右移,但该方法每次循环最多只删除一个节点,删除后第2个节点仍可能被删除,所以cur不能右移,只有第2个节点不是需要删除的节点时,cur才需要右移。
二刷:
- 上面的1;
- 上面的2;
- 上面的4。
第2题(707.设计链表)虽然有多个函数需要实现,代码量大,但感觉相比第1题要容易很多。这一题关键仍在于使用虚拟头节点来统一操作,以及维护链表的长度。
class MyLinkedList {
public:
struct ListNode {
int val;
ListNode* next;
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
MyLinkedList() {
_size = 0;
_virtHead = new ListNode(0, nullptr);
}
int get(int index) {
if (index < 0 || index > _size - 1) {
return -1;
}
ListNode* cur = _virtHead->next;
while (index--) {
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
ListNode* newHead = new ListNode(val, _virtHead->next);
_virtHead->next = newHead;
_size++;
}
void addAtTail(int val) {
ListNode* newTail = new ListNode(val, nullptr);
ListNode* cur = _virtHead;
while (cur->next) {
cur = cur->next;
}
cur->next = newTail;
_size++;
}
void addAtIndex(int index, int val) {
if (index > _size) {
return;
}
index = max(index, 0);
ListNode *cur = _virtHead;
while (index--) {
cur = cur->next;
}
ListNode *newNode = new ListNode(val, cur->next);
cur->next = newNode;
_size++;
}
void deleteAtIndex(int index) {
if (index < 0 || index > _size - 1) {
return;
}
ListNode* cur = _virtHead;
while (index--) {
cur = cur->next;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
_size--;
return;
}
private:
int _size;
ListNode* _virtHead;
};
/**
* 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);
*/
每个函数实现的方法可以概括为首先判断index合法性,再建立cur,再根据index右移cur一定次数,再添加/删除,最后更改链表长度变量。学到的点在于在类中设置private变量并用初始化函数对其进行初始化。
二刷:deleteAtIndex()中,题目要求在index前插入,那么index是可以等于_size的(从0开始计数),即便第_size个节点不存在。
第3题(206.反转链表)的有递归与非递归两种方法。
非递归的思路是使用左右双指针,不断将右边节点指向左边,再将左右指针右移。最开始写了虚拟头节点,将左右指针分别初始化为虚拟头节点和头节点,但发现这一题目并不需要虚拟头节点,因为要让原始头节点变成尾部,即指向nullptr。所以修改为
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *l = head, *r = head->next;
head->next = nullptr;
while (r) {
ListNode* tmp = r->next;
r->next = l;
l = r;
r = tmp;
}
return l;
}
};
示例正确通过但未AC,报错为空指针错误。找到原因在于有可能原始头节点本身就为空,所以r初始化时就会出现空指针错误。这里再次说明访问一个指针指向内容时,一定要保证该指针非空。于是改正的做法是在一开始判断head是否为空,为空则直接返回head,或将l,r分别设置为空,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 *l = nullptr, *r = head;
while (r) {
ListNode* tmp = r->next;
r->next = l;
l = r;
r = tmp;
}
return l;
}
};
所以这一题非递归解法的错误都发生在左右两个指针的初始化上,前2次设置错误都有考虑不周。
而对于递归解法,刚开始一直碍于函数参数只有一个而不知如何递归,浅看了题解后才意识到可以自己定义一个递归函数,在默认函数里进入递归函数的入口即可。写完后又出现空指针错误,原来是因为忘记写递归函数的出口,也就是判断r是否为空,为空则返回l。忘记这个就会导致r->next出现空指针错误,更改后AC。
// 从前往后反转的递归
class Solution {
public:
ListNode* reverse(ListNode* l, ListNode* r) {
// if (r == nullptr) {
if (!r) {
return l;
}
ListNode* tmp = r->next;
r->next = l;
return reverse(r, tmp);
}
ListNode* reverseList(ListNode* head) {
return reverse(nullptr, head);
}
};
另外之后需要学习下NULL, nullptr的区别。
令人没想到的是还没结束,竟还真有仅需要一个ListNode*参数的递归写法,即从后往前反转链表。自己想不到,纯粹看的题解如下
// 从后往前反转的递归
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}
ListNode *last = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return last;
}
};
思路是不断递归到链表末尾,再将末尾节点的next指向倒数第2个节点,倒数第2个节点的next再指向空,再退出当前递归函数,返回上一层递归函数,从后往前前进一个节点进行相同操作,如此循环往复直到第1层递归。同样有一些需要注意的地方:
- 首先要设定递归出口,其中head本身为空也要有,因为同前面非递归中提到的,head本身可能是空的。
- 其次题目要求返回新链表的head,即旧链表的末尾,所以在递归出口和最后返回值处应分别返回head和last。
- 最后就是递归这一行(第8行)的位置只能是这里,不能放在第9行或第10行之后,因为那样的话head->next->next或head->next已经发生改变,递归过程无法顺利到达链表末尾。
二刷:
从后往前反转的递归:
- 测试用例为空时,要直接返回空;
- 返回值是原本链表的最后一个节点,不是第一个节点;
- 设置head->next->next = head后,还要设置head->next = nullptr,否则原链表的第一个节点处就会形成环。