【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;}}// 遍历完成没有找到环,返回 falsereturn 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

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

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

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

相关文章

华为云使用指南02

5.​​使用GitLab进行团队及项目管理​​ GitLab旨在帮助团队进行项目开发协作&#xff0c;为软件开发和运营生命周期提供了一个完整的DevOps方案。GitLab功能包括&#xff1a;项目源码的管理、计划、创建、验证、集成、发布、配置、监视和保护应用程序等。该镜像基于CentOS操…

【TB作品】MSP430G2553,超声波倒车雷达PCB,单片机,超声波SR04,键盘,oled,

题目 硬件&#xff1a;MSP430G2553、 SR04超声波传感器 、3*4键盘、 无源蜂鸣器、oled显示屏 软件 1 、实时显示测量得到的距离 2、按键设置一个报警门限数值&#xff0c;直接输入数值后确认 3、低于报警门限数值就开始报警&#xff0c;而且距离越近蜂鸣器的鸣叫频率越高 程序…

【踩坑】使用CenterNet训练自己的数据时的环境配置与踩坑

环境配置 系统&#xff1a;Ubuntu22.04 Python&#xff1a;3.8 CUDA&#xff1a;11.7 pytorch&#xff1a;2.2.1 因为种种原因没有使用原工程的老版本python和pytorch……但总之也跑通了&#xff0c;可喜可贺&#xff0c;可喜可贺。 新建conda环境&#xff1a; conda create -…

JavaEE之网络初识(网络中的一些基本概念)详解

&#x1f63d;博主CSDN主页: 小源_&#x1f63d; &#x1f58b;️个人专栏: JavaEE &#x1f600;努力追逐大佬们的步伐~ 目录 1. 前言 2. 网络中的一些基本概念 2.1 IP地址 2.2 端口号 2.3 网络协议 2.4 协议分层 2.5 封装 2.6 分用 (封装的逆向过程) 2.7 客户端 vs …

JUC并发编程之常用方法

sleep() public void testSleepAndYield() {Thread t1 new Thread(() -> {try {log.debug("t1-sleep...");Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}, "t1");log.debug("t1 start 前的状态&#…

Appium设备交互API

设备交互API指的是操作设备系统中的一些固有功能&#xff0c;而非被测程序的功能&#xff0c;例如模拟来电&#xff0c;模拟发送短信&#xff0c;设置网络&#xff0c;切换横竖屏&#xff0c;APP操作&#xff0c;打开通知栏&#xff0c;录屏等。 模拟来电 make_gsm_call(phon…

Douyin视频详情数据API接口(视频详情,评论)

抖音官方并没有直接提供公开的视频详情数据采集API接口给普通用户或第三方开发者。抖音的数据采集通常受到严格的限制&#xff0c;以保护用户隐私和平台安全。 请求示例&#xff0c;API接口接入Anzexi58 如果您需要获取抖音视频详情数据&#xff0c;包括评论、点赞等&#xff…

软考 - 系统架构设计师 - 关系模型的完整性规则

前言 关系模型的完整性规则是一组用于确保关系数据库中数据的完整性和一致性的规则。这些规则定义了在关系数据库中如何存储、更新和查询数据&#xff0c;以保证数据的准确性和一致性。 详情 关系模型的完整性规则主要包括以下三类&#xff1a; 实体完整性规则 这是确保每个…

C++基本语法

C是如何工作的 文章目录 C是如何工作的1、新建Hello World工程1.1使用Visual Studio新建项目1.2 HelloWorld1.2.1 命名空间1.2.2 输出输出 1.3 注释1.4 函数1.4.1 使用有返回的函数1.4.2 自定义函数 1、新建Hello World工程 1.1使用Visual Studio新建项目 按照下面的图片&…

x86的内存分段机制

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

利用python搭建临时文件传输服务

场景 如果想从一台服务器上传输文件又多种方法&#xff0c;其中常见的是利用scp进行传输&#xff0c;但是需要知道服务器的账号密码才能进行传输&#xff0c;但有时候我们并不知道账号密码&#xff0c;这个时候我们就可以通过python -m SimpleHTTPServer 命令进行传输文件 启…

C语言例4-14:从键盘输入小写字母转换成大写字母并输出。

代码如下&#xff1a; //从键盘输入小写字母转换成大写字母并输出。 #include<stdio.h> int main(void) {char c1,c2;printf("输入小写字母&#xff1a; \n");c1 getchar(); //从键盘输入一个字符putchar(c1);printf(",%d\n",c1);c2 c1-32; …

Git基础(23):Git分支合并实战保姆式流程

文章目录 前言准备正常分支合并1. 创建两个不冲突分支2. 将dev合并到test 冲突分支合并1. 制造分支冲突2. 冲突合并 前言 Git分支合并操作 准备 这里先在Gitee创建了一个空仓库&#xff0c;方便远程查看内容。 正常分支合并 1. 创建两个不冲突分支 &#xff08;1&#xf…

C++ 控制语句(二)

一 break continue和goto语句 1 break语句 在switch语句中&#xff0c;分隔case子句&#xff0c;跳出switch语句。 在循环语句中可以立即终止循环语句的执行。 2 continue语句 功能:在一次循环过程中,跳过continue语句以下的语句,直 接进入下一次循环操作。 3 goto语句 …

Linux文件系统和日志管理

文件系统的组成 Linux 文件系统会为每个文件分配两个数据结构&#xff1a;索引节点&#xff08;index node&#xff09; 和 目录项&#xff08;directory entry&#xff09;&#xff0c;它们主要用来记录文件的元信息和目录层次结构。 索引节点&#xff0c;也就是 inode&#…

【数据结构】顺序表的实现——静态分配

&#x1f388;个人主页&#xff1a;豌豆射手^ &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;数据结构 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进…

指针数组的有趣程序【C语言】

文章目录 指针数组的有趣程序指针数组是什么&#xff1f;指针数组的魅力指针数组的应用示例&#xff1a;命令行计算器有趣的颜色打印 结语 指针数组的有趣程序 在C语言的世界里&#xff0c;指针是一种强大的工具&#xff0c;它不仅能够指向变量&#xff0c;还能指向数组&#…

【Java程序设计】【C00344】基于Springboot的船舶维保管理系统(有论文)

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

直面位运算

在这篇文章之前&#xff0c;笔者只是简单了解过位运算相关概念&#xff0c;但是每次刷LeetCode碰到位运算相关题目&#xff0c;都会敬而远之。一方面是觉得看起来晦涩难懂&#xff0c;另一方面觉得日常开发用处不大。 近期本着学习的目的&#xff0c;静下心来研究了一下&#x…

单链表的插入和删除

一、插入操作 按位序插入&#xff08;带头结点&#xff09;&#xff1a; ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。 typedef struct LNode{ElemType data;struct LNode *next; }LNode,*LinkList;//在第i 个位置插插入元素e (带头结点) bool Li…