【Leetcode】单链表常见题

Alt

🔥个人主页Quitecoder

🔥专栏Leetcode刷题

Alt

本节内容我们来讲解常见的几道单链表的题型,文末会赋上单链表增删查,初始化等代码

目录

  • 1.移除链表元素
  • 2.链表的中间节点
  • 3.返回倒数第K个节点:
  • 4.环形链表(判断)
  • 5.环形链表(判断加返回)
      • 5.1环的起始节点推导过程
  • 6.相交链表
  • 7.随机链表的复制
  • 8.反转链表
      • 方法一:迭代法
      • 方法二:递归法
  • 9.合并两个有序链表

1.移除链表元素

题目链接: 203.移除链表元素
题目描述在这里插入图片描述

首先,这道题需要删除元素,我可以初始化一个结构体指针cur进行遍历链表,对于每个节点,检查它的值是否等于val如果cur指向的节点值等于val,则需要删除这个节点,这里一个结构体指针是不够的,是因为单链表的单向性,我们则需要再定义另一个指针prev来实现

首先,定义并初始化两个结构体指针:

struct ListNode* cur = head;
struct ListNode* prev = NULL;

定义两个指针cur(当前节点指针)和prev(前一个节点指针)。cur初始化为指向头节点head,而prev初始化为NULL

在这个删除链表中指定值节点的函数中,prev指针被初始化为NULL是出于以下几个原因:

  1. 表示头节点之前:链表的头节点之前没有节点,所以在遍历开始之前,prev指向“虚拟的”头节点之前的位置,这在逻辑上用NULL表示

  2. 处理头节点可能被删除的情况:如果链表的头节点(第一个节点)就是需要删除的节点,那么在删除头节点后,新的头节点将是原头节点的下一个节点。因为头节点没有前一个节点,所以使用NULL作为prev的初始值可以帮助我们处理这种情况。在代码中,如果发现头节点需要被删除(cur->val == valprev == NULL),就将头节点更新为下一个节点

  3. 简化边界条件的处理:通过将prev初始化为NULL,我们可以用统一的方式处理需要删除的节点是头节点的情况和位于链表中间或尾部的情况。这样,当prev不是NULL时,就意味着我们不在头节点,可以安全地修改prev->next来跳过需要删除的cur节点

紧接着进行遍历过程:


while (cur != NULL) {
    if (cur->val == val) {
        struct ListNode* next = cur->next;
        free(cur);
        if (prev != NULL) {
            prev->next = next;
        }
        else
        {
            head = next;
        }
        cur = next;
    }
    else {
        prev = cur;
        cur = cur->next;
    }
}
  • 如果cur指向的节点值等于val,则需要删除这个节点。首先,保存cur的下一个节点到临时变量next中。如果prev不是NULL(即当前节点不是头节点),则将prev->next设置为next,跳过当前节点,从而将其从链表中删除。如果prevNULL即当前节点是头节点),则需要更新头节点headnext
  • 释放(删除)当前节点cur所占用的内存。
  • cur更新为next,继续遍历链表

节点值不等于val:如果当前节点值不等于val,则curprev都前进到下一个节点

2.链表的中间节点

题目链接: 876.链表的中间节点
题目描述在这里插入图片描述

我们这道题用到了快慢指针的思路:
设置一个快指针,一次走两步,慢指针一次走一步,当节点个数为奇数时,意味着我的快指针指向尾节点,慢指针指向中间节点,此时的判断条件为快指针节点的next指针指向空
当节点个数为偶数时,意味着当我快指针刚好为空时,慢指针走到中间第二个节点,所以代码如下:

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    while (fast != NULL && fast->next != NULL) {
        fast = fast->next->next;
        slow = slow->next;
    }
    return slow;
}

注意

来看这个判断条件:

while (fast != NULL && fast->next != NULL)

这里能不能交换呢?:

while (fast->next != NULL && fast != NULL)

上面的代码片段错误之处在于 while 循环中条件判断的顺序。特别是在判断 fast 不为 NULL 以及 fast->next 不为 NULL 的时候

问题在于,当循环检查条件 fast->next != NULL && fast != NULL 时,它首先检查 fast->next 是否不为 NULL。如果 fast 本身是 NULL,那么尝试访问 fast->next 将会导致未定义行为(通常是一个访问违规错误,导致程序崩溃)。这是因为你试图访问一个 NULL 指针的成员,这在 C 和 C++ 中是不合法的。

正确的方式是首先检查 fast 是否为 NULL,然后再检查 fast->next 是否不为 NULL。这确保了代码不会试图在 NULL 指针上进行成员访问

3.返回倒数第K个节点:

题目链接: 面试题02.02.返回倒数第K个节点
题目描述在这里插入图片描述

简单思路:

设置两个指针,一个先走k步,再两个指针同时前移直到前一个指向空

int kthToLast(struct ListNode* head, int k)
{
    struct ListNode*p1=head;
    struct ListNode*p2=head;
    while(k--)
    {
        p1=p1->next;
    }
    while(p1!=NULL)
    {
        p1=p1->next;
        p2=p2->next;
    }
    return p2->val;
}

4.环形链表(判断)

题目链接: 141.环形链表
题目描述在这里插入图片描述

龟兔赛跑算法
设置快指针一次前行两步,慢指针一次一步,若有环,则两个指针一定相遇:

bool hasCycle(struct ListNode *head) {
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while (fast != NULL && fast->next != NULL) {
        fast = fast->next->next; // 快指针每次前进两步
        slow = slow->next; // 慢指针每次前进一步
        if (fast == slow) { // 如果快慢指针相遇,表示链表有环
            return true;
        }
    }
    // 遍历完成没有找到环,返回 false
    return false;
}

简单证明:当两个指针都入环时,快指针开始追赶慢指针,速度相差一,相对移动的距离为1,则一定能追上

5.环形链表(判断加返回)

题目链接: 142.环形链表II
题目描述在这里插入图片描述

环形链表中寻找环的起始节点的算法是基于“快慢指针”策略。这个算法分为两个主要阶段:

  1. 确定链表中是否存在环
    使用两个指针,slowfast,它们初始时都指向链表的头节点head。然后,slow每次向前移动一个节点,而fast每次向前移动两个节点。如果链表中存在环,那么fast指针最终会再次与slow指针相遇(因为fast指针会从后面追上slow指针)。如果在任何时候fast指针遇到NULL(表示链表尾部),则链表中不存在环。

  2. 找到环的起始节点
    slowfast指针相遇时,我们可以确定链表中存在环。但要找到环的起始节点,我们可以使用下面的方法:

    • slowfast首次相遇后,将一个指针(比如slow2)放置在链表的起始处head,而将slow保留在相遇点。
    • 然后同时将slow2slow每次向前移动一个节点,直到它们相遇。它们相遇的节点就是环的起始节点。

5.1环的起始节点推导过程

假设环外的长度(从头节点到环起始节点的长度)是L,从环起始节点到slowfast首次相遇点的长度是S,环的剩余长度是R。因此,环的总长度C = S + R
**在这里插入图片描述**

slowfast首次相遇时:

  • slow指针走过的长度是L + S
  • fast指针走过的长度是L + S + nC,其中nfast指针在环中绕行的次数。

因为fast指针走的距离是slow指针的两倍,所以我们有:

[2(L + S) = L + S + nC]

通过简化这个方程,我们得到:

[L + S = nC]

或者

[L = nC - S]

这个方程告诉我们从头节点到环的起始节点的距离L等同于从首次相遇点继续前进直到再次回到环起始节点的距离(即n圈环长度减去首次相遇点到环起始节点的距离S)。这就是为什么当我们将一个指针放在链表头部,另一个保留在首次相遇点,它们以相同的速度移动时,它们相遇的点就是环的起始节点

struct ListNode *detectCycle(struct ListNode *head) {
    struct ListNode *slow = head, *fast = head;
    
    while (fast != NULL && fast->next != NULL) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) { 
            struct ListNode *slow2 = head;
            while (slow2 != slow) {
                slow = slow->next;
                slow2 = slow2->next;
            }
            return slow; 
        }
    }
    
    return NULL; 
}

6.相交链表

题目链接: 160.相交链表
题目描述在这里插入图片描述

思路:
相交链表指的是两个链表在某一点开始合并成一个链表。这意味着从相交点到链表的末尾,这两个链表都具有相同的节点

解决相交链表问题的一个有效方法是使用两个指针遍历两个链表。以下是实现这一思路的步骤:

  1. 创建两个指针

创建两个指针,p1p2,分别指向两个链表的头节点

  1. 同步遍历链表

同时移动两个指针,每步向前移动一次。如果一个指针到达链表末尾,则将其移动到另一个链表的头节点继续遍历。这样,两个指针会分别遍历两个链表的节点

  1. 相遇点或结束
    • 如果两个链表相交,p1p2会在相交点相遇。这是因为p1p2会遍历整个结构(两个链表的总长度),这样调整确保它们最终会有相同的遍历长度。当它们移动到相交点时,由于它们步调一致,因此会同时到达相交点。
    • 如果链表不相交,p1p2最终都会到达各自链表的末尾并同时为NULL这意味着它们没有相交点

假设链表A的非共享部分长度为a,链表B的非共享部分长度为b,两个链表的共享部分长度为c。当p1p2遍历完各自的链表后,它们会分别遍历对方的链表,所以它们各自遍历的总长度是a + c + b。这意味着无论ab的长度差异如何,它们最终会同时到达相交点或链表的末尾。这个方法的优点是,它不需要知道两个链表的长度,也不需要额外的存储空间,只需要两个指针即可解决问题

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
     if (headA == NULL || headB == NULL) {
        return NULL;
    }
    struct ListNode *pA = headA, *pB = headB;
    while (pA != pB) {
        pA = pA == NULL ? headB : pA->next;
        pB = pB == NULL ? headA : pB->next;
    }
    return pA;
}

7.随机链表的复制

题目链接: 138.随机链表的复制
题目描述在这里插入图片描述

思路:

  1. 遍历原链表,为每个原节点创建一个新节点:这个新节点有相同的值,并将其插入到原节点和下一个原节点之间。
if (!head) return NULL;  
    struct Node* curr = head;
    while (curr) {
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        newNode->val = curr->val;
        newNode->next = curr->next;
        newNode->random=NULL;
        curr->next = newNode;
        curr = newNode->next;
    }
  1. 更新新节点的random指针:由于每个新节点都紧跟在其对应的原节点后面,可以通过原节点的random指针找到新节点的random指针应该指向的节点
 curr = head;
    while (curr) {
        if (curr->random) {
            curr->next->random = curr->random->next;
        }
        curr = curr->next->next;
    }
  1. 将混合链表拆分为原链表和复制链表:恢复原链表,并提取出复制链表
struct Node* pseudoHead = (struct Node*)malloc(sizeof(struct Node));
    pseudoHead->next = head->next;
    struct Node* copyCurr = pseudoHead->next;
    curr = head;
    while (curr) {
        curr->next = curr->next->next;
        if (copyCurr->next) {
            copyCurr->next = copyCurr->next->next;
        }
        curr = curr->next;
        copyCurr = copyCurr->next;
    }

    struct Node* copiedHead = pseudoHead->next;
    free(pseudoHead); 
    return copiedHead;

解释

  • 第一步:遍历原链表,对于每个节点创建一个新节点,将新节点插入原节点和原节点的下一个节点之间。

  • 第二步:再次遍历链表,这次是为了设置新节点的random指针。因为每个新节点都位于其对应的原节点之后,可以通过原节点的random指针直接找到对应新节点的random目标节点。

  • 第三步:将原链表和复制的链表分离。在这一步中,恢复原始链表的next指针,并将复制链表的next指针指向正确的节点

所以这道题只是逻辑复杂一点,并没有很难理解

8.反转链表

题目链接: 206.反转链表
题目描述在这里插入图片描述

方法一:迭代法

迭代法通过遍历链表,逐个改变节点的指向来实现链表的反转。其基本思路如下:

  1. 初始化三个指针prev(指向当前节点的前一个节点,初始为NULL),curr(指向当前节点,初始为链表的头节点head),next(临时保存curr的下一个节点)

  2. 遍历链表:在遍历过程中,逐个节点地改变指向,直到currNULL

  3. 更新指针:在每次迭代中,首先保存curr的下一个节点(next = curr->next),然后改变curr的指向(curr->next = prev)。之后,移动prevcurr指针前进一步(prev = currcurr = next

  4. 更新头节点:当遍历完成,currNULL时,prev指向的是新的头节点

struct ListNode* reverseList(struct ListNode* head) 
{
    if(head==NULL)
    return NULL;
    struct ListNode*prev,*cur,*_next;
    prev=NULL;
    cur=head;
    _next=head->next;

    while(cur)
    {
      cur->next=prev;
      prev=cur;
      cur=_next;
      if(_next)
         _next=_next->next;
    }
    return prev;
}

方法二:递归法

递归法利用递归回溯的过程实现链表的反转。其基本思路如下:

  1. 递归基:如果链表为空或只有一个节点,直接返回当前节点作为新的头节点。

  2. 递归步骤:对于链表head->...->n1->n2->...->null,假设从n1开始的链表已经被成功反转,即head->n1<-n2<-...<-newHead。我们的目标是将head节点放到最后,即n1->head->null并将n1next设置为null

  3. 执行反转:递归调用自身,传入head->next作为新的链表头,直到链表末尾。然后设置head->next->next = head(这实现了反转),再将head->next设置为null(断开原来的连接)

  4. 返回新的头节点:递归的最深处将返回新的头节点,每层递归都返回这个头节点,最终实现整个链表的反转

struct ListNode* reverseListRecursive(struct ListNode* head) {
    // 递归基:如果链表为空或只有一个节点,没有反转的必要
    if (head == NULL || head->next == NULL) {
        return head;
    }

    // 递归步骤:假设head->next之后的链表已经被成功反转了
    struct ListNode* newHead = reverseListRecursive(head->next);

    // head->next此时指向反转后的链表的最后一个节点
    // 将其next指针指回head,完成对head节点的反转
    head->next->next = head;

    // 断开原来head指向head->next的指针,防止形成环
    head->next = NULL;

    // 每一层递归返回新的头节点
    return newHead;
}

9.合并两个有序链表

题目链接: 21.合并两个有序链表
题目描述在这里插入图片描述

这里与我们归并排序的思路相似,设置两个指针分别遍历两个链表,取元素插入到新链表中,直到某个链表遍历完成

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    struct ListNode *new ;

    new->val = 0;
    new->next = NULL;
    struct ListNode *p3 = new;
    struct ListNode *p1 = list1, *p2 = list2;

    while (p1 && p2) {
        if (p1->val < p2->val) {
            p3->next = p1;
            p1 = p1->next;
        } else {
            p3->next = p2;
            p2 = p2->next;
        }
        p3 = p3->next;
    }

    p3->next = p1 ? p1 : p2;

    struct ListNode *result = new->next; // 保存合并后链表的头节点
    free(new); // 释放new节点占用的内存
    return result; // 返回合并后的链表头节点
}

p3->next = p1 ? p1 : p2;这一步也是后面的关键,我不知道哪个链表遍历完,剩余一个链表还剩元素,我就需要将剩下的元素整体接入新链表中,这里就用三目运算符,如果p1不为空,则指向p1剩余元素,如果p1为空,则指向p2

本节内容到此结束,感谢大家阅读!!!

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

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

相关文章

【Java - 框架 - Lombok】(1) 普通Java项目通过Lombok+Logback完成日志的创建使用 - 快速上手

普通Java项目通过"Lombok""Logback"完成日志的创建使用 - 快速上手&#xff1b; 步骤A 说明 创建"Maven"项目&#xff1b; 图片 步骤B 说明 添加相关依赖项&#xff1b; 图片 代码 <!-- "Lombok"依赖项--> <dependency>&…

C语言--动态内存管理

为什么存在动态内存管理&#xff1f; 在之前我们讲到的类型创建的变量再空间开辟好之后就不能再改变了&#xff0c;但是有时候我们希望能够自由地为某个变量分配空间&#xff0c;就比如上一篇文章中的通讯录&#xff0c;在没有动态内存管理情况下&#xff0c;我们就只能为通讯…

18.字面量

文章目录 一、字面量二、区分技巧三、扩展&#xff1a; /t 制表符 一、字面量 在有些资料&#xff0c;会把字面量说成常量、字面值常量&#xff0c;这种叫法都不是很正确&#xff0c;最正确的叫法还是叫做&#xff1a;字面量。 作用&#xff1a;告诉程序员&#xff0c;数据在…

地物波谱库共享网站汇总

ENVI自5.2版本重新梳理了原有的标准波谱库&#xff0c;新增一些物质波谱&#xff0c;在ENVI5.6中存放在…\Harris\ENVI56\ resource\speclib&#xff0c;分别存放在四个文件夹中&#xff0c;储存为ENVI波谱库格式&#xff0c;有两个文件组成&#xff1a;.sli和.hdr。 ENVI保留…

小米还涉足了哪些领域

小米作为一家全球性的移动互联网企业&#xff0c;其业务领域相当广泛&#xff0c;除了核心的智能手机业务外&#xff0c;还涉足了许多其他领域。以下是对小米涉足领域的简要介绍&#xff1a; 智能硬件与IoT平台&#xff1a;小米是全球领先的智能硬件和IoT平台公司&#xff0c;致…

linux:线程同步

个人主页 &#xff1a; 个人主页 个人专栏 &#xff1a; 《数据结构》 《C语言》《C》《Linux》 文章目录 前言线程同步条件变量接口简单示例pthread_cond_wait为什么要有mutex伪唤醒问题的解决 (if->while) 总结 前言 本文作为我对于线程同步知识总结 线程同步 同步&…

资讯头条P3自媒体搭建

自媒体素材管理与文章管理 一.后台搭建 1.1 搭建自媒体网关 导入网关模块>>>在网关模块的pom.xml文件中添加该子模块>>>刷新maven <modules><module>heima-leadnews-app-gateway</module><!--新增--><module>heima-leadnew…

Aurora插件安装

介绍 Latext是一种基于TEX的排版系统。 CTeX中文套装是基于Windows下的MiKTeX系统&#xff0c;集成了编辑器WinEdt和PostScrip处理软件Ghostscript和GSview等主要工具。CTeX中文套装在MikTeX的基础上增加了对中文的完整支持。 CTeX&#xff1a; CTeX套装 - CTEX 下载安装 然后…

【Java程序设计】【C00345】基于Springboot的船舶监造管理系统(有论文)

基于Springboot的船舶监造管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 项目获取 &#x1f345;文末点击卡片获取源码&#x1f345; 开发环境 运行环境&#xff1a;推荐jdk1.8&#xff1b; 开发工具&#xff1a;eclipse以及i…

Day50:WEB攻防-PHP应用文件包含LFIRFI伪协议编码算法无文件利用黑白盒

目录 文件包含-原理&分类&利用&修复 文件读取 文件写入 代码执行 远程利用思路 黑盒利用-VULWEB 白盒利用-CTFSHOW-伪协议玩法 78-php&http协议 79-data&http协议 80-81-日志包含 87-php://filter/write&加密编码 88-data&base64协议 …

韩顺平Java | C21网络编程

1 网络的相关概念 ip地址的组成&#xff1a;网络地址 主机地址 A类&#xff1a;0 ~ 2^7-1 0 ~ 127 B类&#xff1a;128 ~ 1282^6-1 128 ~ 191 C类&#xff1a;192 ~ 1922^5-1 192 ~ 223 D类&#xff1a;224 ~ 2242^4-1 224 ~ 239 E类&#xff1a;240 ~ 2402^3-1 240 ~ 2…

Unity PS5开发 天坑篇 之 URP管线与HDRP管线部署流程以及出包介绍04

目录 一, URP管线、HDRP管线下的Unity项目部署 1. PS5开发论坛关于Unity可支持的版本说明: 2. URP管线下的项目与部署 2.1 Build PS5 URP Project 2.2 运行画面 3. HDRP管线下的项目与部署 3.1 附上可以运行的画面: 4. PS5打包方式介绍 4.1 PC串流调试模式: Build Typ…

selenium自动化测试

selenium自动化测试 1、Javaselenium环境搭建2、测试&#xff0c;打开任意网页3、selenium 常见的Api3.1元素定位findElement3.1.1 css 选择语法3.1.2 xpath 选择语法 1、Javaselenium环境搭建 下载chromedriver&#xff0c;版本要与Chrome浏览器版本一致。 下载之后将chro…

算法打卡day25|回溯法篇05|Leetcode 491.递增子序列、46.全排列、47.全排列 II

算法题 Leetcode 491.递增子序列 题目链接:491.递增子序列 大佬视频讲解&#xff1a;递增子序列视频讲解 个人思路 和昨天的子集2有点像&#xff0c;但昨天的题是通过排序&#xff0c;再加一个标记数组来达到去重的目的。 而本题求自增子序列&#xff0c;是不能对原数组进行…

springboot检测脚本

import requests import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) session requests.session()# 从文本文件中读取 with open(dic.txt, r) as file:paths file.readlines()# 移除每个末尾的换行符 paths [path.strip() for path in pa…

线程创建方式、构造方法和线程属性

欢迎各位&#xff01;&#xff01;&#xff01;推荐PC端观看 文章重点&#xff1a;学会五种线程的创造方式 目录 1.开启线程的五种方式 2.线程的构造方法 3.线程的属性及获取方法 1.开启线程的五种方式 创造线程的基本两步&#xff1a;&#xff08;1&#xff09;使用run方法…

并发编程之Callable方法的详细解析(带小案例)

Callable &#xff08;第三种线程实现方式&#xff09; Callable与Runnable的区别 Callable与Runnable的区别 实现方法名称不一样 有返回值 抛出了异常 ​class Thread1 implements Runnable{Overridepublic void run() { ​} } ​ class Thread2 implements Callable<…

x86的内存分段机制

8086 是 Intel 公司第一款 16 位处理器&#xff0c;诞生于 1978 年&#xff0c;所以说它很古老。 一.8086 的通用寄存器 8086 处理器内部共有 8 个 16 位的通用处理器&#xff0c;分别被命名为 AX、 BX、 CX、 DX、 SI、 DI、 BP、 SP。如下图所示。 “通用”的意思是…

【JavaSE】String类详解

目录 前言 1. 什么是String类 1.1 String的构造 1.2 String类的基本操作&#xff1a;打印、拼接、求字符串长度 2. String类的常用方法 2.1 字符串查找 2.2 字符串替换 2.3 字符串拆分 2.4 字符串截取 2.5 字符串和其他类型的转换 2.6 去除字符串左右两边的空格 3.…

日赚2000万的短剧,还能火多久?

沈瑶初十年前就义无反顾地爱上高禹川&#xff0c;当他们两人再次相遇&#xff0c;她主动靠近高禹川&#xff0c;不料&#xff0c;她却意外怀孕&#xff0c;高禹川为了负责选择领证&#xff0c;但不公布两人的关系...... 这是一部情绪稳定女航医与傲娇疯批男机长的虐恋剧。在这个…