目录
嵌入式学习分享个人主页:Orion嵌入式随想录 - 小红书 (xiaohongshu.com)
203.移除链表元素
-
文章讲解:代码随想录
原链表删除
-
链表由一系列节点组成,每个节点包含两部分:一部分是存储数据的元素,另一部分是一个指针,指向该节点所在链表中的下一个节点。
-
链表中的节点不必在内存中连续存储,这意味着它们可以分布在内存的任意位置,通过指针相互连接起来。
-
如果C++移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过内存使用的空间大一些而已。
-
解题思路
-
让节点next指针直接指向下下一个节点.
-
移除头结点和移除其他节点的操作是不一样的,链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。将头结点向后移动一位就可以从链表中移除了一个头结点。
-
-
解题步骤
-
删除头节点:判断头节点不为空,且头节点元素为删除数值,则指针指向下一个
-
删除非头节点:判断cur与curnext均不为空,且curnext节点元素为删除数值,则cur指向curnextnext
-
-
代码注意
-
ListNode* removeElements(ListNode* head, int val)函数
-
delete tmp;注意释放内存
-
-
代码一:原链表
// 时间复杂度: 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;
}
};
虚拟头节点
-
解题思路
-
以统一的逻辑来移除链表的节点
-
设置一个虚拟头结点,原链表的所有节点就都可以按照统一的方式进行移除
-
return 头结点的时候,别忘了
return dummyNode->next;
, 这才是新的头结点
-
-
解题步骤
-
new新节点dummy,指向头节点
-
建立临时指针cur指向dummy遍历列表,(要删的元素都是遍历节点cur的next)
-
更新curnext为curnextext 并删除临时节点指向的内存
-
没找到值则更新cur
-
Return 新节点的next
-
-
代码注意
-
cur->next != NULL
-
ListNode* tmp = cur->next;注意要删除前一定建立临时节点temp并删除内存
-
cur->next !=NULL,不是cur
-
需要更新cur!!
-
强调head = dummyHead->next;旧头节点可能被删除了,需要赋值dummyHead->next,或者直接return dummyHead->next;
-
注意要在循环结束后再删除虚拟头节点
-
-
代码二:虚拟头节点
// 时间复杂度: O(n)
// 空间复杂度: O(1)
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;
}
};
707.设计链表 ⭐
-
文章讲解:代码随想录
虚拟头节点
注意第n个节点的定义,原头节点head对应n=0。
定义了一个名为
LinkedNode
的链表节点结构体,它包含以下三个部分:
数据成员
int val;
:用于存储节点的值,类型为int
。指针成员
LinkedNode* next;
:指向同一链表中的下一个LinkedNode
节点。这个指针初始时应该指向nullptr
,表示链表的结束或当前节点是链表的最后一个节点。构造函数
LinkedNode(int val)
:一个带参数的构造函数,它接收一个int
类型的值,将其用于初始化节点的val
成员变量,并将next
指针初始化为nullptr
。这个结构体的定义允许您创建具有特定整数值的链表节点,并且确保每个新节点的
next
指针都正确地初始化为nullptr
,这样在构建链表时就不会有悬挂指针。在下面示例中,首先创建了三个
LinkedNode
对象,并将它们通过next
指针连接起来形成链表。然后,遍历链表并打印每个节点的值。最后释放链表占用的内存,以避免内存泄漏。在实际应用中,还需要考虑更多的错误处理和内存管理问题。
#include <iostream>
// 定义链表节点的结构体
struct LinkedNode {
int val; // 当前节点存储的数据值
LinkedNode* next; // 指向链表中下一个节点的指针
// 构造函数,用于创建带有特定整数值的节点
// 并初始化其指向下一个节点的指针为nullptr
LinkedNode(int val) : val(val), next(nullptr) {}
};
int main() {
// 使用new动态创建三个链表节点,每个节点存储一个整数值
LinkedNode* node1 = new LinkedNode(1);
LinkedNode* node2 = new LinkedNode(2);
LinkedNode* node3 = new LinkedNode(3);
// 通过next指针将节点连接起来形成链表:1 -> 2 -> 3
node1->next = node2; // node1的下一个节点是node2
node2->next = node3; // node2的下一个节点是node3
// 初始化一个指针用于遍历链表
LinkedNode* current = node1;
// 遍历并打印链表中的每个节点的值
while (current != nullptr) {
std::cout << current->val << " "; // 打印当前节点的值
current = current->next; // 移动到链表中的下一个节点
}
// 打印完成,输出一个换行符
std::cout << std::endl;
// 释放链表占用的内存
// 从头节点开始,逐个删除节点直到链表为空
current = node1;
while (current != nullptr) {
LinkedNode* next = current->next; // 暂存下一个节点的地址
delete current; // 删除当前节点,释放内存
current = next; // 移动到下一个节点继续删除
}
// 设置current指针为nullptr,避免悬挂指针问题
current = nullptr;
return 0; // 程序结束,返回0表示正常退出
}
-
解题思路
-
删除链表节点:
-
添加链表节点:
-
查找节点:临时指针遍历,找的是值,直接指向要找的
-
头部插入节点:在虚拟节点和实际头节点中插入。注意先让newnode指向head,再让dummy指向newnode。即先连上再断开。
-
尾部插入节点:cur遍历到尾节点,直接指向newnode。
-
第n个节点前插入节点:注意是dummy设为cur。因为在第n个之前插入。需要知道前一个节点更新指向。同时注意先连上后断开。
-
删除第n个节点:注意是dummy设为cur。因为在第n个后删除。需要知道前一个节点更新指向。
-
-
解题步骤
-
初始化虚拟节点和长度
-
查找就是cur为head
-
增加删除节点都对应cur->next
-
增加要先连后断
-
删除要释放内存
-
增删要改长度size
-
-
代码注意
-
LinkedNode(int val):val(val), next(nullptr){}析构函数初始化
-
成员对象放最后
-
注意判断索引
-
-
代码一:虚拟节点
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);
LinkedNode* cur = _dummyHead;
while(index--) {
cur = cur->next;
}
newNode->next = cur->next;//按照n=0思考
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;
};
206.反转链表
-
文章讲解:代码随想录
双指针法 ⭐
-
解题思路
-
变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表
-
链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。
-
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
-
开始反转,首先把 cur->next 节点用tmp指针保存一下。(因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。)
-
开始循环走,继续移动pre和cur指针。
-
最后cur 指针指向null,循环结束,链表反转完毕。 return pre指针,pre指针指向新的头结点。
-
-
解题步骤
-
初始化双指针
-
设定临时指针
-
cur没到null则循环
-
循环内:临时指针保存下一个,断开cur指向pre
-
更新pre、cur
-
返回pre
-
-
代码注意
-
cur->next = pre; 翻转是赋值操作,cur->next只表示指向,不表示动作
-
-
代码一:双指针法 ⭐
// 时间复杂度: O(n)
// 空间复杂度: O(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;
}
};
-
代码二:反向双指针法
-
递归写法和双指针法实质上都是从前往后翻转指针指向,其实还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。
-
// 时间复杂度: O(n)
// 空间复杂度: O(n)
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;
}
};
递归法
-
解题思路
-
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
-
关键是初始化 cur = head,pre = NULL与双指针相同,只不过写法变了。
-
-
代码三:递归法(说明:基于代码随想录课程学习,部分内容引用自代码随想录文章)
// 时间复杂度: O(n)
// 空间复杂度: O(n)
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);
}
};
(说明:基于代码随想录课程学习,部分内容引用自代码随想录文章)