代码随想录算法训练营第三天| 203.移除链表元素、 707.设计链表、 206.反转链表

本文主要是介绍代码随想录算法训练营第三天| 203.移除链表元素、 707.设计链表、 206.反转链表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

203.移除链表元素

在这里插入图片描述

题目链接: 203.移除链表元素
文档讲解:代码随想录
状态:没做出来,做题的时候定义了一个cur指针跳过了目标val遍历了一遍链表,实际上并没有删除该删的节点。

错误代码:

    public ListNode removeElements(ListNode head, int val) {ListNode sentinel = new ListNode();sentinel.next = head;ListNode cur = sentinel;while (cur.next != null) {ListNode next = cur.next;if (next.val == val) {// 这里是错误的,cur 被设置为 next.next,直接跳过了 next 节点。但这并没有实际从链表中删除 next 节点,它仍然存在于链表中。// 如果 next.next 是 null,则 cur 被设置为 null,导致后续的 cur.next 会抛出空指针异常cur = next.next;} else {cur = next;}}return sentinel.next;}

思路:定义一个ListNode类型的cur指针,遍历链表,遇到等于目标val的节点,通过修改节点的next实现删除元素。因为NodeList是引用类型,所以当cur从虚拟头节点出发时,修改cur中的引用会影响虚拟头节点后面的值,从而实现删除操作。

题解

    public ListNode removeElements(ListNode head, int val) {// 创建一个哨兵节点,简化边界情况的处理ListNode sentinel = new ListNode();sentinel.next = head;ListNode cur = sentinel;// 遍历链表while (cur.next != null) {ListNode next = cur.next;// 如果下一个节点的值等于指定值if (next.val == val) {// 应该通过修改cur.next来删除next节点cur.next = next.next;} else {// 否则,继续向后遍历cur = next;}}// 返回处理后的链表头节点return sentinel.next;}

什么时候使用虚拟头节点?
虚拟头节点(哨兵节点)主要解决了由于头节点可能为空或者需要特别处理而导致的额外操作。
如果在使用虚拟头节点后仍然从 head 节点出发,那么虚拟头节点的定义就失去了意义。

所以,当定义了虚拟头节点(哨兵节点)后,一般情况下,遍历操作中的 cur(当前节点)都从虚拟头节点出发。这是因为虚拟头节点是为了简化处理头节点的特殊情况而引入的,从虚拟头节点开始遍历,可以统一处理所有节点(包括原本的头节点),避免了额外的边界条件检查。

707.设计链表

在这里插入图片描述

题目链接:707.设计链表
文档讲解:代码随想录
状态:没做出来(使用了双向链表,没有使用虚拟头节点,结果一堆边界问题。。。)

题解

单向链表+虚拟头节点题解

public class MyLinkedList {private ListNode dummy; // 虚拟节点private int size;       // 链表的长度/** 初始化链表 */public MyLinkedList() {dummy = new ListNode(0);size = 0;}/** 获取链表中下标为 index 的节点的值,如果下标无效则返回 -1 */public int get(int index) {if (index < 0 || index >= size) {return -1;}ListNode current = dummy.next;for (int i = 0; i < index; i++) {current = current.next;}return current.val;}/** 在链表头部插入值为 val 的节点 */public void addAtHead(int val) {ListNode newHead = new ListNode(val);newHead.next = dummy.next;dummy.next = newHead;size++;}/** 在链表尾部追加值为 val 的节点 */public void addAtTail(int val) {ListNode newTail = new ListNode(val);ListNode current = dummy;while (current.next != null) {current = current.next;}current.next = newTail;size++;}/** 在链表中下标为 index 的节点之前插入值为 val 的节点 */public void addAtIndex(int index, int val) {if (index < 0 || index > size) {return;}
// 注意:不是ListNode current = dummy.next;
// 简而言之,ListNode cur = dummy; 确保了在插入节点时,从链表的头节点开始遍历,而不是跳过头节点。
// 例如index=0,则是dummy.next = newNodeListNode current = dummy;for (int i = 0; i < index; i++) {current = current.next;}ListNode newNode = new ListNode(val);newNode.next = current.next;current.next = newNode;size++;}/** 删除链表中下标为 index 的节点 */public void deleteAtIndex(int index) {if (index < 0 || index >= size) {return;}ListNode current = dummy;for (int i = 0; i < index; i++) {current = current.next;}current.next = current.next.next;size--;}static class ListNode {int val;ListNode next;// 节点构造函数public ListNode(int val) {this.val = val;}}
}

双向链表无虚拟头节点题解(不推荐)

class MyLinkedList {ListNode head;static class ListNode {int val;ListNode pre;ListNode next;public ListNode() {}public ListNode(int val, ListNode pre, ListNode next) {this.val = val;this.pre = pre;this.next = next;}@Overridepublic String toString() {return "ListNode{" +"val=" + val +", pre=" + pre +", next=" + next +'}';}}public MyLinkedList() {head = null;}public int get(int index) {if (index < 0 || head == null) {return -1;}ListNode cur = head;while (index > 0 && cur != null) {cur = cur.next;index--;}return (cur == null) ? -1 : cur.val;}public void addAtHead(int val) {ListNode node = new ListNode(val, null, head);if (head != null) {head.pre = node;}head = node;}public void addAtTail(int val) {ListNode node = new ListNode(val, null, null);ListNode cur = head;while (cur.next != null) {cur = cur.next;}cur.next = node;node.pre = cur;}public void addAtIndex(int index, int val) {if (index < 0) {return;}if (index == 0) {addAtHead(val);return;}ListNode cur = head;while (index > 1 && cur != null) {cur = cur.next;index--;}if (cur == null) {return;}ListNode node = new ListNode(val, cur, cur.next);if (cur.next != null) {cur.next.pre = node;}cur.next = node;}public void deleteAtIndex(int index) {if (index < 0 || head == null) {return;}if (index == 0) {head = head.next;if (head != null) {head.pre = null;}return;}ListNode cur = head;while (index > 0 && cur != null) {cur = cur.next;index--;}if (cur == null) {return;}if (cur.pre != null) {cur.pre.next = cur.next;}if (cur.next != null) {cur.next.pre = cur.pre;}}
}

206.反转链表

在这里插入图片描述

题目链接: 206.反转链表
文档讲解:代码随想录
状态:没做出来,明明很简单的题,为啥没做出来呢。。。。。想着用dummy节点,dummy->1->2->3… dummy->2->1->3… dummy->3->2->1->…,这个dummy的next一直在变。。。

思路:

在这里插入图片描述

题解

    public ListNode reverseList(ListNode head) {// 初始化前一个节点为nullListNode prev = null;// 当前节点从head开始ListNode curr = head;while (curr != null) {// 暂时保存下一个节点ListNode next = curr.next;// 当前节点的next指向前一个节点curr.next = prev;// 移动前一个节点到当前节点prev = curr;// 移动当前节点到下一个节点curr = next;}// 最后prev将指向新的头节点return prev;}

总结反思

为啥做题的时候总是有很多乱七八糟的思路,自己找的还总是最差的那种呢。。。。

链表题解的两个技巧
遇到链表相关的题,无论问题是什么,先要想想是不是可以用上以下的两个技巧。

  1. 哨兵节点:哨兵节点是一个非常常用的链表技巧,在处理链表边界问题的场景下,可以减少我们代码的复杂度。
    主要解决的问题如下:
    • 处理完一条链表后,需要返回这个链表的头结点。我们在一开始的时候使用哨兵节点(dummy),让它的 next 节点指向 head 节点。最后 return 时直接返回 dummy.next 即可。
    • 在对链表进行插入或删除节点时,使用哨兵节点可以简化删除 head 节点或者向 head 前插入节点时的处理逻辑。因为头节点没有前驱节点,这就导致对其的增删需要额外操作。
    • 在某些遍历链表的时候,可能会需要同时记录 pre 节点。当你从 head 节点开始遍历时,head 是没有 pre 节点的(为null)。而此时引用哨兵节点,相当于帮助 head 节点初始化了一个 pre 节点,可以方便的解决 pre 节点为空的问题

为啥反转链表这道题不用虚拟头结点呢?

对于反转链表的操作,无论是递归还是迭代方法,都集中在反转节点的指针上,而不涉及节点的插入或删除操作。因此,反转链表的核心在于调整指针的方向,而不是处理节点的前驱节点或边界条件。这也是为什么在反转链表时,虚拟头节点并不能带来实际的简化。

  1. 双指针:其实不止是链表,对于数组题来说,也是非常好用的技巧。
    双指针的主要用法有以下几种:
    • 两个指针在两条链表上行走:这种方法通常用于合并两个有序链表、找到两个链表的交点等问题。
    • 快慢指针,同时前进:快慢指针通常用于检测链表中是否存在环,或者找到链表的中间节点等情况。
    • 前后指针,前指针先走 n 步,之后两个指针同时前进:这种方法常用于找到链表的倒数第 n 个节点,或者解决某些数组问题。
    • 一个指针用来迭代遍历链表,另一个指针用来记录:在链表操作中,常常需要用一个指针来迭代遍历链表,另一个指针用来记录节点,例如 pre 和 cur;cur 和 next;pre、cur 和 next。

这篇关于代码随想录算法训练营第三天| 203.移除链表元素、 707.设计链表、 206.反转链表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/999592

相关文章

python使用Akshare与Streamlit实现股票估值分析教程(图文代码)

《python使用Akshare与Streamlit实现股票估值分析教程(图文代码)》入职测试中的一道题,要求:从Akshare下载某一个股票近十年的财务报表包括,资产负债表,利润表,现金流量表,保存... 目录一、前言二、核心知识点梳理1、Akshare数据获取2、Pandas数据处理3、Matplotl

Django开发时如何避免频繁发送短信验证码(python图文代码)

《Django开发时如何避免频繁发送短信验证码(python图文代码)》Django开发时,为防止频繁发送验证码,后端需用Redis限制请求频率,结合管道技术提升效率,通过生产者消费者模式解耦业务逻辑... 目录避免频繁发送 验证码1. www.chinasem.cn避免频繁发送 验证码逻辑分析2. 避免频繁

精选20个好玩又实用的的Python实战项目(有图文代码)

《精选20个好玩又实用的的Python实战项目(有图文代码)》文章介绍了20个实用Python项目,涵盖游戏开发、工具应用、图像处理、机器学习等,使用Tkinter、PIL、OpenCV、Kivy等库... 目录① 猜字游戏② 闹钟③ 骰子模拟器④ 二维码⑤ 语言检测⑥ 加密和解密⑦ URL缩短⑧ 音乐播放

Python使用Tenacity一行代码实现自动重试详解

《Python使用Tenacity一行代码实现自动重试详解》tenacity是一个专为Python设计的通用重试库,它的核心理念就是用简单、清晰的方式,为任何可能失败的操作添加重试能力,下面我们就来看... 目录一切始于一个简单的 API 调用Tenacity 入门:一行代码实现优雅重试精细控制:让重试按我

Mysql中设计数据表的过程解析

《Mysql中设计数据表的过程解析》数据库约束通过NOTNULL、UNIQUE、DEFAULT、主键和外键等规则保障数据完整性,自动校验数据,减少人工错误,提升数据一致性和业务逻辑严谨性,本文介绍My... 目录1.引言2.NOT NULL——制定某列不可以存储NULL值2.UNIQUE——保证某一列的每一

Python实现MQTT通信的示例代码

《Python实现MQTT通信的示例代码》本文主要介绍了Python实现MQTT通信的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 安装paho-mqtt库‌2. 搭建MQTT代理服务器(Broker)‌‌3. pytho

MySQL进行数据库审计的详细步骤和示例代码

《MySQL进行数据库审计的详细步骤和示例代码》数据库审计通过触发器、内置功能及第三方工具记录和监控数据库活动,确保安全、完整与合规,Java代码实现自动化日志记录,整合分析系统提升监控效率,本文给大... 目录一、数据库审计的基本概念二、使用触发器进行数据库审计1. 创建审计表2. 创建触发器三、Java

Python中反转字符串的常见方法小结

《Python中反转字符串的常见方法小结》在Python中,字符串对象没有内置的反转方法,然而,在实际开发中,我们经常会遇到需要反转字符串的场景,比如处理回文字符串、文本加密等,因此,掌握如何在Pyt... 目录python中反转字符串的方法技术背景实现步骤1. 使用切片2. 使用 reversed() 函

C# 比较两个list 之间元素差异的常用方法

《C#比较两个list之间元素差异的常用方法》:本文主要介绍C#比较两个list之间元素差异,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1. 使用Except方法2. 使用Except的逆操作3. 使用LINQ的Join,GroupJoin

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.