leetCode-hot100-链表专题

leetCode-hot100-链表专题

  • 链表简介
  • 单链表
    • 单链表的使用
    • 例题
      • 206.反转链表
      • 19.删除链表的倒数第N个结点
      • 24.两两交换链表中的节点
      • 25.K个一组翻转链表
  • 双向链表
    • 双向链表的使用
  • 循环链表
      • 61.旋转链表
      • 141.环形链表
      • 142.环形链表Ⅱ
  • LinkedList
    • LinkedList的使用

链表简介

参考博客:java数据结构之链表。
链表是一种常见的基础数据结构,它由一系列节点组成,每个节点包含数据域和一个或多个指针域,用于指向其它节点。链表不像数组那样在内存中连续存储,它的节点可以分散地存储在内存中,通过指针连接起来。
链表的主要优点是它对插入和删除操作的效率很高,尤其是当链表的长度变化很大时。在链表中进行插入和删除操作,只需要改变相应节点的指针,而不需要像数组那样进行大量元素的移动。而需要频繁查找元素的场景适合使用顺序表。

链表的主要类型包括:

  • 单向链表:每个节点只包含一个指向下一个节点的指针。
  • 双向链表:每个节点包含两个指针,一个指向前一个节点,一个指向下一个节点。
  • 循环链表:链表中最后一个节点的指针指向第一个节点,形成一个环。
  • 双向循环链表:是双向链表和循环链表的结合,每个节点有两个指针,分别指向前一个节点和后一个节点,并且最后一个节点的指针指向第一个节点。

需要理解的点
关于连续?
链表在逻辑上是连续的,但是物理上并不一定是连续的,链表的结点一般都是从堆上申请,由于每次都是按照一定的分配策略,所以两次申请到的结点可能连续,可能不连续。
关于“头”
链表中的头节点的data域中是空的,一般编码设置头节点都是为了方便后续的操作,不需要进行特殊处理,而且可以简化链表的操作,关于头节点,看到的博文中有个很形象的比喻:带不带头可以简单理解为有人驾驶的列车和无人驾驶的列车,有人驾驶的列车不能在火车头前面加车厢,第一节车厢永远是驾驶室;而无人驾驶的列车则可以在最前面加车厢,哪节车厢在最前面哪节就是头。即head只是用来标识。java数据结构之链表。(这篇博文讲解了链表操作的底层逻辑,文章的内容也是以此为参考,很不戳!建议大家都去读读)
关于“循环”
判断是不是循环就看最后一个结点的next域存放的是null还是第一个结点的地址。

单链表

单链表的使用

方法备注
addFirst(int data)将data插入链表头部
addLast(int data)将data插入链表尾部
addIndex(int index,int data)在下标为index位置插入data,第一个数据节点为0号下标
contains(int key)查找关键字key是否在单链表当中
remove(int key)删除第一次出现关键字为key的节点
removeAllKey(int key)删除所有值为key的节点
size()得到单链表的长度
clear()清空链表
display()从头结点开始遍历,若该结点不等于null就打印输出该结点的元素

例题

206.反转链表

思路:
本题是25.K个一组翻转链表,25题中需要对分段的链表进行翻转,而本题翻转整个链表,直接翻转即可,步骤如下:

  • 初始化两个指针,precurpre开始时为null,因为它需要指向当前节点cur之前的一个节点,而反转开始前没有这样的节点。cur初始化为链表的头部head
  • 进入一个循环,只要cur不是null,就执行循环体。这意味着只要当前节点不是链表的最后一个节点,就继续反转。
  • 在循环内部,首先使用一个临时节点next保存cur的下一个节点。这是必要的,因为一旦cur.next被修改指向pre,我们就失去了对原始下一个节点的引用。
  • 接下来,将cur.next指向pre,这是反转节点的关键步骤。
  • 然后,将pre移动到它的新位置,即当前的cur节点。
  • 最后,将cur移动到下一个节点,即之前保存在next中的节点。

cur变为null时,循环结束,此时pre是新的头节点,因为它指向原始链表的最后一个节点。
时间复杂度:
这个算法的时间复杂度是O(n),其中n是链表的长度。这是因为算法会遍历整个链表一次,每个节点都会被访问一次以进行反转。
代码实现:

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while(cur != null){
            ListNode next = cur.next;

            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}

19.删除链表的倒数第N个结点

思路1
本题采用快慢指针来解决,让快指针比慢指针先走n步,当快指针到达链表尾部时,慢指针所指向的下一个结点就是需要删除的倒数第n个结点,在做链表相关的题目时一般需要在头节点的前面设置一个虚拟的结点,方便边界的判断。详细的视频讲解点击视频讲解-删除链表的倒数第N个结点。
在这里插入图片描述

时间复杂度
时间复杂度为O(n),其中n是链表的长度。代码中有一个循环,循环的次数是链表的长度n,因此时间复杂度为O(n)
代码实现

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0);
        ListNode slow = dummy;
        ListNode fast = dummy;
        dummy.next = head;
        //让快指针先向前走n步
        for(int i = 0;i < n;i++){
            fast=fast.next;
        }
        while(fast.next != null){
            slow = slow.next;
            fast = fast.next;
        }
        //删除慢指针的下一个结点,即需要删除的结点
        slow.next = slow.next.next;
        return dummy.next;
    }
}

思路2:
删除链表的第n个节点比较简单,所以我们可以先将链表翻转,然后删除第n个节点后再次翻转即可,其中翻转节点的操作在25.K个一组翻转链表以及206.反转链表中都有用到,具体的步骤解释详细见206题的解析。
时间复杂度:
时间复杂度为O(n),其中n为链表长度。
代码实现:

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        
        ListNode dummy = new ListNode(0);
        //翻转链表,删除第n个节点
        dummy.next = reserve(head);
        ListNode cur = dummy;
        //循环找到要删除节点的前一个节点
        for(int i = 0; i < n - 1; i++){
            cur = cur.next;
        }
        //删除第n个节点
        cur.next = cur.next.next;
        //翻转链表,得到删除倒数第n个节点的链表
        dummy.next = reserve(dummy.next);
        return dummy.next;
        
    }
    //翻转链表
    private ListNode reserve(ListNode head){
        ListNode pre = null;
        ListNode cur = head;
        while(cur != null){
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
}

24.两两交换链表中的节点

思路
本题采用三指针来解决,首先需要新建一个虚拟头节点,方便操作,然后分别新建三个指针指向虚拟头节点和头节点以及头节点的下一个节点,通过操作三个指针使得节点互换位置,然后将指针整体后移,相同的操作。详细的讲解点击视频讲解-两两交换链表中的节点。
在这里插入图片描述

时间复杂度
时间复杂度为O(n),其中n是链表的长度。
代码实现

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;

        //定义两个指针分别指向虚拟头节点dummy和其下一个节点
        ListNode pre = dummy;
        ListNode cur = dummy.next;
        while(cur != null && cur.next != null){
            //定义第三个指针
            ListNode next = cur.next;
            pre.next = next;
            cur.next = next.next;
            next.next = cur;
            pre = cur;
            cur = cur.next;
        }
        return dummy.next;
    }
}

25.K个一组翻转链表

思路
本题采用的思路是,先将链表按K个节点一组分割,然后单独对每一组进行翻转,最后再拼接起来即可,翻转函数逻辑比较简单,就是采用三个指针,每次让中间的指针(刚开始指向头节点)指向其前一个指针并整体后移,重复操作。分割和拼接的操作其实也很简单,如果看代码不太明白,可以画个链表跟着操作走一遍,就会明白每一步的含义,代码里有注释,希望对你有用,详细的视频讲解点击视频讲解-K个一组翻转链表。
时间复杂度
时间复杂度为O(n),其中n是链表的长度。代码中有一个while循环,每次循环都会处理k个节点(反转、拼接),所以循环的次数最多为n/k。在循环中,反转操作的时间复杂度为O(k),拼接操作的时间复杂度为O(1)。因此,总的时间复杂度可以近似为O(n)
代码实现

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        //定义首尾指针指向dummy
        ListNode start = dummy;
        ListNode end = dummy;
        //按K个一组分割链表
        while(true){
            for(int i = 0 ;i < k && end != null;i++) end = end.next;
            if(end == null) break;
            //分割后的链表的头节点
            ListNode startNext = start.next;
            //分割后的剩余链表的头节点
            ListNode endNext = end.next;

            //分割前K个节点
            end.next = null;
            //对前K个节点的链表进行翻转
            start.next = reverse(start.next);
            //将翻转后的链表的与剩余的节点拼接到一起
            startNext.next = endNext;
            //更新头尾节点,开始下一轮的翻转
            start = end = startNext;
        }
        return dummy.next;
    }

    //定义反转函数
    private ListNode reverse(ListNode head){
        ListNode cur = head;
    ListNode pre = null;
    while(cur != null){
        ListNode next = cur.next;
        //每次cur指针指向前一个节点,达到翻转的目的,然后将三个指针后移,重复操作,直到cur为空
        cur.next = pre;
        pre = cur;
        cur = next;
    }
    return pre;
    } 
}

双向链表

双向链表相当于是单链表中的一种改进,在单链表的基础上增加了一个前驱结点来存放前一个结点的位置,这使得某些操作会更加简单。

双向链表的使用

双向链表的使用和单链表基本相同。

方法备注
addFirst(int data)将data插入链表头部
addLast(int data)将data插入链表尾部
addIndex(int index,int data)在下标为index位置插入data,第一个数据节点为0号下标
contains(int key)查找关键字key是否在单链表当中
remove(int key)删除第一次出现关键字为key的节点
removeAllKey(int key)删除所有值为key的节点
size()得到单链表的长度
clear()清空链表
display()从头结点开始遍历,若该结点不等于null就打印输出该结点的元素

循环链表

1.循环链表的表尾节点指针next指向头结点
2.循环链表从一个节点出发可以找到其他任意一个节点
3.循环链表的判空操作和普通单链表的判空方法基本相同,不同之处在于循环链表的判空是判断头结点的指针是否指向头结点,而普通单链表的判空是判断头结点的指针是否指向null
4.判断节点n是否为循环单链表的表尾节点和普通单链表的判断方法基本相同,不同之处在于循环链表的判空是判断n节点的指针是否指向头结点,而普通单链表的判空是判断n节点的指针是否指向null。

61.旋转链表

思路:
(1)成环:

  • 首先检查链表是否为空,如果为空,则直接返回null
  • 定义一个指针cur,初始指向头节点head
  • 遍历链表,计算链表的长度n,并将链表的最后一个节点指向头节点,使链表形成一个环。

(2)找到需要分段的节点:

  • 再次遍历链表,这次是为了找到新的头节点位置。由于旋转次数k可能大于链表长度n,因此使用k % n来获取实际需要旋转的次数。
  • 循环n - k % n次,找到旋转后链表的头节点的前一个节点,即cur

(3)改变头节点的位置,断开环:

  • cur.next设置为新的头节点head
  • cur.next设置为null,断开环,使链表恢复为线性结构。

(4)返回头节点:
最后返回新的头节点head,此时链表已经完成了旋转操作。视频讲解点击视频讲解-旋转链表。
在这里插入图片描述

时间复杂度:
时间复杂度为O(n),其中n为链表的长度。
代码实现:

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        if(head == null) return null;
        //1.成环
        ListNode cur = new ListNode();
        cur = head;
        int n = 1;
        while(cur.next != null){
            cur = cur.next;
            n++;
        }
        cur.next = head;
        //2.找到需要分段的节点
        for(int i = 0;i < n - k % n;i++){
            cur = cur.next;
        }
        //3.改变头节点的位置,断开环
        head = cur.next;
        cur.next = null;
        return head;
    }
}

141.环形链表

思路:
本题主要是下面两个思想:
快慢指针: 通过使用两个速度不同的指针在链表中遍历,可以有效地检测环的存在。慢指针每次走一步,快指针每次走两步。
相遇点: 如果链表中有环,快指针会在环中追上慢指针,最终导致两者相遇。如果没有环,快指针会在到达链表末尾时终止。
为什么快指针不能每次移动三步或者更多呢?快指针每次移动两步,慢指针每次移动一步。这种配置保证了快指针的速度是慢指针的两倍。这样,在最坏的情况下,当链表中存在环时,快指针和慢指针将在环中相遇,在这种情况下,相遇所需的步数最多是环长度的两倍。
如果快指针每次移动三步或更多步,相遇的模式变得更复杂,需要更多的步骤来分析和证明算法的正确性。同时,更快的速度可能会跳过慢指针,特别是在环比较小的情况下,增加了算法的复杂性。视频讲解点击视频讲解-环形链表。
时间复杂度
时间复杂度为O(n),其中n为链表长度。
代码实现:

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;

        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if(slow == fast){
                return true;
            }
        }
        return false;
    }
}

142.环形链表Ⅱ

思路:
算法思想:
(1)初始化:

  • 使用两个指针 fastslow,它们都从链表的头节点 head 开始。

(2)检测环的存在:

  • 在一个 while 循环中,fast 指针每次移动两步,slow 指针每次移动一步。
  • 如果链表中存在环,fastslow 指针最终会在环内相遇(相同的节点)。

(3)寻找环的起点:

  • 一旦 fastslow 指针相遇,将 slow 指针重新指向链表的头节点,同时保持 fast 指针在相遇点。
  • 这时,slowfast 指针每次都移动一步。当它们再次相遇时,相遇点就是环的起始节点。

证明:
在这里插入图片描述
由上面的证明可知,当slowfast第一次相遇时,此时让slow回到headfast从第一次相遇的点同步出发,最终二者会在环的入点相遇(fast回到headslow在第一次相遇点出发是同样的效果)。

视频讲解点击视频讲解-环形链表Ⅱ。
时间复杂度:
时间复杂度为O(m+n),其中m为链表非环部分的长度,n为环的长度,无论链表的非环部分和环的长度如何,算法的运行时间都与环的长度成线性关系,所以时间复杂度可以简化为 O(n)
代码实现:

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;

        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            //第一次相遇,将slow置为头节点
            if(slow == fast){
                slow = head;
                while(slow != fast){
                    slow = slow.next;
                    fast = fast.next;
                }
                return slow;
            }
        }
        return null;
    }
}

LinkedList

LinkedList底层使用了双向链表并实现了List接口,不支持随机访问,但是在任意位置插入和删除元素的效率很高,适合需要频繁插入和删除元素的场景。

LinkedList的使用

//创建一个空的LinkedList
List<Integer> list = new LinkedList<>()

常用方法:

方法备注
add(e)在尾部插入e
add(int index, e)将e插入index位置
addAll(C)将C中的元素全部插入尾部
remove(int index)删除 index 位置元素
remove(Object o)删除遇到的第一个 o
get(int index)获取下标 index 位置元素
set(int index, e)将下标 index 位置元素设置为 e
clear()清空
contains(Object o)判断 o 是否在线性表中
indexOf(Object o)返回第一个 o 所在下标
lastIndexOf(Object o)返回最后一个 o 的下标
subList(int fromIndex, int toIndex)截取部分 list

三种遍历方法

//使用forEach遍历
for(int item : list){
    System.out.print(e + " ");
}
//使用迭代器遍历-正向
ListIterator<Integer> i1 = list.listIterator();
while(i1.hasNext()){
    System.out.print(i1.next+" ");
}
//使用迭代器遍历-反向
ListIterator<Integer> i2 = list.listIterator(list.size());
while (i2.hasPrevious()) {
    System.out.print(i2.previous()+" ");
}

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

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

相关文章

泛微开发修炼之旅--21关于泛微Ecology实现CAS服务端支持内网认证重定向至内网,外网认证重定向至外网的解决方案

文章详情链接&#xff1a;http://21关于泛微Ecology实现CAS服务端支持内网认证重定向至内网&#xff0c;外网认证重定向至外网的解决方案

秋招突击——6/16——复习{(单调队列优化DP)——最大子序和,背包模型——宠物小精灵收服问题}——新作{二叉树的后序遍历}

文章目录 引言复习&#xff08;单调队列优化DP&#xff09;——最大子序和单调队列的基本实现思路——求可移动窗口中的最值总结 背包模型——宠物小精灵收服问题思路分析参考思路分析 新作二叉树的后续遍历加指针调换 总结 引言 复习 &#xff08;单调队列优化DP&#xff09…

修改yarn、npm、pnpm为国内镜像源

国内由于网络的原因&#xff0c;使用官方的npm、yarn、pnpm访问下载依赖库会很慢&#xff0c;有时候还会出现无法访问的情况&#xff0c;这时候就需要我们给npm、yarn、pnpm换一个国内的镜像源的&#xff0c;一般的我们可以将镜像换成淘宝的源&#xff0c;由于平时比较常用到的…

Vue65-组件之间的传值

1、收数据 2、传数据 3、批量的数据替换 若是info里面有四个数据&#xff0c;传过来的dataObj里面有三个数据&#xff0c;则info里面也只有三个数据了 解决方式&#xff1a; 该写法还有一个优势&#xff1a;传参的时候&#xff0c;顺序可以随意&#xff01;

谈论实时摄像头的稳定性,防抖算法

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

C语言的基本输入输出函数+构造类型数据——数组

C语言的基本输入输出函数 1. 字符输入输出函数 getchar()、putchar() getchar()&#xff1a;从标准输入&#xff08;通常是键盘&#xff09;读取一个字符&#xff0c;并返回其ASCII值。putchar()&#xff1a;将指定的字符&#xff08;由其ASCII值表示&#xff09;写入标准输出…

模版与策略模式

一&#xff0c;怎么选择 如果需要固定的执行流程&#xff0c;选模版 如果不需要固定的执行流程&#xff0c;只需要对一个方法做具体抽象&#xff0c;选策略 参考文章&#xff1a; 常用设计模式汇总&#xff0c;告诉你如何学习设计模式 二&#xff0c;常用写法 子类 exten…

龙讯旷腾PWmat计算vdW异质结中热载流子冷却 | 复刻《Phys. Chem. Chem. Phys 》文献

01 NAMD 背景介绍 在各类光物理与光化学过程当中&#xff0c;均会牵涉到激发态载流子动力学过程&#xff0c;诸如电荷弛豫、复合以及输运等等。光激发或者电子注入将初始的平衡状态打破&#xff0c;所产生的热载流子在其演化进程中&#xff0c;会与原子核产生强烈耦合。此时&a…

关于车规级功率器件热可靠性测试的分享

随着中国电动汽车市场的稳步快速发展和各大车企布局新能源的扩散&#xff0c;推动了车规级功率器件的快速增长。新能源汽车行业和消费电子都会用到半导体芯片&#xff0c;但车规级芯片对外部环境要求很高&#xff0c;涉及到的一致性和可靠性均要大于工业级产品要求&#xff0c;…

利用Systemverilog+UVM搭建SOC及ASIC的RTL验证环境

在集成电路设计的复杂世界中&#xff0c;验证环节是确保设计满足预期功能和性能要求的关键步骤。随着系统级芯片&#xff08;SOC&#xff09;和特定应用集成电路&#xff08;ASIC&#xff09;的规模和复杂性不断增加&#xff0c;传统的验证方法已经难以满足高效、准确的验证需求…

基于顺序表的排序

任务内容 基于一个顺序表&#xff0c;实现如下排序算法&#xff1a; 直接插入排序&#xff1b;交换排序 &#xff08;冒泡&#xff09;&#xff1b;简单选择排序 代码实现 #include<iostream> #include<string> using namespace std; #define keytype int type…

Javaweb登录校验

登录校验 JWT令牌的相关操作需要添加相关依赖 <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>一、摘要 场景&#xff1a;当我们想要访问一个网站时&am…

真空玻璃可见光透射比检测 玻璃制品检测 玻璃器皿检测

建筑玻璃检测 防火玻璃、钢化玻璃、夹层玻璃、均质钢化玻璃、平板玻璃、中空玻璃、真空玻璃、镀膜玻璃夹丝玻璃、光栅玻璃、压花玻璃、建筑用U形玻璃、镶嵌玻璃、玻璃幕墙等 工业玻璃检测 钢化安全玻璃、电加温玻璃、玻璃、半钢化玻璃、视镜玻璃、汽车安全玻璃、汽车后窗电热…

Docker+MySQL:打造安全高效的远程数据库访问

在现代应用开发和部署中&#xff0c;数据库是关键组件之一。无论是开发环境还是生产环境&#xff0c;快速、可靠地部署和管理数据库都是开发人员和运维人员面临的常见挑战之一。 Docker是一种流行的容器化技术&#xff0c;它使得应用程序的部署和管理变得非常简单和高效。通过使…

大数据-数据分析初步学习,待补充

参考视频&#xff1a;数据分析只需3小时从入门到进阶&#xff08;up亲身实践&#xff09;_哔哩哔哩_bilibili 数据指标&#xff1a; 对当前业务有参考价值的统计数据 分类&#xff1a;用户数据&#xff0c;业务数据&#xff0c;行为数据 用户数据 存量&#xff1a; DAU&#…

CSS3中鲜为人知但非常强大的 Clip-Path 属性

CSS3中鲜为人知但非常强大的 Clip-Path 属性 在CSS3中,clip-path属性可以让我们快速创建各种各样的不规则图形,而无需使用图片或者复杂的绘图工具。它可以帮助我们实现一些非常出色的视觉效果,但遗憾的是它并不是很常见。 clip-path属性可以接受多种不同的值,比如polygon()、…

Matlab终于能够实现Transformer预测了

声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ 目录 原理简介 数据介绍 结果展示 完整代码 今天…

Day9—Spark运行模式及RDD的创建

Spark概述 大数据开发的总体架构 可以看到&#xff0c;在数据计算层&#xff0c;作为Hadoop核心组成的MapReduce可以结合Hive通过类SQL的方式进行数据的离线计算&#xff08;当然也可以编写独立的MapReduce应用程序进行计算&#xff09;&#xff1b;而Spark既可以做离线计算&a…

金属配件加工厂设备远程监控

随着科技的飞速发展&#xff0c;智能制造已成为制造业转型升级的重要方向。在金属配件加工领域&#xff0c;设备的稳定运行和高效管理对于提升产品质量、降低生产成本至关重要。HiWoo Cloud平台凭借其强大的远程监控功能&#xff0c;为金属配件加工厂提供了全新的解决方案&…

RabbitMQ详解-06RabbitMQ高级

1. 过期时间TTL 可以对消息设置预期的时间&#xff0c;在这个时间内都可以被消费者接收获取&#xff1b;过了之后消息自动被删除。RabbitMQ可以对消息和队列设置TTL。有以下两种设置方法&#xff1a; 通过队列属性设置&#xff0c;队列中所有消息都有相同的过期时间。对消息进…