单链表——OJ题(一)

fe594ea5bf754ddbb223a54d8fb1e7bc.gif

目录

​一.前言

二.移除链表元素

三.返回链表中间节点

四.链表中倒数第K个节点

五.合并两个有序链表

六.反转链表

七.链表分割

八.链表的回文结构

九.相交链表

十.环形链表

十一.环形链表(二)

​六.结语


8fb442646f144d8daecdd2b61ec78ecd.png一.前言

本文主要对平时的链表OJ进行解析,帮助大家更加深入理解关于链表的性质特点。码字不易,希望大家多多支持我呀!(三连+关注,你是我滴神!)

二.移除链表元素

链接:203.移除链表元素

第一种思路:遍历删除

遍历变量cur:用于查找符合val的节点。再添加一个前置变量,用于连接删除过后的节点。

但其实这样子演示还是有弊端存在的~

当开头就出现符合val的节点,那么这两个指针变量又该如何指向呢?

我们可以把前面符合的都先删掉,最后再让head重新指向,另外两个变量也重新指向。

情况分析完毕,现在开始代码部分:

struct ListNode* removeElements(struct ListNode* head, int val) 
{struct ListNode* pre = NULL, * cur = head;while (cur){if (cur->val == val)//开始分析情况,如果找到就删除{//开始删除if (cur == head)//刚好要头删{head = cur->next;free(cur);cur = head;//cur需要重新用head赋值,以便遍历}else//中间部分删除。意味着有pre变量了{free(cur);pre->next = cur->next;cur = pre->next;//因为free后cur没指向了,需要重新赋值}}else//找不到就让cur往下走,随便标记pre{pre = cur;cur = cur->next;}}return head;
}

其实本质就是头删与中间删而已,只不过我们需要为其添加特定条件来应对各种情况~

第二种思路:遍历原链表,把不是val的节点尾插到新链表

需要用next变量来保存cur的下一个节点,这样方便cur指向。(当然不用next变量也可以,因为我们并没有改变cur的下一个变量)

在新链表中创造一个节点tail用来尾插。

代码部分:

struct ListNode* removeElements(struct ListNode* head, int val)
{struct ListNode* cur = head;struct ListNode* newhead = NULL, * tail = NULL;while (cur){if (cur->val == val)//当遇到val时,消除再重新指向{struct ListNode* del = cur;cur = cur->next;free(del);}else//没遇到val时,移动至新链表进行尾插{//当新链表为空时if (newhead == NULL){newhead = tail = cur;}else//当新链表不为空时{tail->next = cur;tail = tail->next;//tail->next = NULL;//尾插后记得置空//但是注意,不要在这里置空 }cur = cur->next;}}if (tail){tail->next = NULL;}return newhead;
}

三.返回链表中间节点

链接:876.返回链表中间节点

常规思路:

遍历一遍算出链表长度,再遍历一遍找到中间节点。

第二种思路:快慢指针

一开始两指针起点一致,然后遍历的时候让慢指针(slow)走一步,快指针(fast)走两步

快指针的速度是慢指针的一倍,那么当快指针指向结尾的时候,慢指针的指向就是中间节点了。

奇数个节点和偶数个节点的情况可能会不一样,所以我们还需要再来分析一遍。

对于奇数个而言是走到尾节点,对于偶数个而言是走向空。

struct ListNode* middleNode(struct ListNode* head) 
{struct ListNode* fast = head, * slow = head;while (fast && fast->next)//偶数个时fast指向空结束,奇数时fast指向尾节点结束{slow = slow->next;fast = fast->next->next;}return slow;
}

四.链表中倒数第K个节点

链接:链表中倒数第K个节点

这里我们还是可以使用多次遍历的方式来找到目标节点,所以我们额外添加一个条件:只能遍历一遍,那又要怎么做到呢?

还是老规矩,采用快慢指针的方法,只不过步数需要调整。

比如我们要找倒数第3个节点,我们先让fast走3步。

然后再让2个节点同时走,走到fast为空时结束。从空开始算起。

还有另外一种方式,就是我们可以先走k-1步。

走到尾节点结束,从尾节点算起。

struct ListNode* FindKthToTail(struct ListNode* head, int k)
{struct ListNode* fast = head, * slow = head;while (k--){
//也要考虑例如走倒数第1000个节点这种极端情况,一旦越界立马返回if (fast==NULL){return NULL;}else{fast = fast->next;}}while (fast){fast = fast->next;slow = slow->next;}return slow;
}

五.合并两个有序链表

链接:21.合并两个有序链表

一般思路:构建一个新的链表,然后在两个原链表之间取小的尾插。 (其实这个跟之前的合并数组想法很像)

持续到其中一个链表头节点指向空为止~

代码部分:

 struct ListNode {int val;struct ListNode* next;};struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{if (list1 == NULL){return list2;}if (list2 == NULL){return list1;}struct ListNode* tail = NULL, * head = NULL;while (list1 && list2){if (list1->val > list2->val){if (head == NULL)//当新链表为空时{head = list2;tail = list2;}else//当新链表不为空时{tail->next = list2;tail = tail->next;}list2 = list2->next;}else{if (head == NULL)//当新链表为空时{head = list1;tail = list1;}else//当新链表不为空时{tail->next = list1;tail = tail->next;}list1 = list1->next;}}if (list1){tail->next = list1;}if (list2){tail->next = list2;}return head;

 另一种思路:使用带头哨兵位

无带头节点(哨兵位)时还得判断是不是第一次插入,而有了带头节点后就减少了一步判断。

当单链表改成哨兵位时,还可以不用二级指针。

运行代码:

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{if (list1 == NULL){return list2;}if (list2 == NULL){return list1;}struct ListNode* tail = NULL, * head = NULL;tail = head = (struct ListNode*)malloc(sizeof(struct ListNode));//创造哨兵位while (list1 && list2){if (list1->val > list2->val){tail->next = list2;tail = tail->next;list2 = list2->next;}else{tail->next = list1;tail = tail->next;list1 = list1->next;}}if (list1){tail->next = list1;}if (list2){tail->next = list2;}//返回的时候删除掉哨兵位struct ListNode* del = head;head = head->next;free(del);return head;
}

六.反转链表

链接:206.反转链表

第一种思路:三指针

双指针还不能够实现反转。当n2指向n1时你就找不到下一个了。前两个是反转,后面是为了寻找下一个。

反转结束后再整体往后移动继续反转。

当n2等于空时结束。

但是这样是会有问题的,在我们执行最后一步时,n2已经是执行空了,所以不能让n3执行空的下一位,这样是错误的。

当解决这个问题后还有一个问题,我们没有考虑过链表为空的情况,所以还是跟上面一样的错误。所以很麻烦。

第二种思路:把节点拿下来头插

在第一个节点取下来头插指向newhead后,让newhead指向它,cur继续指向第二节点。

以此类推,直到cur指向空时停止。

运行代码:

struct ListNode* reverseList(struct ListNode* head) 
{struct ListNode* newnode = NULL;struct ListNode* cur = head;while (cur){struct ListNode* next = cur->next;//进行头插cur->next = newnode;newnode = cur;cur = next;}return newnode;}

七.链表分割

链接:CM11.链表分割

本题难点在于不能改变原有顺序(即相对顺序),所以我们开始的思路是再弄两条新链表。

做好尾插后再把两条链表链接起来,最后返回头节点即可。但本题坑点很多,其中不带哨兵位的话会比带哨兵位难很多。

当我们要链接两条链表的时候,要作出很多的判断~

  • 当lesstail为空的时候,就不能用链接了,而是直接返回greaterhead.
  • 当lesstail不为空时,得确保lesshead是头
  • 等等

当我们设置哨兵位的时候,就不用考虑哪个链表是否为空的情况,直接链接即可。

最终指向如上图所示,最后我们只需要消除哨兵位即可。

代码部分:

class Partition 
{
public:ListNode* partition(ListNode* head, int x){struct ListNode* ghead, * gtail, * lhead, ltail;ghead = gtail = (struct ListNode*)malloc(sizeof(struct ListNode));lhead = ltail = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* cur = head;while (cur)//拆分链表{if (cur->val < x){ltail->next = cur;ltail = ltail->next;}else{gtail->next = cur;gtail = gtail->next;}cur = cur->next;}//链接新链表ltail->next = ghead->next;struct ListNode* Head = lhead->next;free(ghead);free(lhead);return Head;}
};

这样还不够完整,还会出现错误。 

最后补充一个小细节,当我们的cur指向5并成功尾插的时候,按理来说5后面应该指向空,因为5后序的数据都不符合第二链表条件,但我们会发现5还会指向1造成了循环。所以我们要及时置空。

完整代码:

class Partition 
{
public:ListNode* partition(ListNode* head, int x){struct ListNode* ghead, * gtail, * lhead, ltail;ghead = gtail = (struct ListNode*)malloc(sizeof(struct ListNode));lhead = ltail = (struct ListNode*)malloc(sizeof(struct ListNode));struct ListNode* cur = head;while (cur)//拆分链表{if (cur->val < x){ltail->next = cur;ltail = ltail->next;}else{gtail->next = cur;gtail = gtail->next;}cur = cur->next;}gtail->next = NULL;//及时置空//链接新链表ltail->next = ghead->next;struct ListNode* Head = lhead->next;free(ghead);free(lhead);return Head;}
};

八.链表的回文结构

链接:OR36.链表的回文结构

回文结构也分奇数回文与偶数回文:

想法:找到中间节点,再逆置中间节点后面的节点。最后再来与头节点与指向中间节点的数据一一对比。

我们可以套用前面已经写好的寻找中间节点代码与反转链表来实现找到中间节点与逆置中间节点后面的节点,最后再来处理对比。

逆置完后链表会变成这样,跟前面链表分割一个性质,如果没有刻意去切断链接,那就会藕断丝连。不过这种情况问题不大,应该这两个指针只要任意一个指向空就结束,例如在上图中1与1相等,指向下一位,2与2相等,一个指向3,另一个也指向3还是相等,最后一个指向空结束。

运行代码:

class PalindromeList {
public:struct ListNode* middleNode(struct ListNode* head){struct ListNode* fast = head, * slow = head;while (fast && fast->next)//偶数个时fast指向空结束,奇数时fast指向尾节点结束{slow = slow->next;fast = fast->next->next;}return slow;}struct ListNode* reverseList(struct ListNode* head){struct ListNode* newnode = NULL;struct ListNode* cur = head;while (cur){struct ListNode* next = cur->next;//进行头插cur->next = newnode;newnode = cur;cur = next;}return newnode;}bool chkPalindrome(ListNode* head) {struct ListNode* mid = middleNode(head);struct ListNode* rmid = reverseList(mid);while (head && rmid)//一方指向空就停止{if (head->val != rmid->val){return false;}else{head = head->next;rmid = rmid->next;}}return true;}
};

九.相交链表

链接:160.相交链表

经典误区:

接下来我们应该如何来判断相交呢?——寻找尾节点。记住!比的是地址而不是数值,如果本来就相交,那么一定是共用一个地址的!数值相等的话并不意味着两个节点是同一个地址~

尾节点相等就说明相交。

真正麻烦的是如何找到相交的点~

一种思路是:暴力遍历,依次拿a的节点跟b所有的节点进行地址对比。时间复杂度O(N^2)

优化思路:在寻找交点的时候,想办法让两链表长度一致开始遍历对比。时间复杂度O(N)

运行代码:

 struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB){struct ListNode* curA = headA;struct ListNode* curB = headB;//题目说明链表不为空,而且设置为1也没事,反正我们只需要差值int lenA = 1;int lenB = 1;//寻找尾节点看是否相交while (curA->next){lenA++;curA = curA->next;}while (curB->next){lenB++;curB = curB->next; }if (curA!= curB){return NULL;}//开始寻找第一个相交点//先算绝对值int gap = abs(lenA - lenB);struct ListNode* lonlist = headA;//假设A是长的struct ListNode* shortlist = headB;//假设B是短的if (lenA < lenB){lonlist = headB;shortlist = headA;}//让长的提前走gap步while (gap--){lonlist = lonlist->next;}//长度一致时开始一一对比while (lonlist != shortlist){lonlist = lonlist->next;shortlist = shortlist->next;}return lonlist;}

十.环形链表

链接:141.环形链表

本题难点在于不清楚哪个节点是环内的,哪个节点是环外的。

所以我们只能看是否会相遇,如果不是环是不会相遇的,而且fast只会走向尾节点。

运行代码:

 bool hasCycle(struct ListNode* head) {struct ListNode* fast = head;struct ListNode* slow = head;//很关键的一步之所以设置两个条件,就是为了预防链表不为环的情况//当链表奇数个节点时,用fast-next来跳出循环//当链表偶数个节点时,用fast来跳出循环while (fast && fast->next){fast = fast->next->next;slow = slow->next;//如果是带环链表,那么迟早会相遇if (fast == slow){return true;}}//如果是不带环链表,那么fast或fast->next会到空节点,所以直接falsereturn false;}

  • 思考:
  • 如果slow走1步,fast走2步,一定能追上吗?会不会错过了。
  • 如果slow走1步,fast走n步(n>=3),一定能追上吗?会不会错过?

死循环

所以用快慢指针2步走的方法是最稳健的。多于3步的都是要考虑很多情况的。

十一.环形链表(二)

链接:142.环形链表(二)

思路一:

先假设它们在不断地追击

这里我们是否需要判断slow会多走一圈C(圈长)才会和fast相遇呢?——不需要,因为我们在分析走2的时候已经得出结论:它们不会错过。

如果是这样想那结果是经不起推敲的,漏洞百出。

当环很短,L很长的时候,那么L=C-X的公式是不成立的。

我们前面做了这么多的测试就是为了推出这个公式。

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

因为在最后的meet和head相遇的原理就是 L = n*C-x,正是因为我们有这个公式,在后续才可以用whiel循环让head与meet重合找到第一个环入口点。

首先slow与fast相遇时slow从入口点到相遇点已经走过了x的距离,所以从相遇点开始走到入口点就需要C-X的距离,另外还得考虑到无法相遇时要多走的(n-1)C圈,但归根到底,二者相遇的地点一定是入口点,所以我们最后的总结就是让head等于meet即可,因为它们相当于 L(head) = (n*C-x)(meet)

方法二:

把当前meet结点搞成尾,让newnode新结点变成头。

复制相交链表接口

一个从newnode开始走,一个从head开始走,找交点。

因为通过条件slow==fast可以得出只有环才可以相遇,那么接下来只需要找相交结点即可.

那我们又为什么要进行断尾呢?因为套用相交链表的前提是要用到尾部结点的,如果不主动断尾,就会陷入死循环。

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

4b12323f94834afd9ec146a3c10df229.jpeg六.结语

如果大家能够把这些经典的链表OJ题融会贯通,那你接下来关于链表的题型大部分都会迎刃而解的啦~最后感谢大家的观看,友友们能够学习到新的知识是额滴荣幸,期待我们下次相见~

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

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

相关文章

Day33力扣打卡

打卡记录 最大和查询&#xff08;排序单调栈上二分&#xff09; 链接 大佬的题解 class Solution:def maximumSumQueries(self, nums1: List[int], nums2: List[int], queries: List[List[int]]) -> List[int]:ans [-1] * len(queries)a sorted(((a, b) for a, b in zi…

机器学习第11天:降维

文章目录 机器学习专栏 主要思想 主流方法 1.投影 二维投射到一维 三维投射到二维 2.流形学习 一、PCA主成分分析 介绍 代码 二、三内核PCA 具体代码 三、LLE 结语 机器学习专栏 机器学习_Nowl的博客-CSDN博客 主要思想 介绍&#xff1a;当一个任务有很多特征…

如何在IAR软件中使用STLINK V2编译下载和调试stm8单片机

安装使用IAR后&#xff0c;如使用系统默认设置&#xff0c;往往很难正常实现用stlink v2来下载和调试stm8芯片&#xff0c;我的解决方法如下&#xff1a; 1、打开项目的options菜单&#xff1a; 2、在项目的选项菜单中选择ST-LINK作为调试工具&#xff1a; 3、选择额外的输出…

IDEA JRebel安装使用教程

1、下载插件 版本列表&#xff1a;https://plugins.jetbrains.com/plugin/4441-jrebel-and-xrebel/versions 下载&#xff1a;JRebel and XRebel 2022.4.1 这里下载2022.4.1版本&#xff0c;因为后续新版本获取凭证会比较麻烦。下载完成会是一个压缩包。 2、安装 选择第一步…

使用VSCode+PlatformIO搭建ESP32开发环境

Arduino IDE本来就是为创客们开发的&#xff0c;虽然没代码提示功能&#xff0c;文件的关系也不清晰&#xff0c;函数不能跳转&#xff0c;头文件也打不开&#xff0c;但人家的初衷就是为了简单而生的&#xff1b;但还是有一些同学喜欢高级点的IDE&#xff0c;也没问题&#xf…

C语言经典好题:字符串左旋(详解)

这题还是比较简单的&#xff0c;各位看完有收获吗 #include<stdio.h> #include<string.h> void leftturn(char arr[],int k) {int len strlen(arr);for (int i 0;i <k;i)//左旋k个字符{//创建临时变量char tmp 0;tmp arr[0];//将数组第一个字符存储到临时变…

【C++进阶之路】第五篇:哈希

文章目录 一、unordered系列关联式容器1.unordered_map&#xff08;1&#xff09;unordered_map的介绍&#xff08;2&#xff09;unordered_map的接口说明 2. unordered_set3.性能对比 二、底层结构1.哈希概念2.哈希冲突3.哈希函数4.哈希冲突解决&#xff08;1&#xff09;闭散…

ArmSoM-RK3588编解码之mpp编码demo解析:mpi_enc_test

一. 简介 [RK3588从入门到精通] 专栏总目录mpi_enc_test 是rockchip官方编码 demo本篇文章进行mpi_enc_test 的代码解析&#xff0c;编码流程解析 二. 环境介绍 硬件环境&#xff1a; ArmSoM-W3 RK3588开发板 软件版本&#xff1a; OS&#xff1a;ArmSoM-W3 Debian11 三. …

Python---变量的作用域

变量作用域&#xff1a;指的是变量的作用范围&#xff08;变量在哪里可用&#xff0c;在哪里不可用&#xff09;&#xff0c;主要分为两类&#xff1a;局部变量和全局变量。 定义在函数外部的变量就称之为全局变量&#xff1b; 定义在函数内部的变量就称之为局部变量。 # 定义…

nodejs+vue线上生活超市购物商城系统w2c42

超市管理系统的开发流程包括对超市管理系统的需求分析&#xff0c;软件的设计建模以及编写程序实现系统所需功能这三个阶段。对超市管理系统的需求分析。在这个阶段&#xff0c;通过查阅书籍&#xff0c;走访商场搜集相关资料&#xff0c;了解经营者对软件功能的具体所需和建议…

gitlab设置项目clone地址

直接在线修改地址 虽然是个小问题但是我查了很多都是说要去修改配置文件&#xff0c;可是我是docker部署的&#xff0c;修改配置文件之后我还要重新打包镜像想想都不咋规范&#xff0c;后才终于知道可以直接设置&#xff0c;不要改配置文件&#xff01;&#xff01;&#xff0…

PHP中cookie与session使用指南

PHP中cookie与session使用指南 Cookie和session的出现&#xff0c;是为了解决http协议无状态交互的窘境&#xff0c;它们都用于存储客户端的相关信息 0x01 Cookie使用 简介 Cookie 是一种在客户端存储数据的机制&#xff0c;通常用于记录用户的状态和偏好。下面将介绍如何在…

【日常总结】Swagger-ui 导入 showdoc (优雅升级Swagger 2 升至 3.0)

一、场景 环境&#xff1a; 二、存在问题 三、解决方案 四、实战 - Swagger 2 升至 3.0 &#xff08;Open API 3.0&#xff09; Stage 1&#xff1a;引入Maven依赖 Stage 2&#xff1a;Swagger 配置类 Stage 3&#xff1a;访问 Swagger 3.0 Stage 4&#xff1a;获取 js…

【JUC】十、ForkJoin

文章目录 1、分支合并框架2、案例3、ForkJoinTask4、工作窃取算法5、ForkJoinPool 一个个任务执行在一个个线程上&#xff0c;倘若某一个任务耗时很久&#xff0c;期间其他线程都无事可做&#xff0c;显然没有利用好多核CPU这一计算机资源&#xff0c;因此&#xff0c;出现了&q…

13 redis中的复制的拓扑结构

1、一主一从 为了性能考虑&#xff0c;主节点可以不开启AOF&#xff0c;但是要避免重启。 2、一主多从 适用于读操作的场景。由于从节点多&#xff0c;所以主的复制压力大 3、树状主从 数据先同步到redisB,redisC从节点C,E来看&#xff0c;redisB相当于主机了&#xff0c;可以…

【JavaEE】Servlet实战案例:表白墙网页实现

一、功能展示 输入信息&#xff1a; 点击提交&#xff1a; 二、设计要点 2.1 明确前后端交互接口 &#x1f693;接口一&#xff1a;当用户打开页面的时候需要从服务器加载已经提交过的表白数据 &#x1f693;接口二&#xff1a;当用户新增一个表白的时候&#xff0c;…

玩转系统|长亭雷池WAF详细使用教程——深入了解

目录 配置防护站点 界面操作​ 如何配置域名、端口、上游服务器​ 工作原理​ 在单独设备上部署雷池&#xff08;推荐&#xff09;​ 直接在网站服务器上部署雷池​ 和其他反代设备一起部署的情况​ 配置后网站无法访问&#xff0c;如何排查​ 测试防护效果 确认网站…

ETL-使用kettle批量复制sqlserver数据到mysql数据库

文章标题 1、安装sqlserver数据库2、下载kettle3、业务分析4、详细流程&#xff08;1&#xff09;转换1&#xff1a;获取sqlserver所有表格名字&#xff0c;将记录复制到结果&#xff08;2&#xff09;转换2&#xff1a;从结果设置变量&#xff08;3&#xff09;转换3&#xff…

【广州华锐互动】VR溺水预防教育:在虚拟世界中学会自救!

在现代社会中&#xff0c;水上安全和救援行动的重要性不言而喻。尤其在自然灾害、游泳事故或航海事故中&#xff0c;有效的救援行动可以挽救许多生命。然而&#xff0c;传统的救援训练往往存在成本高、风险大、效率低等问题。在这样的背景下&#xff0c;虚拟现实&#xff08;VR…

Oracle实时同步技术

Oracle数据库的价值 Oracle数据库是一种高度可靠、安全和强大的关系型数据库管理系统&#xff0c;它具有以下几个方面的价值&#xff1a; 可靠性和稳定性&#xff1a;Oracle数据库以其高度可靠性、稳定性和数据完整性而闻名于世。 安全性&#xff1a;Oracle数据库提供了一系列…