算法刷题Day3 | 203.移除链表元素、707.设计链表、206.反转链表

目录

  • 0 容易被大家忽略的问题
  • 1 移除链表元素
    • 1.1 开始的错误解题
    • 1.2 头结点和头指针的区别
      • 1.2.1 区别
      • 1.2.2 思考
    • 1.3 正确的题解
      • 1.3.1 直接移除法
      • 1.3.2 添加虚拟头节点辅助移除法
      • 1.3.3 总结
  • 2 设计链表
    • 2.1 我的解题
  • 3 反转链表
    • 3.1 我的解题
    • 3.2 其他方法(双指针法、递归写法)

请添加图片描述

  • 🙋‍♂️ 作者:海码007
  • 📜 专栏:算法专栏
  • 💥 标题:算法刷题Day3 | 203.移除链表元素、707.设计链表、206.反转链表
  • ❣️ 寄语:书到用时方恨少,事非经过不知难!

0 容易被大家忽略的问题

为什么一些对链表操作的函数,需要新建一个节点指针然后再去对链表操作呢?

例如写了一个遍历链表的函数,传入head指针。如果我们直接对 head 指针进行操作遍历。就需要 head = head→next 然后遍历下一个节点的值。此时就改变了head的指向,等遍历完之后head就变成一个空指针了,然后你发现操作完链表。

1 移除链表元素

  • 🎈 文档讲解:
  • 🎈 视频讲解:
  • 🎈 做题状态:感觉很挫败,角色很简单的链表删除居然没做出来

1.1 开始的错误解题

其实我最开始的解题问题很大:

  • 头结点的操作和其他节点是不一样的,这一点一开始没有思考到
  • 头结点和头指针没有搞清楚,导致后面移动元素时一团糟。

在这里插入图片描述

1.2 头结点和头指针的区别

1.2.1 区别

在链表中,有两个概念:头结点和头指针。

  1. 头结点

    • 头结点是指链表中的第一个实际存储数据的节点。
    • 有些链表的设计中会在头部额外添加一个节点,称为头结点,这个节点不存储任何数据,只是为了方便对链表的操作,例如在删除或插入节点时不需要特殊处理头节点的情况。
    • 头结点并不一定是链表的第一个元素,它可以位于第一个元素之前,也可以位于第一个元素的位置。
  2. 头指针

    • 头指针是指向链表中第一个节点的指针。
    • 头指针是为了能够方便地访问链表,它存储了链表的起始地址,通过头指针可以遍历整个链表。
    • 在很多情况下,头指针也被用来代表整个链表本身,因此当我们传递链表作为参数时,通常传递的是头指针。

下面是头结点和头指针的示例:

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

int main() {
    // 创建头结点
    ListNode* head = new ListNode(-1); // 头结点不存储有效数据
    ListNode* node1 = new ListNode(1);
    ListNode* node2 = new ListNode(2);

    head->next = node1;
    node1->next = node2;

    // 头指针指向链表的第一个节点
    ListNode* headPtr = head;

    // headPtr->next 其实就是 head->next。
    std::cout << "headPtr->next = " << headPtr->next << std::endl;
    std::cout << "head->next =  " << head->next << std::endl;
    
    // 但是 headPtr 指针和 head 头结点的地址是不同的
    std::cout << "&headPtr = " << &headPtr << std::endl;
    std::cout << "&head =  " << &head << std::endl;

    // 释放内存
    delete node2;
    delete node1;
    delete head;

    return 0;
}

在这个示例中,head是头指针,指向链表的第一个节点(头结点)。链表的头结点是一个额外添加的节点,用于简化对链表的操作(添加一个额外的头结点,也被称作虚拟头结点,可以让链表头部元素删除操作和其他元素删除操作保持一致)。

1.2.2 思考

在刚才的代码案例中 头指针和头结点的地址是不同的,但是 headPtr->nexthead->next 是相同的。
head 是指向链表头结点的指针,而 headPtr 是指向head的指针。

1.3 正确的题解

1.3.1 直接移除法

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        // 直接移除法
        // 1. 先判断头结点是目标节点的情况
        while (head && head->val == val)
        {
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }

        // 2. 移除非头结点
        ListNode* cur = head;
        while (cur && cur->next)
        {
            if (cur->next->val == val)
            {
                // 创建一个临时指针指向待删除的内存
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            }
            else
            {
                cur = cur->next;
            }
        }
        return head;
    }
};

首先通过遍历的方式将头结点是目标元素的值移除完,然后再考虑后序的情况。 下面是几个易混淆的值

  • head->val 是第一个节点的值
  • head->next 存储的是下一个节点
  • head->next->val 存储的是下一个节点的值

※※ 然后有个很重要的点需要弄清楚,就是 cur 指针指向的是哪个位置,此时头结点已经全部判断并移除完毕了,可以确定第一个节点肯定不是目标值。那么 cur 指针是从第一个节点开始移动呢?(也就是 ListNode* cur = head->next;)还是从第二个节点开始移动呢?(ListNode* cur = head;

  • 假如 cur 指针从第二个节点开始移动,那么第二个节点就需要判断自己是不是目标值,然后如果是的话,要将该节点移除。也就是找到当前节点的上个节点,但是我们此时已经找不到上个节点的信息了,因为我们是单向链表,只能一直向后遍历。
  • 假如 cur 指针从第一个节点开始移动,那么就去判断 cur->next->val == val 是否为目标值,如果是的话,那么就将 cur->next = cur->next->next; 此时删除操作就完成了。

在这里插入图片描述

1.3.2 添加虚拟头节点辅助移除法

链表的删除操作,头结点和非头结点是不同的,但是可以通过创建一个虚拟头结点,然后达到头结点和其他节点一样的删除操作。
现在添加了一个新的虚拟头结点,那么此时 cur 指针可以直接指向虚拟头结点,然后判断虚拟头结点的下一个节点是否需要移除(也就是实际存储数据的第一个节点)。

    ListNode* removeElements(ListNode* head, int val)
    {
        // 虚拟头结点法
        // 1. 创建一个虚拟头结点,然后指向第一个节点位置
        ListNode* dummyNode = new ListNode(-1);
        dummyNode->next = head;
        ListNode* cur = dummyNode;

        // 2. 遍历
        while (cur && cur->next)
        {
            if (cur->next->val == val)
            {
                ListNode* tmp = cur->next;
                cur->next = cur->next->next;
                delete tmp;
            }
            else
            {
                cur = cur->next;
            }
        }

        // 3. 返回更新后的头结点指针,注意不是返回head,因为头结点可能被移除,此时因该返回 虚拟头结点指向的下一个节点
        return dummyNode->next;
    }

1.3.3 总结

  • 直接移除法的是分头结点和非头结点两种情况进行操作。(cur 指针从head开始遍历)
  • 虚拟头结点法是在第一个节点前添加一个虚拟头结点,让头结点和非头节点操作保持一致。(cur指针从dummyHead开始遍历)

2 设计链表

  • 🎈 文档讲解:https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html
  • 🎈 视频讲解:https://programmercarl.com/0707.%E8%AE%BE%E8%AE%A1%E9%93%BE%E8%A1%A8.html
  • 🎈 做题状态:一定要注意虚拟头结点

2.1 我的解题

采用虚拟头结点的方式解题,这样可以保证头结点和非头结点操作的一致性

如下图: cur 是当前指针, dummy 是虚拟头结点,这个节点指向的下一个节点是第一个节点。在解题过程中一定要注意当前指针 cur 指向的是哪里,同时搞清楚当前指针和第一个节点的关系。

【注意】: dummy->next 返回的才是第一个节点,一定要记清楚。

在这里插入图片描述

class MyLinkedList {
public:
    struct LinkNode
    {
        int val;
        LinkNode* next;
        LinkNode(int val) : val(val), next(nullptr) {}
    };

    MyLinkedList() {
        dummyHead = new LinkNode(0);
        size = 0;
    }

    int get(int index) {
        // 如果 index 不在 区间内直接返回-1
        if (index > size - 1 || index < 0) 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;
        for (int i = 0; i < size; i++)
        {
            cur = cur->next;
        }
        // 遍历完后,cur指向最后一个节点
        LinkNode* newNode = new LinkNode(val);
        cur->next = newNode;
        size++;
    }

    void addAtIndex(int index, int val) {
        // 当index等于size时,插入链表末尾
        if (index == size)
        {
            addAtTail(val);
            return;
        }
        if (index > size || index < 0) return;

        // 当index是一个有效值时,将新的节点插入index下标之前
        LinkNode* cur = dummyHead;
        while (index > 0)
        {
            cur = cur->next;
            index--;
        }
        // 在cur后面插入新的节点
        LinkNode* newNode = new LinkNode(val);
        newNode->next = cur->next;
        cur->next = newNode;
        size++;
    }

    void deleteAtIndex(int index) {
        // 先判断index是否有效
        if (index > size - 1 || index < 0) return;

        LinkNode* cur = dummyHead;
        while (index > 0)
        {
            cur = cur->next;
            index--;
        }
        // 此时cur指向了要删除元素的前一个节点
        LinkNode* tmp = cur->next;
        cur->next = cur->next->next;
        delete tmp;
        size--;
    }

private:
    LinkNode* dummyHead;
    int size;
};

3 反转链表

  • 🎈 文档讲解:
  • 🎈 视频讲解:https://programmercarl.com/0206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.html
  • 🎈 做题状态:翻转链表其实就是遍历链表元素,然后插入新链表的头部

3.1 我的解题

注意,reverseHead 是一个虚拟头结点,所以返回的时候是返回 reverseHead->next 。

class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 翻转链表其实就是遍历链表元素,然后插入新链表的头部
        ListNode* reverseHead = new ListNode(0);
        if (head == nullptr) return head;

        ListNode* cur = head;
        while (cur)
        {
            // 将cur当前的值保存到新链表中
            ListNode* newNode = new ListNode(cur->val);
            newNode->next = reverseHead->next;
            reverseHead->next = newNode;
            cur = cur->next;
        }
        return reverseHead->next;
    }
};

3.2 其他方法(双指针法、递归写法)

双指针法

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;
    }
};

递归法

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);
    }

};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/441821.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【硬件工程师面经整理24_其它】

文章目录 1 功放线性指标调试方法2 功放线性指标之间的关系3 光衰减器的原理4 材料硬度由什么决定&#xff1f;5 晶振市场失效率&#xff1f;6 原码、反码和补码 1 功放线性指标调试方法 调试功放线性指标的方法可以根据具体的情况和要求而有所不同&#xff0c;以下是一般性的…

房屋租赁系统|基于 Mysql+Java+JSP技术的房屋租赁系统设计与实现(可运行源码+数据库+设计文档+部署说明+视频演示)

目录 文末获取源码 前台首页功能 管理员功能 租户功能 房屋租赁系统结构图 数据库设计 lunwen参考 概述 源码获取 文末获取源码 前台首页功能 管理员功能 租户功能 房屋租赁系统结构图 数据库设计 lunwen参考 概述 随着科学技术的飞速发展&#xff0c;社会的方方面面…

荔枝派zero驱动开发06:GPIO操作(platform框架)

参考&#xff1a; 正点原子Linux第五十四章 platform设备驱动实验 一张图掌握 Linux platform 平台设备驱动框架 上一篇&#xff1a;荔枝派zero驱动开发05&#xff1a;GPIO操作&#xff08;使用GPIO子系统&#xff09; 下一篇&#xff1a;更新中… 概述 platform是一种分层思…

Mac测试环境搭建

1 下载pycharm 下载地址&#xff1a;PyCharm&#xff1a;JetBrains 出品的用于数据科学和 Web 开发的 Python IDE 2 安装python3.6.8 下载地址&#xff1a;Index of /ftp/python/3.6.8/ 安装后提示错误 换一种方式&#xff1a;用conda 下载地址&#xff1a;Free Download | …

实现QT中qDebug()的日志重定向

背景&#xff1a; 在项目开发过程中&#xff0c;为了方便分析和排查问题&#xff0c;我们需要将原本输出到控制台的调试信息写入日志文件&#xff0c;进行持久化存储&#xff0c;还可以实现日志分级等。 日志输出格式&#xff1a; 我们需要的格式包括以下内容&#xff1a; 1.…

鸡肋的Git

1.前言 对于大多数开发人员来说&#xff0c;我们大多数在学习或者工作过程中只关注核心部分&#xff0c;比如说学习Java&#xff0c;可能对于大多数人而言一开始都是从Java基础学起&#xff0c;然后408&#xff0c;Spring&#xff0c;中间件等&#xff0c;当你发现很多高深的技…

红队专题-开源漏扫-巡风xunfeng源码剖析与应用

开源漏扫-巡风xunfeng 介绍主体两部分:网络资产识别引擎,漏洞检测引擎。代码赏析插件编写JSON标示符Python脚本此外系统内嵌了辅助验证功能文件结构功能 模块添加IP三. 进行扫描在这里插入图片描述 ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/de587a6f6f694…

html前端的几种加密/解密方式

HTML前端的加密解密方式有以下几种&#xff1a; 一、base64加密 Base64编码&#xff1a;Base64是一种将二进制数据转换为可打印字符的编码方式。在前端&#xff0c;可以使用JavaScript的btoa()函数进行Base64编码&#xff0c;使用atob()函数进行解码。 var str "hello…

二维码门楼牌管理系统在教育领域的应用及其优势

文章目录 前言一、二维码门楼牌管理系统概述二、教育领域的应用场景三、二维码门楼牌管理系统的优势四、结语 前言 随着信息技术的快速发展&#xff0c;二维码门楼牌管理系统在教育领域的应用越来越广泛。该系统不仅提高了地址信息的准确性&#xff0c;还为学校、家长和教育工…

Feign实现微服务间远程调用续;基于Redis实现消息队列用于延迟任务的处理,Redis分布式锁的实现;(黑马头条Day05)

目录 延迟任务和定时任务 使用Redis设计延迟队列原理 点评项目中选用list和zset两种数据结构进行实现 如何缓解Redis内存的压力同时保证Redis中任务能够被正确消费不丢失 系统流程设计 使用Feign实现微服务间的任务消费以及文章自动审核 系统微服务功能介绍 提交文章-&g…

C#,数值计算,解微分方程的龙格-库塔四阶方法与源代码

Carl Runge Martin Wilhelm Kutta 1 龙格-库塔四阶方法 数值分析中&#xff0c;龙格&#xff0d;库塔法&#xff08;Runge-Kutta&#xff09;是用于模拟常微分方程的解的重要的一类隐式或显式迭代法。这些技术由数学家卡尔龙格和马丁威尔海姆库塔于1900年左右发明。 对于一阶…

[Electron]中IPC进程间通信

Electron中IPC 进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。在 Electron 中&#xff0c;进程使用 ipcMain 和 ipcRenderer 模块&#xff0c;通过开发人员定义的“通道”传递消息来进行通信。 本文介绍以下几个方面&#xff1a; 1-渲染进程到…

【vue.js】文档解读【day 3】 | 列表渲染

如果阅读有疑问的话&#xff0c;欢迎评论或私信&#xff01;&#xff01; 文章目录 列表渲染v-forv-for 与对象在 v-for 里使用范围值template 上的 v-forv-for与v-if通过key管理状态组件上使用v-for数组变化侦测 列表渲染 v-for 在我们想要渲染出一个数组中的元素时&#xf…

【C语言】数据类型和变量

前言&#x1f49e;&#x1f49e; 啦啦啦~这里是土土数据结构学习笔记&#x1f973;&#x1f973; &#x1f4a5;个人主页&#xff1a;大耳朵土土垚的博客 &#x1f4a5; 所属专栏&#xff1a;C语言笔记 &#x1f4a5;欢迎大家&#x1f973;&#x1f973;点赞✨收藏&#x1f49…

linux网络编程(概念)

概念 通信四元组 IP&#xff08;主机&#xff09; 0号地址与1号地址 端口&#xff08;进程&#xff09; 四元组组成 各种体系结构 网络的封包和解包 ip地址向物理&#xff08;mac&#xff09;地址转换 mac转换ip-------->RARP协议 TCP协议 UDP协议 socket函数接口

瑞_23种设计模式_模板方法模式

文章目录 1 模板方法模式&#xff08;Template Pattern&#xff09; ★ 钩子函数1.1 介绍1.2 概述1.3 模板方法模式的结构1.4 模板方法模式的优缺点1.5 模板方法模式的使用场景 2 案例一2.1 需求2.2 代码实现 3 案例二3.1 需求3.2 代码实现 4 JDK源码解析&#xff08;InputStre…

java项目线上捉BUG经验记录

一 线上问题 昨晚上突然接到公司紧急来电电桩设备大面积离线&#xff0c;意味着某市的车无法充电……”&#xff0c;细想这个平台很久都没有更新&#xff0c;最近出现问题是刚好在一个月前也是出现这种情况&#xff0c;再上一次就是几年前更新的。平台平时是稳定&#xff0c;开…

php使用ElasticSearch

ElasticSearch简介 Elasticsearch 是一个分布式的、开源的搜索分析引擎&#xff0c;支持各种数据类型&#xff0c;包括文本、数字、地理、结构化、非结构化。 Lucene与ElasticSearch Apache Lucene是一款高性能的、可扩展的信息检索&#xff08;IR&#xff09;工具库&#xf…

【企业发展战略】某环境管理集团公司发展战略与规划项目纪实

在集团公司高速发展、业务范围不断扩大时&#xff0c;组织往往对公司未来的发展方向感到迷茫&#xff0c;不知道如何进行更好的规划&#xff0c;找到合适的发展战略&#xff0c;为企业提供更长远的发展空间&#xff0c;带来更多是利益。面对这个问题&#xff0c;华恒智信认为企…

StarRocks实战——欢聚集团极速的数据分析能力

目录 一、大数据平台架构 二、OLAP选型及改进 三、StarRocks 经验沉淀 3.1 资源隔离&#xff0c;助力业务推广 3.1.1 面临的挑战 3.1.2 整体效果 3.2 稳定优先&#xff0c;监控先行&#xff0c;优化运维 3.3降低门槛&#xff0c;不折腾用户 3.3.1 与现有的平台做打通 …