LeetCode_Day3 | 反转链表/移除链表的元素/设计个链表

news2024/12/28 5:57:56

LeetCode_链表

  • 203.移除链表元素
    • 1. 题目描述
    • 2. 直接使用原表删除
      • 1. 思路
      • 2. 代码实现
    • 3. 使用虚拟头节点删除
      • 1. 思路
      • 2. 代码实现
  • 707.设计链表
    • 1.题目描述
    • 2.单链表:虚拟头节点设计
      • 1. 思路
      • 2. 代码实现及部分逻辑解释
      • 3. 需要注意的点
  • 206.反转链表
    • 1.题目描述
    • 2. 双指针法
      • 1.思路
      • 2. 代码实现
    • 3.递归法
      • 1.思路
      • 2.代码实现

203.移除链表元素

1. 题目描述

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

示例:
在这里插入图片描述输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

输入:head = [], val = 1
输出:[]

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

LeetCode题详情链接: https://leetcode.cn/problems/remove-linked-list-elements/

2. 直接使用原表删除

1. 思路

链表删除元素:是将当前元素的前一个元素指向当前元素的后一个元素(即删除了当前元素节点);

需要注意的是直接在原表上删除需要特殊考虑头节点的删除

移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。

头节点的移除: 将头结点向后移动一位,即就是从链表中移除了一个头结点。

2. 代码实现

/**
     *思路:不添加虚拟头节点的原链表删除法
     *     头节点需要另外考虑:如果头节点等于target,那么设置头节点的下个节点为头节点(即删除等于target的值)(比如类似这种head=[1,1,1,1],使用while)。
     *     因为单链表的特性只有指向下个节点的指针,所以设置需要删除元素的位置需要处于中间。
     *时间复杂度:O(n)
     *空间复杂度:O(1)
     */
    public ListNode removeElements(ListNode head, int val) {
         while (head != null && head.val == val){ 
            head = head.next;
        }
        ListNode cur = head;
        while (cur != null && cur.next != null){
            if (cur.next.val == val){
                cur.next = cur.next.next;
            }else{
                cur = cur.next;
            }
        }
        return head;
    }

3. 使用虚拟头节点删除

1. 思路

如果不特殊考虑头节点的删除的话,链表的其他节点删除都是将当前元素的前一个元素指向当前元素的后一个元素(即删除了当前元素节点);

所以给头节点的前边添加一个虚拟的头节点,那么头节点就和其他节点一样操作对比删除了。

简单图解如下(删除head节点时):
在这里插入图片描述

2. 代码实现

 /**
    *思路:添加虚拟头节点的删除法
    *     因为头节点需要额外考虑,所以给头节点的前边添加一个虚拟的头节点,那么头节点就和其他节点一样操作对比删除了。
    *     因为单链表的特性只有指向下个节点的指针,所以设置需要删除元素的位置需要处于中间。
    *时间复杂度:O(n)
    *空间复杂度:O(1)
    */
   public ListNode removeElements(ListNode head, int val) {
        if (head == null){
            return head;
        }
        ListNode dummy = new ListNode(-1,head);//虚拟头节点
        ListNode cur = dummy;
        while (cur.next != null){
           if (cur.next.val == val){
               cur.next = cur.next.next;
           }else {
               cur = cur.next;
           }
        }
        return dummy.next;
   }

707.设计链表

1.题目描述

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index
    等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:
在这里插入图片描述
LeetCode题详情链接: https://leetcode.cn/problems/design-linked-list/

2.单链表:虚拟头节点设计

1. 思路

使用虚拟头节点而不是直接操作头节点,可以对head头节点的处理如同链表中其他元素处理的逻辑一样(比如插入时)。

2. 代码实现及部分逻辑解释

//单链表
class ListNode {
    int val;
    ListNode next;
    ListNode(){}
    public ListNode (int val){
        this.val = val;
    }
}

/**
 *使用虚拟头节点可以对head头节点的处理如同链表中其他元素处理的逻辑一样。
 *
 */
class MyLinkedList {
    
    int size; //size存储链表元素的个数
    
    ListNode head; //虚拟头节点
    
    /**
     *初始化链表
     */
    public MyLinkedList() {
        size = 0;
        head = new ListNode(0);
    }
    
    /**
     * 获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
     */
    public int get(int index) {
        //如果index非法,返回-1
        if (index < 0 || index >= size){
            return -1;
        }
        ListNode currentNode = head;
        for (int i = 0; i <= index; i++){
            //包含一个虚拟头节点,所以查找第 index+1 个节点
            currentNode = currentNode.next;
        }
        return currentNode.val;
    }
    
    /**
     * 在链表最前面插入一个节点,等价于在第0个元素前添加
     */
    public void addAtHead(int val) {
        addAtIndex(0,val);
    }
    
    /**
     * 在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
     */
    public void addAtTail(int val) {
        addAtIndex(size,val);
    }
   
    /**
     * 如果 index 大于链表的长度,则返回空,该节点将 不会插入 到链表中
     * 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
     * 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
     */
    public void addAtIndex(int index, int val) {
        if (index > size){
            return;
        }
        if (index < 0){
            index = 0;
        }
        size++;

        //找到要插入节点的前驱
        ListNode pre = head;
        for (int i = 0; i < index; i++){
            pre = pre.next;
        }
        ListNode toAdd = new ListNode(val);
        //这里注意插入顺序:需先赋值toAdd.next = pred.next;
        //若先赋值pre.next = toAdd,则插入前的pre.next节点(即原index位置上的节点无法表示)
        toAdd.next = pre.next;
        pre.next = toAdd;
    }
    
    /**
     * 删除第index个节点
     */
    public void deleteAtIndex(int index) {
        //如果index非法,返回-1
        if (index < 0 || index >= size){
            return;
        }

        size--;

        //删除头节点
        if (index == 0){
            head = head.next;
            return;
        }

        //找到要删除位置的前驱节点
        ListNode pre = head;
        for (int i = 0; i < index; i++){
            pre = pre.next;
        }
        //删除index位置上的节点
        pre.next = pre.next.next;
    }
}

3. 需要注意的点

  • addAtIndex(index,val)方法插入时,需要注意插入时的赋值顺序:需先赋值toAdd.next = pred.next;

    若先赋值pre.next = toAdd,则插入前的pre.next节点(即原index位置上的节点无法表示)

简单理解如图:
在这里插入图片描述

206.反转链表

1.题目描述

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

示例:
在这里插入图片描述
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

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

LeetCode题详情链接: https://leetcode.cn/problems/reverse-linked-list/

2. 双指针法

1.思路

观察题目发现,链表反转后即全部指针指向相反方向,原头节点指向null

所以只需要改变链表的next指针的指向,直接将链表反转,也不需要重新定义一个新的链表,那样反而是对内存空间的浪费。

  • 定义两个指针: cur指针指向头结点,再定义一个pre指针,初始化为null。
  • 然后开始反转: 反转头节点是将cur->next 指向pre;这里需要注意,因为此时已经改变了cur.next的指向了,所以反转时需要把cur.next保存为临时节点temp。
  • 接着不断向前移动pre和cur指针的位置,继续反转,直至cur指向null时,反转结束。
  • 需要注意移动指针时,需先移动pre,后移动cur; 因为若先移动cur的话,cur的位置就被修改了,pre移动时找不到原cur的位置了
  • 当**cur==null(遍历的边界条件)**时,即为遍历结束。最后返回pre,因为遍历到最后,pre指向反转后链表的头节点,cur指向null。

简单图解如下:

在这里插入图片描述

2. 代码实现

/**
     * 思路:双指针法
     *      观察发现反转链表后头节点指向null,剩余节点的指针指向全都相反
     *      所以定义两个指针:cur指针指向头结点,再定义一个pre指针,初始化为null。
     *      然后开始反转:反转头节点是将cur->next 指向pre;这里需要注意,
     *                  因为此时已经改变了cur.next的指向了,所以反转时需要把cur.next保存为临时节点temp。
     *      然后就是不断向前移动pre和cur指针的位置,继续反转,直至cur指向null时,反转结束。
     *      需要注意移动指针时,需先移动pre,后移动cur;因为若先移动cur的话,cur的位置就被修改了,pre移动时找不到原cur的位置了
     * 时间复杂度:O(n),其中 n 是链表的长度,需要遍历链表一次。
     * 空间复杂度:O(1)
     */
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        ListNode temp = null;
        while (cur != null){
            temp = cur.next;//保存cur的下一个节点
            cur.next = pre; //反转操作
            pre = cur; //pre移动
            cur = temp;//cur移动
        }
        return pre;//遍历到最后,pre指向反转后链表的头节点,cur指向null
    }

3.递归法

1.思路

实际递归就是将上述双指针法的共有逻辑进行了提取。所以尝试递归法之前一定要理解上述双指针法。

根据双指针的思路进行递归转换,以下为双指针思路:

  • 观察发现反转链表后头节点指向null,剩余节点的指针指向全都相反
  • 所以定义两个指针:cur指针指向头结点,再定义一个pre指针,初始化为null。
  • 然后开始反转:反转头节点是将cur->next 指向pre;这里需要注意,
  • 因为此时已经改变了cur.next的指向了,所以反转时需要把cur.next保存为临时节点temp。
  • 然后就是不断向前移动pre和cur指针的位置,继续反转,直至cur指向null时,反转结束。
  • 需要注意移动指针时,需先移动pre,后移动cur;因为若先移动cur的话,cur的位置就被修改了,pre移动时找不到原cur的位置了

递归时将共有处理逻辑(即转换)提出,每次更新pre和cur的位置相当于要进行下一个反转,此时再递归调用共有逻辑。

递归的出口和双指针的循环结束条件一样,即当cur==null时,调用结束。

2.代码实现

/**
     * 思路:递归法
     *      根据双指针的思路进行递归转换,以下为双指针思路:
     *      (观察发现反转链表后头节点指向null,剩余节点的指针指向全都相反
     *      所以定义两个指针:cur指针指向头结点,再定义一个pre指针,初始化为null。
     *      然后开始反转:反转头节点是将cur->next 指向pre;这里需要注意,
     *                  因为此时已经改变了cur.next的指向了,所以反转时需要把cur.next保存为临时节点temp。
     *      然后就是不断向前移动pre和cur指针的位置,继续反转,直至cur指向null时,反转结束。
     *      需要注意移动指针时,需先移动pre,后移动cur;因为若先移动cur的话,cur的位置就被修改了,pre移动时找不到原cur的位置了)
     *      
     *      递归时将共有处理逻辑(即转换)提出,每次更新pre和cur的位置相当于要进行下一个反转,此时再递归调用共有逻辑。
     *      递归的出口和双指针的循环结束条件一样,即当cur==null时,调用结束。
     * 时间复杂度:O(n),其中 n 是链表的长度,需要遍历链表一次。
     * 空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为 n 层。
     */
    public ListNode reverseList(ListNode head) {
        return reverse(null,head);//相当于双指针的初始化,pre=null,cur=head
    }

    private ListNode reverse(ListNode prev, ListNode cur) {
        if (cur == null){
            return prev;
        }
        ListNode temp = cur.next; //保存下一个节点
        cur.next = prev;// 反转
        return reverse(cur, temp);//相当于双指针中更新了pre和cur的位置再进行下一个的反转
    }

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

BIO阻塞模型

作者&#xff1a;V7 博客&#xff1a;https://www.jvmstack.cn 一碗鸡汤 少年辛苦终身事&#xff0c;莫向光阴惰寸功。 —— 杜荀鹤 同步阻塞IO 在介绍阻塞和非阻塞之前先说明一下同步和异步。我们可以将同步和异步看做是发起IO请求的两种方式。同步IO指的是用户空间&…

项目美术部门敏捷开发流程及工具

前言 在项目开发中&#xff0c;针对美术部门的特性和工作风格&#xff0c;合理使用多种工具进行项目管理和进度控制非常必要。如果项目执行中有异地协作、美术内/外包、多项目并行的情况&#xff0c;正确的规划和管理的优势会更加凸显。 而合理使用整套的综合管理工具会让项目…

Day969.如何拆分代码 -遗留系统现代化实战

如何拆分代码 Hi&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于如何拆分代码的内容。 当完成了项目的战略设计&#xff0c;大体设计出目标架构&#xff0c;又根据系统的现状&#xff0c;决定采用“战术分叉”的方式进行微服务拆分之后&#xff0c;接下来的难点就变成…

用GPT-4 写2022年天津高考作文能得多少分?

正文共 792 字&#xff0c;阅读大约需要 3 分钟 学生必备技巧&#xff0c;您将在3分钟后获得以下超能力&#xff1a; 积累作文素材 Beezy评级 &#xff1a;B级 *经过简单的寻找&#xff0c; 大部分人能立刻掌握。主要节省时间。 推荐人 | Kim 编辑者 | Linda ●图片由Lexica …

vue工程搭建

1&#xff1a;查看vue及npm版本&#xff1a; 2&#xff1a;执行npm init nuxt-app <project-name>语句&#xff1a; 若出现npm ERR! code ENOLOCAL 请执行如下语句&#xff1a; npm cache verify npm cache clean --force npm i -g npm npm install -g cnpm --regis…

数据库(mysql语句)循环语句

例题1&#xff1a; 20到50之间能被5除余1的所有自然数的和 EDECLARE i int DECLARE s int SET s0 SET i20 白WHILE i <50 BEGIN IF(i%51) SET s s i SET ii1 END PRINT20到50之间能被5除余1的所有自然数的和是cast(s as varchar(20)) 例题2&#xff1a; 实现如下图 代码…

设计模式之门面模式(Facade Pattern 外观模式)

一、模式定义 门面模式(Facade Pattern)&#xff1a;外部与一个子系统的通信必须通过一个统一的外观对象进行&#xff0c;为子系统中的一组接口提供一个一致的界面&#xff0c;外观模式定义了一个高层接口&#xff0c;这个接口使得这一子系统更加容易使用。门面模式又称为外观…

中国南方Oracle用户组沙龙活动:大环境下的Oracle数据库的机遇与挑战

2023年03月12日(周日)在杭州索菲特西湖大酒店 (浙江省杭州市上城区西湖大道333 号)&#xff0c;中国南方Oracle用户组创始人之一&#xff1a;周亮&#xff08;zhou liang&#xff09;组织举办了主题为《大环境下的Oracle数据库的机遇与挑战》活动&#xff0c;大约有50名左右的人…

YMatrix 5.0 故障自动转移功能新实现,运维更方便!

前言 分布式数据库一般都实现了数据多副本的存储以保证数据的高可用性。在多副本存储的基础上&#xff0c;通过切换活跃的存储副本来实现故障转移&#xff0c;是常见的做法。 YMatrix 5.0 实现了在数据库集群所有数据分片上的故障自动转移&#xff0c;完全实现了数据库集群的…

一文带你深入了解分布式数据的复制原理!!

在分布式数据系统中&#xff0c;复制是一种重要的能力。简单来说&#xff0c;复制就是将数据的副本存储在多个位置&#xff0c;通常是在不同的服务器或节点上。这样做有几个关键的优点&#xff1a; 使得数据与用户在地理上接近&#xff08;从而减少延迟&#xff09;&#xff0…

渗透测试--3.1嗅探欺骗攻击

目录 1.中间人攻击 2. 社会工程学攻击 SET使用实例——建立克隆钓鱼网站收集目标凭证 SET工具集之木马欺骗实战反弹链接 后渗透阶段 钓鱼邮件 总结 1.中间人攻击 中间人攻击&#xff08;Man-in-the-middle attack&#xff0c;简称MITM&#xff09;是一种常见的网络攻击…

一文带你完整了解数据的编码!!

数据编码是将数据转换为计算机可读取和处理的二进制格式的过程。在数据存储中&#xff0c;正确的数据编码非常重要&#xff0c;因为它能够保证数据的完整性、可靠性和可读性。 数据编码可以确保数据在存储过程中不会发生错误。通过使用适当的数据编码规则&#xff0c;可以防止…

三十二、自定义镜像

1 、Docker镜像的原理 Docker镜像本质是什么? Docker中一个centos镜像为什么只有200MB&#xff0c;而一个centos操作系统的iso文件要几个G? Docker中一个tomcat镜像为什么有500MB&#xff0c;而一个tomcat安装包只有10多MB? 操作系统组成部分: 计算机组成原理 进程调度子…

OPPO 关停“造芯”业务 ZEKU:一场大胆的尝试的结束

前言 近期&#xff0c;OPPO 关停了其“造芯”业务 ZEKU&#xff0c;结束了其自主研发处理器的尝试。本文将对这一事件进行探讨&#xff0c;分析其背后的原因及其对整个行业的影响。 1. ZEKU的成立与使命 在2019年&#xff0c;OPPO旗下的ZEKU研究所成立&#xff0c;标志着OPP…

Redis缓存穿透、缓存雪崩和缓存击穿

缓存穿透 缓存穿透&#xff0c;是指查询一个缓存和数据库中都没有的数据。正常的使用缓存流程大致是&#xff0c;数据查询先进行缓存查询&#xff0c;如果key不存在或者key已经过期&#xff0c;再对数据库进行查询&#xff0c;并把查询到的对象&#xff0c;放进缓存。如果数据库…

Centos和Windows之间通过主机名实现相互访问

一、业务需求 在内网环境中,我们想直接通过特定的主机名称去访问我们的服务器,而不用去记忆服务器的IP地址,且不想通过nginx等代理工具或域名配置内容来操作。 二、分析 通常我们可以直接在浏览器中输入【localhost】后按下回车键后就可以访问我们的主机80端口的内容了;那…

计算机网络大作业(Wireshark抓包分析)

实验要求 wireshark的深入学习与掌握&#xff0c;如过滤器的使用&#xff0c;归纳方法通过实验阐述ARP的工作原理利用实验结果分析 ICMP 协议的报文结构字段定义基于实验数据深入分析 TCP 协议的连接过程原理&#xff0c;报文的分片等功能从校园网发起向外网中某 Web 服务器的…

基于正点原子电机实验的pid调试助手代码解析(速度环控制)

这里写目录标题 下位机与PID调试助手传输的原理代码讲解(基于正点原子)解析数据接受和数据发送的底层函数数据接受数据帧格式环形数组以及怎么找到它的帧头位置crc校验 数据发送数据上传函数 通过前两节文章&#xff0c;我已经了解了基本的pid算法&#xff0c;现在在完成了电机…

MatrixGate 5.0 性能再升级,加载速度提升三倍!

前言 数据的加载速度在数据处理和分析领域一直是一个挑战&#xff0c;为应对这一挑战&#xff0c;YMatrix 数据库开发了 mxgate 高速数据加载工具。最近&#xff0c;随着 YMatrix 5.0 的 GA&#xff0c;新版 mxgate 与上一版本&#xff08;4.8.1&#xff09;相比&#xff0c;速…

Linux 虚拟机 磁盘扩容

概述 在单台虚拟机上部署了过多服务&#xff0c;导致磁盘使用过度达到98%。 现在扩容提高磁盘容量&#xff0c;增加10G。 现象 df -h df -ih du -s具体步骤 VMware 扩容 关闭虚拟机的情况下执行&#xff0c;类似于生产环境下需要关闭服务器&#xff0c;从而添加硬盘等相关操作…