Leecode 203 移除链表元素

题目描述

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

  • 示例1

    • 输入:head = [1,2,6,3,4,5,6], val = 6
    • 输出:[1,2,3,4,5]
  • 示例 2:

    • 输入:head = [], val = 1
    • 输出:[]
  • 示例 3:

    • 输入:head = [7,7,7,7], val = 7
    • 输出:[]

解法1 单次遍历删除

本题要求删除链表中制定取值的所有节点,而链表中本身就是有删除操作定义的,即删除制定位置的元素。此时需要从链表头开始遍历走到这个节点的上一个节点,并将指针指向待删除节点的下一个节点,同时释放待删除节点的内存。所以对于本题有一个非常自然的算法思路:

  • 从链表的头到尾进行遍历,同时判断当前节点的值是否与输入目标值相等

    • 如果值相等,则调用链表的删除操作删去这个节点
    • 如果值不相等,则遍历下一个节点

      这个算法思路非常简单而自然,我们可以大致估算一下他的时间复杂度。每次进行删除操作需要从头节点开始遍历,故单次删除的时间复杂度为\(O(n)\),而从头到尾遍历有可能删除\(n\)个数量级的节点,即调用\(n\)次\(O(n)\)复杂度的操作,故该算法时间复杂度为\(O(n^2)\)。虽然这个时间复杂度较大,但是起码也是能够解决问题。接下来我们继续考虑能否仅用一次遍历就能完成整个操作。

一个降低时间复杂度的思想是:逐个遍历整个链表,遍历的同时做判断。如果需要删除,则直接在原地删除,而不必像刚才所说的调用删除操作还需要从头开始遍历一遍。但同时我们注意到,链表的删除操作需要能够拿到待删除节点的前一个节点,将前一个节点的next指向待删除节点的下一个节点,并释放待删除节点的内存。为了能够拿到待删除节点的上一个节点,那么我们可以有两种选择:

  • 使用双指针法一起遍历链表,一个指针指向当前节点,另一个指针指向当前节点的上一个节点;如果当前节点需要删除,则可以利用上一个节点的指针。
  • 还是使用一个指针来进行遍历,但是每次判断使用该指针的next节点的val值,如果next节点需要删除,那么就将该指针的next指向原本next节点的next节点。但这种方法需要特别注意边界检查,因为如果遍历到了最后一个节点,此时的next节点为nullptr,而如果再调用此时“next节点的val值”,则有可能造成异常。

上面提到的这两种算法都是只用了一次遍历,在遍历的同时进行时间复杂度为\(O(1)\)的删除操作,故这两种算法的时间复杂度都是\(O(n)\)。

同时对于上面这两种算法都需要注意,如何处理第一个节点需要删除的情况。为此我的解决方案是先暂时不处理第一个节点,删除除第一个节点以外后续需要删除的所有节点之后,再来判断头节点是否需要删除。当然也可以使用虚拟节点的方式来表示头节点,这样删除每一个节点的操作都是一模一样的了。

在这里我们给出仅用一个指针的单次遍历删除的代码:

class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == nullptr){ // 处理空链表的特殊情况,避免后续直接访问头节点的next出现异常
return nullptr;
}
ListNode* cur = head; // 从当前节点初始化 while(cur->next != nullptr){ // 如果下一个节点非空,则需要进行判断
if(cur->next->val == val){ // 若下一个节点需要删除
ListNode* toDelete = cur->next; // 进行删除操作
cur->next = cur->next->next;
delete toDelete;
}
else{
cur = cur->next; // 如果不用删除,才遍历到下一个节点
}
}
if(head->val == val){ // 最后来处理头节点是否需要删除
ListNode* newHead = head->next;
delete head;
return newHead;
}
return head;
}
};

解法2 使用递归

本题也可以考虑使用递归的方式来求解。我们可以节整条链表看做是两部分,左边一部分看做是已经删除过待删除元素的链表,而右侧则是还未处理过的链表。初始情况相当于左侧已经处理过的链表长度为0,而右侧未处理的则是原始输入的链表。每一次对右侧链表进行操作,可以看做是一模一样的问题,可以直接通过调用递归函数来进行递归。由此我们可以得到下面的递归算法框架:

  • 递归函数(链表头节点,待删除的值)

    • 递归终止条件:如果当前头节点已经为空,说明已经删除完毕,直接返回
    • 判断头节点的情况来进行处理,并调用递归函数:
      • 如果当前头节点需要删除,则执行删除操作,并调用递归,使下一个节点作为后续链表的头节点
      • 如果当前节点不需要删除,则直接调用递归,使下一个节点作为后续链表的头节点

通过上面的递归算法,就可以很轻松地解决这个问题,我们可以得到代码如下:

class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
if(head == nullptr){ // 递归终止条件,如果头节点已经为空,说明待处理的链表已经为空,直接返回
return nullptr;
}
if(head->val == val){ // 如果当前头节点需要删除
ListNode* newNode = head->next; // 则进行删除操作
delete head;
return removeElements(newNode,val); // 并返回下一个节点作为新的头节点
}
else{ // 如果当前头节点不需要删除
head->next = removeElements(head->next,val); // 则调用递归,令下一个节点作为后续已经处理过的链表的头节点
return head; // 返回头节点
}
}
};

我们可以分析这段递归代码的时间复杂度,其实就是对每一个节点都当做头节点来进行了一次判断,并处理相应操作。其实也就相当于将整个链表中的节点都遍历了一次,故时间复杂度为\(O(n)\)。

Leecode 707 设计链表

题目描述

你可以选择使用单链表或者双链表,设计并实现自己的链表。

单链表中的节点应该具备两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。

如果是双向链表,则还需要属性 prev 以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。

实现 MyLinkedList 类:

  • MyLinkedList() 初始化 MyLinkedList 对象。

  • int get(int index) 获取链表中下标为 index 的节点的值。如果下标无效,则返回 -1

  • void addAtHead(int val) 将一个值为 val 的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。

  • void addAtTail(int val) 将一个值为 val 的节点追加到链表中作为链表的最后一个元素。

  • void addAtIndex(int index, int val) 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。如果 index 等于链表的长度,那么该节点会被追加到链表的末尾。如果 index 比长度更大,该节点将 不会插入 到链表中。

  • void deleteAtIndex(int index) 如果下标有效,则删除链表中下标为 index 的节点。

  • 示例:

    • 输入["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
    • [[], [1], [3], [1, 2], [1], [1], [1]]
    • 输出[null, null, null, null, 2, null, 3]
  • 解释:

    • MyLinkedList myLinkedList = new MyLinkedList();
    • myLinkedList.addAtHead(1);
    • myLinkedList.addAtTail(3);
    • myLinkedList.addAtIndex(1, 2); // 链表变为 1->2->3
    • myLinkedList.get(1); // 返回 2
    • myLinkedList.deleteAtIndex(1); // 现在,链表变为 1->3
    • myLinkedList.get(1); // 返回 3

题目思路

很无聊的一道题,考虑使用一个虚拟头节点来建立链表。同时逐个写完这些运算即可,没太多好说的,下面直接给出代码

class MyLinkedList {
private:
struct LinkNode {
int val;
LinkNode* next;
LinkNode(int x) : val(x), next(nullptr) {}
}; LinkNode* _dummyHead; // 虚拟头节点
int _size; // 链表长度 public:
// 构造函数:初始化虚拟头节点和长度
MyLinkedList() : _dummyHead(new LinkNode(0)), _size(0) {} // 析构函数:释放所有节点
~MyLinkedList() {
LinkNode* cur = _dummyHead;
while (cur != nullptr) {
LinkNode* tmp = cur;
cur = cur->next;
delete tmp;
}
} int get(int index) {
if (index < 0 || index >= _size) return -1; // 处理找不到的情况
LinkNode* cur = _dummyHead->next; // 从头节点的下一个节点开始
for (int i = 0; i < index; i++) { // 查找到相应序号的节点
cur = cur->next;
}
return cur->val;
} void addAtHead(int val) {
LinkNode* newNode = new LinkNode(val); // 新建节点并初始化
newNode->next = _dummyHead->next; // 新节点指向原本头节点
_dummyHead->next = newNode; // 虚拟头节点指向新节点
_size++; // 更新链表长度
} void addAtTail(int val) {
LinkNode* cur = _dummyHead; // 新建一个节点指针
while (cur->next != nullptr) { // 遍历直至最后一个节点
cur = cur->next;
}
cur->next = new LinkNode(val); // 在最后插入一个节点
_size++; // 链表长度+1
} void addAtIndex(int index, int val) {
if (index > _size) return; // 处理如果找不到该序号的异常情况,直接返回
LinkNode* cur = _dummyHead; // 新建一个指针,注意此时是在虚拟头节点处
for (int i = 0; i < index; i++) { // 从虚拟头节点往后走index步,此时cur指向index个节点的前一个节点
cur = cur->next;
}
LinkNode* newNode = new LinkNode(val); // 新建一个节点
newNode->next = cur->next; // 新节点指向链表后续的元素
cur->next = newNode; // 用cur指向新节点(注意cur是index节点的前一个节点)
_size++; // 链表长度+1
} void deleteAtIndex(int index) {
if (index < 0 || index >= _size) return; // 处理输入异常情况,直接返回
LinkNode* cur = _dummyHead; // cur从虚拟头节点开始
for (int i = 0; i < index; i++) { // cur往后遍历index步,指向节点为index节点的前一个节点
cur = cur->next;
}
LinkNode* toDelete = cur->next; // 删除index节点
cur->next = cur->next->next;
delete toDelete;
_size--; // 链表长度-1
}
};

本题并没有太多思维量,但是比较麻烦(毕竟要写这么多函数)。个人感觉最难的地方在于在类中建立struct结构,写构造函数和析构函数,以及对成员变量的声明。由于对面向对象还不是很熟练,所以其实还花了不少时间在设置变量这上面。可能还有一个难点在于如果使用虚拟头节点的情况下,在index处插入、删除需要遍历多少步;只要注意从虚拟头节点出发需要多走一步,同时要对index处的节点进行操作需要少走一步到其前一个节点;同时考虑这两个点那就不会错了。

Leecode 206 反转链表

题目描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

  • 示例 1:

    • 输入:head = [1,2,3,4,5]
    • 输出:[5,4,3,2,1]
  • 示例 2:

    • 输入:head = [1,2]
    • 输出:[2,1]
  • 示例 3:

    • 输入:head = []
    • 输出:[]

解法1 双指针遍历翻转(或者应该叫三指针?)

这道题我第一反应的算法是要新建一个链表,每次遍历取出当前链表的最后一个元素,然后插入到新链表中作为头节点。这种算法当然可以解决这个问题,但是新建链表需要消耗额外的空间。同时每一次取最后一个节点的操作的时间复杂度为\(O(n)\),那么依次取出所有的\(n\)个节点插入新链表中,所用的总时间复杂度就应该是\(O(n^2)\)。所以这当然是一个很差的算法。为此我们需要考虑能够仅用多个指针一次遍历就能达到我们的目标。

首先,链表麻烦的一个点在于,每个节点只能取到自己的下一个节点。而如果需要对其进行增、删操作,都需要该节点的上一个节点的参与。而如果每次都从头遍历一次直至当前节点的上一个节点,那么显然这个过程会消耗很多无用的时间复杂度。一个自然的想法就是同时用多个指针进行遍历,那么就可以同时取到链表中节点的上一个节点了。同时,对于本题而言,翻转链表也没有必要重新建一个链表,每个节点只需“原地掉头”即可。而这个“掉头”操作其实只需要将其自身的指针指向原本的上一个节点即可,但是为了进行这个“掉头”操作,我们还需要一些额外的注意事项:

  • 对节点进行掉头,即直接将节点的指针指向其上一个节点,需要:

    • 有指向当前节点的指针cur
    • 有一个指向原本上一个节点的指针pre,这样才能直接修改当前节点的指针(否则必须从头进行一次\(O(n)\)复杂度的遍历)
    • 有一个指向原本下一个节点的指针temp,这样才能使得后续的链表不丢失,还能继续访问。也方便后续继续进行遍历

根据上面分析可知,要想完成“原地掉头的操作”,我们需要三个指针,同时具体的掉头演示动画如下所示(其实我做这道题花的时间最久的在画下面这个动图上):



相信根据上面这个演示动画能够很容易地看出链表中每个元素依次“掉头”的过程,同时我们可以给出具体的代码如下所示

class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr; // 记录上一个节点
ListNode* cur = head; // 记录当前节点
ListNode* temp; // 记录下一个节点,用于使得节点cur返回原链表 while(cur){
temp = cur->next; // 记录下一个节点
cur->next = pre; // 节点“掉头”
pre = cur; // pre和cur往下一个节点遍历
cur = temp;
}
return pre; // 返回已经翻转完成的链表的头节点
}
};

上面代码对照着图一起看还是很清晰的,代码中的每一步操作都对应了图中的一次移动。

解法2 递归法

(今天花太多时间学怎么画图了,这部分等之后有空再来补)

今日总结

感觉今天最大的收获就是学会了怎么用python里的manim来画图,虽然绘制上面图像的主要代码都是让AI辅助着写的,但是AI输出的结果还是一直都有一些错误。没办法我只能一点一点理解这个完全没用过的manim包里的方法。花了好几个小时才慢慢调对代码最后画出来了这版3b1b风格的动画hh。不得不说这个图画出来的感觉比我刷出算法题爽多了,还是挺有成就感的。

p.s. 通过今天画的这个图,我现在感觉我这辈子都忘不了翻转链表需要哪几步操作了。。。

代码随想录第三天 | Leecode 203. 移除链表元素、707. 设计链表、206. 翻转链表的更多相关文章

  1. 代码随想录训练营day 4|链表基础理论,移除链表元素,设计链表,反转链表

    链表理论基础 链表是一种由指针串联在一起的线性结构,每一个节点都由一个数据域和一个指针域组成. 链表的类型有:单链表.双链表.循环链表. 链表的存储方式:在内存中不连续分布. 链表的定义很多人因为不重 ...

  2. 代码随想录算法训练营day03 | LeetCode 203/707/206

    基础知识 数据结构初始化 // 链表节点定义 public class ListNode { // 结点的值 int val; // 下一个结点 ListNode next; // 节点的构造函数(无 ...

  3. 代码随想录训练营day 1 |704 二分查找 27移除算法

    LeetCode 704.二分查找(C++) 题目链接 704.二分查找 题目描述:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 ...

  4. 代码随想录-day1

    链表 今天主要是把链表专题刷完了,链表专题的题目不是很难,基本都是考察对链表的操作的理解. 在处理链表问题的时候,我们通常会引入一个哨兵节点(dummy),dummy节点指向原链表的头结点.这样,当我 ...

  5. 代码随想录第十三天 | 150. 逆波兰表达式求值、239. 滑动窗口最大值、347.前 K 个高频元素

    第一题150. 逆波兰表达式求值 根据 逆波兰表示法,求表达式的值. 有效的算符包括 +.-.*./ .每个运算对象可以是整数,也可以是另一个逆波兰表达式. 注意 两个整数之间的除法只保留整数部分. ...

  6. [LeetCode] 203. 移除链表元素(链表基本操作-删除)、876. 链表的中间结点(链表基本操作-找中间结点)

    题目 203. 移除链表元素 删除链表中等于给定值 val 的所有节点. 题解 删除结点:要注意虚拟头节点. 代码 class Solution { public ListNode removeEle ...

  7. 【LeetCode】203.移除链表元素

    203.移除链表元素 知识点:链表:双指针 题目描述 给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 . 示例 ...

  8. 代码随想录第八天 |344.反转字符串 、541. 反转字符串II、剑指Offer 05.替换空格 、151.翻转字符串里的单词 、剑指Offer58-II.左旋转字符串

    第一题344.反转字符串 编写一个函数,其作用是将输入的字符串反转过来.输入字符串以字符数组 s 的形式给出. 不要给另外的数组分配额外的空间,你必须原地修改输入数组.使用 O(1) 的额外空间解决这 ...

  9. 代码随想录 day0 博客怎么写

    前言 2.25日开始记录自己的博客生涯以及代码随想录训练营的每日内容 一.题目链接怎么找?怎么设置连接? 力扣题目链接1:力扣 二.正文怎么写? 二分查找 算法思路: 二分查找需要保证数组为有序数组同 ...

  10. .net之工作流工程展示及代码分享(三)数据存储引擎

    数据存储引擎是本项目里比较有特色的模块. 特色一,使用接口来对应不同的数据库.数据库可以是Oracle.Sqlserver.MogoDB.甚至是XML文件.采用接口进行对应: public inter ...

随机推荐

  1. 接口响应指标的p99、p95、p50到底是什么?

    一.简介 我们对服务响应时间的衡量指标有Min(最小响应时间).Max(最大响应时间).Avg(平均响应时间)等,P99.P90也是衡量指标 二.指标简介 1.平均值Avg 其中比较常用的值就是平均值 ...

  2. USACO24DEC Cake Game S 题解 [ 黄 ] [ 前缀和 ] [ adhoc ]

    Cake Game:小清新前缀和题,但是我场上想了半天优先队列贪心假完了 /ll/ll/ll. 观察 本题有三个重要的结论,我们依次进行观察. 不难发现,第二个牛一定会拿 \(\frac{n}{2}- ...

  3. Luogu P1777 帮助 题解 [ 紫 ] [ 线性 dp ] [ 状压 dp ]

    帮助:大毒瘤!!!调了我2h,拍了我2h,最后没调出来,重写才AC.wdnmd. 思路 这题主要是线性 dp ,而状压 dp 只是最后在统计答案时的一个辅助. 首先定义 \(dp[i][j][k]\) ...

  4. WinForm 多线程+委托来防止界面假死

    参考: http://www.cnblogs.com/xpvincent/archive/2013/08/19/3268001.html 当有大量数据需要计算.显示在界面或者调用sleep函数时,容易 ...

  5. 阿里云Windows server 2016服务器Antimalware Service Executable进程占比高,cpu接近100%,强制关闭该进程实测

    问题描述:阿里云Windows server 2016服务器Antimalware Service Executable进程占比高,cpu接近100%,需要强制关闭该进程,排查问题,进入系统服务关闭, ...

  6. 赶快检查,木马可能已经植入服务器,Redis未授权访问漏洞记录,redis的key值出现backup要谨慎

    问题描述:为图省事,很多时候我们在使用redis的时候会使用默认空密码,这就增加了安全隐患,如果有下属情况,那赶快去检查下redis,木马或许已经植入服务器,应尽快处理: 1.redis绑定在 0.0 ...

  7. 【编程思维】临近实施 WPF 下拉框闪烁问题!!

    私以为架构是业务开发的发展历史,顺应大方向而生,再为贴切时刻的用户需求,持续微改动. 我本以为了解这个软件的架构没甚意思,加快的开发速度不能过渡到下一个别的软件去: 却不知以小窥大,关键还是计算机思维 ...

  8. 【Python】一键提取inp文件结构的脚本

    inp=input("输入文件路径:") # print(type(inp)) ex_txt=inp+'-Struct.inp' inp=inp+'.inp' import re ...

  9. surpac 中如何删除点

    找到显示的编号 输入线窜线段编号

  10. 解决kali虚拟机无法联网问题

    解决kali虚拟机无法联网问题 1.排查虚拟机网络连接-检查ipv4设置,确定好手动连接还是DHCP 如图一 2.排查虚拟网络编辑器-网卡配置,确定虚拟机直连外部网络是否为同一网口 如图二 3.排查虚 ...