今日任务:
Part_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) {}
};
根据卡哥提示,由于力扣中已经给出如上节点定义,所以平时刷题时确实可能会忽略这部分。下面引用卡哥关于此话题的原文(感谢卡哥):
“有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。
但是这个构造函数不会初始化任何成员变量,下面我来举两个例子:
通过自己定义构造函数初始化节点:
ListNode* head = new ListNode(5);
使用默认构造函数初始化节点:
ListNode* head = new ListNode();
head->val = 5;
所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值!”
由于涉及本人还没有掌握的构造函数,这里将卡哥的原文直接搬过来以供学习。近期也在开始准备写一些C++_Primer第五版的学习笔记,掌握构造函数后再来还愿。
2.链表的基本操作(见part_3拓展)(后续遇到继续补充)
Part_2 力扣203.移除链表元素
题目描述:
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/remove-linked-list-elements
个人解答:
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
while (head && head->val == val) {
ListNode* tmp = head;
head = head->next;
delete tmp;
}
if (!head) return head;
ListNode* p = head, * q = head->next;
while (q) {
if (q->val != val) {
p = p->next;
q = q->next;
}
else {
ListNode* tmp = q;
p->next = q->next;
q = q->next;
delete tmp;
}
}
return head;
}
};
(1)双指针法比较典型
(2)C/C++需要delete,根据卡哥提示,“使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养成手动清理内存的习惯。”
(3)此外,稍微值得一提的是前面的特殊情况分析。
while (head && head -> val == val) head = head->next;
if (!head) return head;
正常思维顺序应该会先写出第二行,即判断空链表。然后由于双指针对于头元素的特殊性,判断并迭代删除头元素可能会造成链表为空,继而呈现如上两行代码。
(4)链表的解答大多包括是否使用虚拟头结点_dummyhead两种,个人解答为不使用,下面引用卡哥的使用虚拟节点的解答作为补充。
卡哥解答:(代码引用自力扣评论,感谢卡哥)
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;
}
};
Part_3 力扣707.设计链表
题目描述:
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。
在链表类中实现这些功能:
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/design-linked-list
个人解答(包含拓展的函数,自行提取力扣所需):
#include<iostream>
using namespace std;
class MyLinkedList {
public:
struct LinkedNode {
int val;
LinkedNode* next;
LinkedNode(int x) : val(x), next(nullptr) {}
};
MyLinkedList() {
_dummyHead = new LinkedNode(0);
_size = 0;
}
int get(int index) {
if (index > (_size - 1) || index < 0) return -1;
LinkedNode* p = _dummyHead->next;
while (index--) p = p->next;
return p->val;
}
void addAtHead(int val) {
LinkedNode* p = new LinkedNode(val);
p->next= _dummyHead->next;
_dummyHead->next = p;
++_size;
}
void addAtTail(int val) {
LinkedNode* q = new LinkedNode(val);
LinkedNode* p = _dummyHead;
while (p->next)p = p->next;
p->next = q;
++_size;
}
void addAtIndex(int index, int val) {
if (index > _size || index < 0) return;
LinkedNode* q = new LinkedNode(val);
LinkedNode* p = _dummyHead;
while (index--)p = p->next;
q->next = p->next;
p->next = q;
++_size;
}
void deleteAtIndex(int index) {
if (index > (_size - 1) || index < 0) return ;
LinkedNode* p = _dummyHead;
while (index--) p = p->next;
LinkedNode* q = p->next;
p->next = p->next->next;
delete q;
--_size;
}
void printList() {
LinkedNode* p = _dummyHead->next;
while (p) {
cout << p->val<<' ';
p = p->next;
}
cout << endl;
}
void reverseList() {
LinkedNode* head = _dummyHead->next;
if (!head || !head->next) printList();
else if (!head->next->next) {
LinkedNode* p = _dummyHead->next->next;
p->next = _dummyHead->next;
_dummyHead->next->next = NULL;
_dummyHead->next = p;
printList();
}
else {
LinkedNode* p = head, * q = p->next, * r = q->next;
p->next = NULL;
while (r) {
q->next = p;
p = q;
q = r;
r = r->next;
}
_dummyHead->next = q;
_dummyHead->next->next = p;
printList();
}
}
private:
int _size;
LinkedNode* _dummyHead;
};
int main() {
MyLinkedList* obj = new MyLinkedList();
obj->addAtTail(1);
obj->addAtTail(2);
obj->addAtTail(3);
obj->addAtTail(4);
obj->addAtTail(5);
obj->printList();
obj->reverseList();
}
//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);
代码结尾main()部分为使用测试。
此外,比较值得注意的是开始的节点位置,观察结果如下:
LinkedNode* p = _dummyHead;//一般用于双指针的slow指针
LinkedNode* p = _dummyHead->next;//一般用于单指针
本题很大程度上满足了链表学习的实际应用,打破以往纯理论的算法学习方式,同时也可以了解C++面向对象的类模板,很适合学习。
Part_4 力扣206.反转链表
题目描述:
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/reverse-linked-list
个人解答:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head||!head->next) return head;
else if (!head->next->next) {
head->next->next = head;
head = head->next;
head->next->next = NULL;
return head;
}
else {
ListNode* p = head, * q = p->next, * r = q->next;
p->next=NULL;
while (r) {
q->next = p;
p = q;
q = r;
r = r->next;
}
head = q;
head->next = p;
return head;
}
}
};
为了致敬双指针法,这种方法暂且简称为三指针法。
(1)三指针法每次调换p与q之间的箭头指向,借用r指针实现位置移动的迭代。
(2)与双指针法可能需要单独讨论首尾类似,三指针法需要讨论链表总长不足三个节点的情况,结束条件需要选取r,即最快的指针,以免出现NULL->next的情况。
(3)结束while循环时,p对于倒数第二节点,q对应最后一个节点,r对应NULL,因此手动再将head对应q,head->next对应p
(4)细心的各位可能已经注意到翻转链表在part_3设计链表中已经作为拓展函数出现过,不过由于_dummyhead和head的定义差异,在拓展函数中做出了一些变量定义调整以适配_dummyhead,实际并无差异,且在part_3中实测可用。
(5)三指针法称不上是高效,在解答过程中本人也感觉到似乎可用省去一个指针,不过其思路易于理解,虽非首创,但却是自己独立构思实现的,而且确实有可取之处,已经可以满足本人刚刚入门以及初刷随想录和力扣的预期。
下面还是欣赏下卡哥的解答:
(1)双指针法(保存、翻转、更新):
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;
}
};
(2)正向递归:
class Solution {
public:
ListNode* reverse(ListNode* pre,ListNode* cur){
if(cur == NULL) return pre;
ListNode* temp = cur->next;
cur->next = pre;
// 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
// pre = cur;
// cur = temp;
return reverse(cur,temp);
}
ListNode* reverseList(ListNode* head) {
// 和双指针法初始化是一样的逻辑
// ListNode* cur = head;
// ListNode* pre = NULL;
return reverse(NULL, head);
}
};
其中,保存、翻转同上双指针法,移位通过cur和tmp=cur->next实现
(3)反向递归:
class Solution {
public:
ListNode* reverseList(ListNode* head) {
// 边缘条件判断
if(head == NULL) return NULL;
if (head->next == NULL) return head;
// 递归调用,翻转第二个节点开始往后的链表
ListNode *last = reverseList(head->next);
// 翻转头节点与第二个节点的指向
head->next->next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head->next = NULL;
return last;
}
};
通过函数自己调用自己实现递归翻转
今日感悟:
今天的三道链表题更多偏向对于理论的实现,并未涉及过多实际场景的应用背景,并且本人曾有过理论学习算法的经历(以前是完全理论学习,不会代码实现),因此今天完成时间终于稍早一些啦,而且今天的收获和完成质量也基本达到预期,很充实的一次打卡,继续加油!