单链表——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.hqwc.cn/news/208480.html

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

相关文章

音频采集的相关基础知识

本文引注: https://zhuanlan.zhihu.com/p/652629744 1.麦克风的种类 (1)模拟麦克风 ECM麦克风&#xff1a;驻极体电容麦克风(ECM)&#xff0c;典型的汽车ECM麦克风是一种将ECM单元与小型放大器电路整合在单个外壳中的装置。放大器提供一个模拟信号&#xff0c;其电压电平允许…

Ps:背景橡皮擦工具抠图实例

背景橡皮擦工具 Background Eraser Tool由于是一个破坏性的工具&#xff08;直接删除像素&#xff09;而少被人使用。 其实&#xff0c;它不仅是一个功能强大的抠图工具&#xff0c;也是可以转换为非破坏性运用的。 原图&#xff08;注&#xff1a;图片来自网络&#xff09; 效…

闪存组织结构概念

文章目录 一、几种不同类型闪存的参数&#xff1a;二、组织结构三、块&#xff08;Block&#xff09;的结构擦除动作原理&#xff1a;写操作读操作 一、几种不同类型闪存的参数&#xff1a; 参数项SLCMLCTLCQLC读取时间/us20~2555~11075~170120~200写入时间/us50~100400~15008…

【开源】基于Vue.js的康复中心管理系统

项目编号&#xff1a; S 056 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S056&#xff0c;文末获取源码。} 项目编号&#xff1a;S056&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 普通用户模块2.2 护工模块2.3 管理员…

Linux-进程替换

进程程序替换目的 首先我们要知道&#xff0c;创建子进程的目的是什么&#xff1f; 想让子进程执行父进程代码的一部分想让子进程执行一个全新的代码 我们之前所写的程序&#xff0c;子进程都是在执行父进程代码的一部分&#xff0c;而要想让子进程执行全新的代码&#xff0…

时序预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost时间序列预测

时序预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost时间序列预测 目录 时序预测 | MATLAB实现基于BiLSTM-AdaBoost双向长短期记忆网络结合AdaBoost时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 1.Matlab实现BiLSTM-Adaboost…

低权限(无权限)时如何在mysql客户端控制台的大量输出中快速定位mysql死锁或慢sql

查看mysql的查看死锁的方式很多&#xff0c;但很多时候我们普通开发者的权限比较低&#xff0c;无法执行某命令。比如本次就准备使用 SHOW ENGINE INNODB STATUS;命令&#xff0c;但客户端提示权限不够。后来本人找到了另一条低权限的命令 show full PROCESSLIST;但是show fu…

MAX/MSP SDK学习07:list传递

实现自定义Obejct&#xff0c;要求将传入的一组数据100后传出。 #include "ext.h" #include "ext_obex.h" typedef struct _listTrans {t_object ob;void* outLet;t_atom* fArr;long listNum;} t_listTrans;void* listTrans_new(t_symbol* s, long arg…

常用通信接口、协议:SCCB

一、概述 SCCB(串行摄像头控制总线)是由欧姆尼图像技术公司&#xff08;OmniVision&#xff09;开发的一种类IIC的总线&#xff0c;主要用于其OV系列的图像传感器上&#xff08;但目前有很多家的图像传感器都有采用该控制总线&#xff09;。相对于IIC总线来说SCCB与之最主要的差…

阿里云服务器ECS经济型e实例优惠99元性能怎么样?

阿里云服务器ECS经济型e实例优惠99元性能怎么样&#xff1f;阿里云服务器优惠99元一年&#xff0c;配置为云服务器ECS经济型e实例&#xff0c;2核2G配置、3M固定带宽和40G ESSD Entry系统盘&#xff0c;CPU采用Intel Xeon Platinum架构处理器&#xff0c;2.5 GHz主频&#xff0…

SkyWalking配置报警推送到企业微信

1、先在企业微信群里创建一个机器人&#xff0c;复制webhook的地址&#xff1a; 2、找到SkyWalking部署位置的alarm-settings.yml文件 编辑&#xff0c;在最后面加上此段配置 &#xff01;&#xff01;&#xff01;一定格式要对&#xff0c;不然一直报警报不出来按照网上指导…

排序算法--归并排序

实现逻辑 ① 将序列每相邻两个数字进行归并操作&#xff0c;形成floor(n/2)个序列&#xff0c;排序后每个序列包含两个元素 ② 将上述序列再次归并&#xff0c;形成floor(n/4)个序列&#xff0c;每个序列包含四个元素 ③ 重复步骤②&#xff0c;直到所有元素排序完毕 void pri…