代码随想录算法训练营第三天| 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

相关文章

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

Python中顺序结构和循环结构示例代码

《Python中顺序结构和循环结构示例代码》:本文主要介绍Python中的条件语句和循环语句,条件语句用于根据条件执行不同的代码块,循环语句用于重复执行一段代码,文章还详细说明了range函数的使... 目录一、条件语句(1)条件语句的定义(2)条件语句的语法(a)单分支 if(b)双分支 if-else(

MySQL数据库函数之JSON_EXTRACT示例代码

《MySQL数据库函数之JSON_EXTRACT示例代码》:本文主要介绍MySQL数据库函数之JSON_EXTRACT的相关资料,JSON_EXTRACT()函数用于从JSON文档中提取值,支持对... 目录前言基本语法路径表达式示例示例 1: 提取简单值示例 2: 提取嵌套值示例 3: 提取数组中的值注意

CSS3中使用flex和grid实现等高元素布局的示例代码

《CSS3中使用flex和grid实现等高元素布局的示例代码》:本文主要介绍了使用CSS3中的Flexbox和Grid布局实现等高元素布局的方法,通过简单的两列实现、每行放置3列以及全部代码的展示,展示了这两种布局方式的实现细节和效果,详细内容请阅读本文,希望能对你有所帮助... 过往的实现方法是使用浮动加

JAVA调用Deepseek的api完成基本对话简单代码示例

《JAVA调用Deepseek的api完成基本对话简单代码示例》:本文主要介绍JAVA调用Deepseek的api完成基本对话的相关资料,文中详细讲解了如何获取DeepSeekAPI密钥、添加H... 获取API密钥首先,从DeepSeek平台获取API密钥,用于身份验证。添加HTTP客户端依赖使用Jav

Java实现状态模式的示例代码

《Java实现状态模式的示例代码》状态模式是一种行为型设计模式,允许对象根据其内部状态改变行为,本文主要介绍了Java实现状态模式的示例代码,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来... 目录一、简介1、定义2、状态模式的结构二、Java实现案例1、电灯开关状态案例2、番茄工作法状态案例

nginx-rtmp-module模块实现视频点播的示例代码

《nginx-rtmp-module模块实现视频点播的示例代码》本文主要介绍了nginx-rtmp-module模块实现视频点播,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习... 目录预置条件Nginx点播基本配置点播远程文件指定多个播放位置参考预置条件配置点播服务器 192.

CSS自定义浏览器滚动条样式完整代码

《CSS自定义浏览器滚动条样式完整代码》:本文主要介绍了如何使用CSS自定义浏览器滚动条的样式,包括隐藏滚动条的角落、设置滚动条的基本样式、轨道样式和滑块样式,并提供了完整的CSS代码示例,通过这些技巧,你可以为你的网站添加个性化的滚动条样式,从而提升用户体验,详细内容请阅读本文,希望能对你有所帮助...