leetCode-hot100-链表专题

news2024/10/23 4:57:53

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()+" ");
}

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

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

相关文章

Z语言学习——基于通讯案例

目录 1数据类型 2初始状态 3 Alice的消息发送 4 Bob接收与发送消息 5 Alice接收消息 6消息的增删改查 6.1 删除消息 6.2查询消息 6.3修改/增加消息 7定理证明——重要目的 案例背景&#xff1a; (1)构建一个交互式的通讯方案&#xff1b; (2)攻击者控制了所有的通讯…

计算机编码以及URL转码

目录 一、计算机编码 1.ASCII编码 2. GB2312编码 3.GBK编码 4.UTF-8编码 二、URL转码 1.encodeURI和decodeURI 2.encodeURIComponent 和 decodeURIComponent 三、Base64 一、计算机编码 在计算机中&#xff0c;所有的数据在存储和运算时都要使用二进制数表示&#xf…

开源驰骋低代码-积极拥抱AI时代

开源驰骋AI低代码-积极拥抱AI时代 驰骋AI代码开发平台ccfast通过集成人工智能技术&#xff0c;为开发者提供了一系列强大的辅助功能&#xff0c;极大地提升了开发效率和流程体验。以下是针对您列出的功能的详细解释&#xff1a; 概要说明 驰骋低代码开发平台是一款基于云计算和…

【人工智能,机器学习,统计学习,科学表征】开源商用与研发合作

个体工户linjing-lab托管在Github&#xff0c;现公开招募商用与合作人员&#xff0c;目标人群分为以下几个方向&#xff1a; 数学、信息科学、计算机专业的大学高年级学生&#xff0c;熟悉C和面向对象模型&#xff0c;擅长Pybind11编译算子到Python环境。26岁以下的大学本科毕…

Java学习 (四) 面向对象--类与方法

关于面向对象的知识点 1、 java类 以及类成员 属性 方法 构造器 代码块 内部类 2、面向对象特征 封装 继承 多态 3、 其他关键字使用 this \ super \ package \ import \static \ final\ interface\ abstract …

北方高温来袭!动力煤却不涨反跌的原因分析

内容提要 北方高温而南方降雨偏多的格局或将继续&#xff0c;整体水力发电量增长可能继续明显增长&#xff0c;但火电增幅可能继续缩小。5月重点火电厂的发电量和耗煤量增速均呈现负增长&#xff0c;耗煤量月度同比下降7%&#xff0c;而重点水电同比大增近40%。我国电力行业绿…

Apple Phone Memory

Apple Phone Memory 苹果手机内存查询&#xff0c;哪些应用程序&#xff08;app&#xff09;占用内存&#xff1a; 设置 通用 iPhone储存空间 清理下QQ音乐&#xff1a;

灵感互娱U3D笔试题

文章目录 题目1解析 题目2解析 题目3解析 题目4数组链表 题目5解析 题目6解析 题目7解析题目8解析 后话 题目1 以下C#代码的输出顺序是什么 namespace ConsoleApp2 {internal class Program{class A{ public A(string text){Console.WriteLine(text);}}class B{static A a1 …

同城跑腿多合一系统源码小程序支持安卓+IOS+公众号+H5

&#x1f680; 同城跑腿多合一小程序&#xff1a;便捷生活新选择 &#x1f4a8; 一、引言&#xff1a;走进便捷新纪元 在这个快节奏的现代生活中&#xff0c;时间成了最宝贵的财富。而“同城跑腿多合一小程序”正是为了满足大家对于便捷、高效生活的追求而诞生的。它不仅是一款…

高效设计必选!5款好用的UI动效工具

UI 动态设计是应用程序设计的重要组成部分。随着技术的积累&#xff0c;UI设计中的动态效果遍地开花&#xff0c;UI动态效果可以使我们的页面更时尚、更有趣、更人性化。5G网络的快速发展也使美丽的动态效果几乎无缝地嵌入到UI界面中。今天&#xff0c;毫不夸张地说&#xff0c…

豆瓣电影top250网页爬虫

设计思路 选择技术栈:确定使用Python及其相关库&#xff0c;如requests用于发送网络请求&#xff0c;获取网址&#xff0c;用re(正则表达式)或BeautifulSoup用于页面内容解析。设计流程:规划爬虫的基本流程&#xff0c;包括发起请求、接受响应、解析内容、存储数据等环节。模块…

CCF推荐会议必投攻略:这些顶级会议投完直通录取大门

CCF推荐会议必投攻略&#xff1a;这些顶级会议投完直通录取大门&#xff01; 会议之眼 快讯 CCF介绍 CCF&#xff08;China Computer Federation&#xff09;即中国计算机学会&#xff0c;前身是中国电子学会计算机专业委员会&#xff0c;成立于1962年。这是由从事计算机及相…

容器基本概念_从虚拟化技术_到容器化技术_开通青云服务器_并远程连接_容器安装---分布式云原生部署架构搭建007

这一部分,属于以前都会用到的,会快速过一遍,对于关键技术问题会加以说明 https://www.yuque.com/leifengyang/oncloud文档地址在这里,可以看,有些命令可以复制使用 可以看到容器的出现就是 目的就是,让你做的所有的软件,都可以一键部署启动 打包就是docker build 然后: 对于…

Github生成Personal access tokens及在git中使用

目录 生成Token 使用Token-手工修改 使用Token-自动 生成Token 登录GitHub&#xff0c;在GitHub右上角点击个人资料头像&#xff0c;点击Settings → Developer Settings → Personal access tokens (classic)。 在界面上选择点击【Generate new token】&#xff0c;填写如…

ctfshow web七夕杯

web签到 执行命令没有回显&#xff0c;我们直接写文件就可以了 有字符长度限制 ls />a nl /*>a访问url/api/a下载文件 easy_calc <?phpif(check($code)){eval($result."$code".";");echo($result); }function check(&$code){$num1…

Python-gui开发之Pycharm+pyside6/Pyqt6环境搭建

Python-gui开发之Pycharm+pyside6/Pyqt6环境搭建 软件版本一、软件安装1、Python安装2、Pycharm安装3、pyside6或pyqt6安装①安装pyside6②安装PyQt6和pyqt6-tools二、Pycharm项目配置1、插件安装2、新建项目以及环境配置3、包管理安装三、在Pycharm中配置PySide61、pyside6 Qt…

Redis持久化主从哨兵分片集群

文章目录 1. 单点Redis的问题数据丢失问题并发能力问题故障恢复问题存储能力问题 2. Redis持久化 -> 数据丢失问题RDB持久化linux单机安装Redis步骤RDB持久化与恢复示例RDB机制RDB配置示例RDB的fork原理总结 AOF持久化AOF配置示例AOF文件重写RDB与AOF对比 3. Redis主从 ->…

C/C++ struct stat介绍

目录 前言 struct stat struct stat 使用 参考 共勉 前言 本文详细介绍了stat结构体的成员变量&#xff0c;以及使用案例&#xff0c;希望能够帮到您。 struct stat struct stat 结构体用于表示文件或者文件系统对象信息的一种结构体的声明&#xff0c;通常在POSIX&#x…

Gobject tutorial 八

The GObject base class Object memory management Gobject的内存管理相关的API很复杂&#xff0c;但其目标是提供一个基于引用计数的灵活的内存管理模式。 下面我们来介绍一下&#xff0c;与管理引用计数相关的函数。 Reference Count 函数g_object_ref和g_object_unref的…

基于JSP的个人交友网站系统

开头语&#xff1a; 你好&#xff0c;我是计算机学长猫哥。在这个信息时代&#xff0c;个人交友网站为我们提供了一个便捷的社交平台。如果你对本系统感兴趣或有更多需求&#xff0c;欢迎联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技…