第三天💪(ง •_•)ง💪,编程语言:C++
目录
链表的理论基础
文档讲解:代码随想录链表理论基础
链表是一种通过指针将各节点串联在一起的线性结构。每一个节点一般由两部分组成,一个数据域,一个指针域(存放指向下一个节点的指针)。
特点:·1. 链表最后一个节点的指针域指向NULL(空指针)。2. 链表的入口节点称为链表的头节点head。
链表的类型:链表一般可分为三种类型,单链表、双链表、循环链表。
单链表:
双链表: 每个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
循环链表(可用于解决约瑟夫环问题):链表首尾相连。
链表的存储方式:我们知道数组再内存中是连续分布的,但是链表在内存中不是连续分布的,而是散乱分布在内存中的某个地址上,分配机制取决于操作系统的内存管理。
链表的代码定义(以单链表为例):
// 单链表
struct ListNode {
int val; // 节点上存储的元素
ListNode *next; // 指向下一个节点的指针
ListNode() : val(0), next(NULL) {}
ListNode(int x) : val(x), next(NULL) {}
ListNode(int x, ListNode* next) : val(x), next(next) {} // 节点的构造函数
};
//初始化方式
//第一种
ListNode* head = new ListNode();
head->val = 5;
//第二种
ListNode* head = new ListNode(5);
//第三种
ListNode* tail = new ListNode(4);
ListNode* head = new ListNode(5, tail);
链表的操作:
1.删除节点:链表可以通过截断要删除的节点和整个链表之间的联系,来实现节点的删除。要注意c++中被删除的节点空间不会被释放,需要手动进行空间释放,即delete删除节点。
2.插入节点:链表可以通增加对新节点的联系,来实现节点的插入。
性能分析:
注意:数组定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。但是链表的长度可以是不固定的,并且可以动态增删,适合数据量不固定,频繁增删,较少查询的场景。
203.移除链表元素
文档讲解:代码随想录移除链表元素
视频讲解:手撕移除链表元素
题目:
初看:有两个问题:1.如何遍历找到目标节点;2.如何删除目标节点。通过链表理论基础的学习,我们知道了如何删除目标节点,即让目标节点的前一个节点跳过目标节点指向下一个节点。遍历节点则可以通过给的头节点head依次向下遍历,但要注意,用这种方式如果头节点是目标节点,需要单独处理。
代码:
//时间复杂度:O(n)
//空间复杂度:O(1)
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
// 删除头结点
while (head != NULL && head->val == val) { // 注意这里不是if
ListNode* tmp = head;
head = head->next;
delete tmp;
}
// 删除非头结点
ListNode* cur = head;
while (cur != NULL && cur->next!= NULL) {
if (cur->next->val == val) {
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
} else {
cur = cur->next;
}
}
return head;
}
};
学习:使用头节点遍历的方式,需要单独处理头节点。通过视频学习,此题还可以通过设置一个虚拟头节点的方式,使得头节点变为第二个节点,实现对所有节点的统一处理。
代码:
//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummyhead = new ListNode();
dummyhead->next = head;
ListNode* cur = dummyhead;
while (cur->next != nullptr) {
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;
}
};
收获:
- 链表中元素的遍历,不同于数组能够通过下标直接找到目标元素,而是需要从头节点开始依次遍历。当然如果是双链表,且设置了尾节点,也可以从后往前找。
- 如果需要处理链表中的头节点,也可以通过设置虚拟头节点的方式,使得头节点虚拟的变为第二节点,从而实现对所有节点的统一处理。
707.设计链表
文档讲解:代码随想录设计链表
视频讲解:手撕设计链表
题目:
初看:第一想法是实际上就是需要设计一个类,并且类内需要实现添加,插入,查询,删除的操作。
学习:对于链表来说,链表的操作方式主要就分为两种:1.直接使用原来的链表来进行操作;2.设置一个虚拟头节点再进行操作。因此实际上就是先设计一个节点结构体,然后一一实现各操作。
节点设计:要注意还需要设置一个int型变量,统计链表内节点个数。
class MyLinkedList {
public:
//创建一个单链表结构体
struct ListNode {
int val;
ListNode* next;
ListNode(int val) : val(val), next(nullptr) {}
};
//初始化
MyLinkedList() {
length = 0;
dummyhead = new ListNode(0);
}
private:
//链表长度
int length;
//设置虚拟头节点
ListNode* dummyhead;
};
最终实现:
class MyLinkedList {
public:
//创建一个单链表结构体
struct ListNode {
int val;
ListNode* next;
ListNode(int val) : val(val), next(nullptr) {}
};
MyLinkedList() {
length = 0;
dummyhead = new ListNode(0);
}
int get(int index) {
if (index > length - 1 || index < 0 ) {
return -1;
}
ListNode* cur = dummyhead;
while(index--) {
cur = cur->next;
}
return cur->next->val;
}
void addAtHead(int val) {
ListNode* newNode = new ListNode(val);
newNode->next = dummyhead->next;
dummyhead->next = newNode;
length++;
}
void addAtTail(int val) {
ListNode* newNode = new ListNode(val);
ListNode* cur = dummyhead;
//不能写while(length--)会改变length的值
while(cur->next != nullptr) {
cur = cur->next;
}
cur->next = newNode;
length++;
}
void addAtIndex(int index, int val) {
if (index > length) {
return;
}
if(index < 0) index = 0;
ListNode* newNode = new ListNode(val);
ListNode* cur = dummyhead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;
cur->next = newNode;
length++;
}
void deleteAtIndex(int index) {
if (index > length - 1 || index < 0) {
return;
}
ListNode* cur = dummyhead;
while(index--) {
cur = cur->next;
}
ListNode* tmp = cur->next;
cur->next = cur->next->next;
delete tmp;
tmp = nullptr;
length--;
}
private:
//链表长度
int length;
ListNode* dummyhead;
};
收获:
- 重点考察对于链表遍历,删除,插入的熟悉程度。以及对c++类设计的认识。
206.反转链表
文档讲解:代码随想录反转链表
视频讲解:手撕反转链表
题目:
初看:第一想法是从头节点开始遍历到最后一个节点,然后从最后一个节点开始往前进行处理,但发现由于是单链表的原因,到达最后一个节点后无法回头遍历。而从头节点开始,当头节点指向NULL后,第二个
学习:学习后明白,由于需要反转节点,且该链表为单链表,因此需要两个指针来进行节点反转的处理,同时还需要一个中间指针tmp保存下一个处理节点,才能保证节点遍历下去,而不丢失。
代码:
//时间复杂度O(n)
//空间复杂度O(1)
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* tmp;
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur != nullptr) {
tmp = cur->next;
cur->next = pre;
pre = cur;
cur = tmp;
}
return pre;
}
};
收获:
- 处理链表同样有双指针的方式,当发现一个指针不够处理的时候,要注意是否能够通过双指针来进行节点的保存和处理。
- 在进行链表循环处理的同时,重点要注意链表内的节点不能丢失。与数组相同,同样是坚持循环不变量原则,保证循环能够正常持续下去。
总结
第三天,反转链表未能解出😔😔,但学习到了新方法,继续加油(ง •_•)ง。