数据结构--链表进阶面试题

news/2024/5/20 13:57:14

        在链表题目开始之前我们来复习一道数组元素的逆序问题:

        给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

提示:

  • 1 <= nums.length <= 10^5
  • -2^31 <= nums[i] <= 2^31 - 1
  • 0 <= k <= 10^5

 思路1:旋转k次,每一次项右轮转一个元素。时间复杂度O(N^2)。空间复杂度O(1)。

 思路2:创建一个新的数组,现将原数组的后k个元素放置到新数组中,然后再把剩余元素依次放入新数组中,最后把新数组的元素按顺序放回原数组。时间复杂度O(N)。空间复杂度O(N)。

上面两种思路各有优势,也有缺点。我们再来看看思路三。

思路3:先逆序前n-k个元素,再逆序后k个元素,最后整体逆序。这种方法在时间、空间复杂度上都是最优的,但是思路不好想到,这就要大家多多积累,我们来看看代码:

void revese(int* nums,int left ,int right)
{while(left <= right){int tmp = nums[left];nums[left] = nums[right];nums[right] = tmp;left++;right--;}}void rotate(int* nums, int numsSize, int k) 
{k %= numsSize;revese(nums,0,numsSize-k-1);revese(nums,numsSize-k,numsSize-1);revese(nums,0,numsSize-1);
}

例1:返回链表的倒数第k个节点

        实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。给定的 k 保证是有效的。

解:

        这到题的思路和之前的快慢指针相似,我称之为先后指针,我们先创建两个指针(fast、slow),既然是找倒数第k个节点,那我们就先让fast走k个节点,然后让两个指针同时向后走,当fast指针走到尾节点时,slow节点就是倒数第k个节点。代码如下:

typedef struct ListNode ListNode;
int kthToLast(struct ListNode* head, int k)
{ListNode* fast = head;ListNode* slow = head;while(k--){fast = fast->next;}while(fast){fast = fast->next;slow = slow->next;}return slow->val;
}

 例2:链表的回文结构

        对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。

解:

        这道题有些难度,不过我们使用之前学的解法思路可以很轻松的过这道题。既然是回文结构,那我们就先找到他的中间节点,然后我们在逆序后半段链表,然后再比较原头结点和逆序后的头结点的值,如果不相等就返回false,反之继续向后遍历,直到其中有一个指针指向为空,返回true。

 

bool chkPalindrome(ListNode* A) {//寻找中间节点struct ListNode * fast = A;struct ListNode * slow = A;while(fast && fast->next){slow = slow->next;fast = fast->next->next;}//翻转后半链表:头插法struct ListNode* pcur = slow;struct ListNode* newhead = NULL;while(pcur){struct ListNode* next = pcur->next;pcur->next = newhead;newhead = pcur;pcur = next;}//向后遍历while(A && newhead){if(A->val != newhead->val){return false;}A = A->next;newhead = newhead->next;}return true;}

例3:相交链表

        给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 1 <= m, n <= 3 * 10^4
  • 1 <= Node.val <= 10^5
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listA 和 listB 没有交点,intersectVal 为 0
  • 如果 listA 和 listB 有交点,intersectVal == listA[skipA] == listB[skipB]

图示两个链表在节点 c1 开始相交

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headA 和 headB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案 。

 解:

        本道题在初见可能会没有思路,因为我们没有办法确定他们是否相交了或者走到哪个节点了。但是仔细思考,我们还是有办法去处理这些情况的。

        首先,我们要确定两个链表是否真的相交了,那我们可以先遍历两个数组,如果它们的尾节点为同一个,说明它们是相交的,反之即为不想交,返回空指针。

        如果是相交的话,我们就要重新去寻找它们的第一个相交节点,我们可以在第一次遍历判断他们是否相交时,顺便计算一下两个链表的节点个数,这样不需要重新再去计算,降低了时间复杂度。如果A链表长,那就让A先走len(A) - len(B)和节点,然后让两个节点从所在位置继续向后遍历,当两个节点相等时,就找到了第一个相交节点,返回该节点即可。

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{ListNode* cur1 = headA;ListNode* cur2 = headB;int len1 = 1;int len2 = 1;while(cur1->next){cur1 = cur1->next;len1++;}while(cur2->next){cur2 = cur2->next;len2++;}if(cur1 != cur2){return NULL;}else{ListNode* pcur1 = headA;ListNode* pcur2 = headB;if(len1 >= len2){int num = len1 - len2;while(num--){pcur1 = pcur1->next;}}else{int num = len2 - len1;while(num--){pcur2 = pcur2->next;}}while(pcur1 != pcur2){pcur1 = pcur1->next;pcur2 = pcur2->next;}return pcur1;}
}

 例4:随机链表的复制

        给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

        构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 

        例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

        返回复制链表的头节点。用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val,random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为  null 。

你的代码  接受原链表的头节点 head 作为传入参数。

 解:

        本题的难点在于节点的随机指针random难以确定指向的方位。所以需要我们另辟蹊径,我们可以直接在原链表的每个节点之后复制一个相同的节点,这样我们就可以直到该新节点的随机节点的位置在原节点的随机节点的下一个位置。怎么样,是不是很巧妙。

        首先我们还是先判断原链表是否为空,为空就返回NULL。反之,我们就要开始插入复制节点了,各位要注意,新复制的原点的随机节点在第一遍遍历的时候都赋值为NULL,因为,第一遍还没有创建好所有的节点,你可能找不到相应的随机节点。第二遍遍历就是单纯把所有的random都指向他们相应的节点。第三遍遍历为的是还原原链表并分离新链表。代码如下:

/*** Definition for a Node.* struct Node {*     int val;*     struct Node *next;*     struct Node *random;* };*/
typedef struct Node Node;
Node* copyRandomList(Node* head) 
{if (head == NULL) return NULL;// 第一遍遍历:在每个原节点后面插入复制节点struct Node *cur = head;while (cur != NULL) {struct Node *copyNode = (struct Node *)malloc(sizeof(struct Node));//固定位置插入copyNode->val = cur->val;copyNode->next = cur->next;//随机指针先置为空,后续节点完全创建好之后再进行复制copyNode->random = NULL;cur->next = copyNode;cur = copyNode->next;}// 第二遍遍历:设置复制节点的 random 指针cur = head;while (cur != NULL) {//NULL已经赋值过了,所以无需再次复制if (cur->random != NULL) {cur->next->random = cur->random->next;}cur = cur->next->next;}// 第三遍遍历:将复制节点从原链表中分离出来//分离新链表的同时,还原原链表cur = head;struct Node *newHead = head->next;struct Node *newCur = newHead;while (cur != NULL)     {cur->next = cur->next->next;if (newCur->next != NULL) {newCur->next = newCur->next->next;}cur = cur->next;newCur = newCur->next;}return newHead;
}

例5:环形链表

        给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

提示:

  • 链表中节点的数目范围是 [0, 10^4]
  • -10^5 <= Node.val <= 10^5
  • pos 为 -1 或者链表中的一个 有效索引 。

解:

         这道题不是很难,但是用到了很多数理逻辑思维。首先我们要确定链表中是否存在环,我们先假设有环,那如何确定环的存在也是一个问题。我们来看一张图:

        我们依旧使用快慢指针来解题,不了解的可以看前两篇文章。slow走一步,fast走两步,直到slow进入环中,fast开始追击slow,因为fast每次都比slow多走一步,那是不是说明fast早晚都可以追上slow呀。既然能追上,是不是就说明有环存在,这道题就解了。如果没有环,两个指针就不可能相遇,我们在里面判断一下就可以了。代码如下:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) 
{//判断是否为空链表if(head == NULL){return false;}ListNode* fast = head;ListNode* slow = head;//循环中如果fast和slow相等说明成环,不成环在循环结束后会返回faslewhile(fast && fast->next){fast = fast->next->next;slow = slow->next;if(fast == slow){return true;}}return false;
}

例5拓展:

        我们要讨论的远远不止这些,如果slow固定为走一步,但是fast不是走两步,而是走三步、四步、五步……呢?下面我们来一个一个讨论:

        我们假设进环前的节点个数为L,环的节点个数为C,当slow进环时,fast与slow相距N。

        我们先来看fast走三步的情况, 每移动一次指针,fast和slow的距离就会减少2,如果N为偶数,过程为N、N-2、N-4、……、2、0,最后两个指针会相遇;如果N为奇数,过程为N、N-2、N-4、……、3、1、-1,我们会发现两个指针错过了,并且越来越远,我们就需要重新计算它们的距离。错过之后它们的距离变成了C-1,当C-1为偶数时,它们最终会相遇对吧;但是当C-1为奇数时,它们就永远不会相遇了。那这里是不是就无解了呢,我们继续往下去探讨:

        我们假设slow走的路程是L,那么fast走的就是L + x*C + C-N(x为正整数)。我们还知道fast走的路程是slow的三倍,那么我们就可以写出关系式:3L = L + x*C + C-N。化简之后我们可以得到2L = (x+1)*C -N。由于C-1是奇数,仔细看你会发现:偶数 = 任意整数 * 偶数 - 奇数。这是一个恒不成立的等式,也就是说这种情况不存在。那我们是不是就可以证明,fast必定可以追上slow。

        看完三步后我们再来看看四步的,每移动一次指针,fast和slow的距离就会减少3,这时我们要分三种情况去看:N%3==0时,过程为N、N-3、N-6、……、3、0,可以相遇;N%3==1时,过程N、N-3、N-6、……、4、1、C-2;当N%3==2时,过程为N、N-3、N-6、……、5、2、C-1。后面的过程和三步的相似,各位感兴趣可以自己去算一下,这里不过多着笔。

        从这两种情况中我们要学的是数理逻辑思维,在正式工作时很少用到,但是在面试时可能会被问,大家注意一下就可以了。

例6:环形链表二

        给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

        如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

提示:

  • 链表中节点的数目范围在范围 [0, 10^4] 内
  • -10^5 <= Node.val <= 10^5
  • pos 的值为 -1 或者链表中的一个有效索引

解:

        这道题用上面的思路可以很快就解决, 我们使用两步fast的快慢指针来解决。

 

        首先,我们让fast和slow走到相遇节点,记为meet。我们来算一下,假设slow走了L+N ,fast走了L + x*C + N。由关系可得,2(L+N) = L + x*C + N。化简得L = (x-1)*C + C-N。其中C是环的节点数,C-N就是meet到入环节点的距离,因为(x-1)是可以等于0的,我们惊喜地发现,L = C-N。也就是说在头指针和meet一起向后走,会同时到达我们要的节点。代码如下:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
ListNode *detectCycle(struct ListNode *head) 
{ListNode* fast = head;ListNode* slow = head;ListNode* meet = NULL;//相遇while(fast && fast->next){slow =  slow->next;fast = fast->next->next;if(fast == slow){meet = fast;break;}}//判断链表是否无环if(fast == NULL || fast->next == NULL){return NULL;}//返回入环的第一个节点ListNode* pcur = head;while(pcur != meet){pcur = pcur->next;meet = meet->next;}return meet;}


http://www.mrgr.cn/p/85801587

相关文章

Sarcasm detection论文解析 |使用 BERT 进行中间任务迁移学习的刺检测

论文地址 论文地址&#xff1a;https://www.mdpi.com/2227-7390/10/5/844#/ github&#xff1a;edosavini/TransferBertSarcasm (github.com) 论文首页 笔记框架 使用 BERT 进行中间任务迁移学习的讽刺检测 &#x1f4c5;出版年份:2022 &#x1f4d6;出版期刊:Mathematics &…

自定义表单元素组件内容变化触发ElForm重新校验

对于下图中“付费类型”怎么实现有很多种方式&#xff0c;我能想到的是以下两种&#xff1a; Element Plus的RadioButton自定义组件 1. RadioButton 它本质上就是一个单选组件&#xff0c;它跟Element Plus的RadioButton本质上没有区别&#xff0c;无非是外观上的差别。那么…

2024 年 5 月 8 日 周三 晴 热(471 字)

正文翻开日历,才注意到已经立夏了呢。今天总结起来,就一个字:累。下午跑了三个乡镇,去找镇长对接帐户的事情。虽说我是被迫拉上的,不用自己操心,但是坐车真的很累。时间长,4 个多小时,弯道多,气热。最后跑完回到行里的时候感觉人快死掉了。并且因为这件事,中午觉也没…

TLP元素与PCIE数据流

不同于并行总线,PCIe 这样的串行总线不使用总线上的控制信号来表示某时刻链路上正在发生什么。相反地,PCIe 链路上的发送方发出的比特流必须要有一个预期的大小,还要有一个可供接收方辨认的格式,这样接收方才能理解比特流的内容。此外,PCIe 在传输数据包时并不使用任何直接…

高效备战!2024年陕西省绿色工厂申报条件好处和各地区奖补

什么是绿色工厂&#xff1f; 绿色工厂是制造业的生产单元&#xff0c;是绿色制造的实施主体&#xff0c;属于绿色制造体系的核心支撑单元&#xff0c;侧重于生产过程的绿色化。 通过采用绿色建筑技术建设、改造厂房&#xff0c;预留可再生能源应用场所和设计负荷&#xff0c;…

ESD静电问题 | 摄像头空气放电重启

【转自微信公众号&#xff1a;必学大课堂】

情感分类学习笔记(1)

文本情感分类&#xff08;二&#xff09;&#xff1a;深度学习模型 - 科学空间|Scientific Spaces 一、代码理解 cw lambda x: list(jieba.cut(x)) #定义分词函数 您给出的代码定义了一个使用 jieba 分词库的分词函数。jieba 是一个用于中文分词的 Python 库。该函数 cw 是…

添加一个索引要投产,需要哪些步骤?

编程一生 致力于写大家都能看懂的、有深度的 技术文章 05/2024 01 开场白 亚马逊有个bar raiser文化。就是说新招来的人一定要超过之前入职人员的平均水平&#xff0c;宁缺毋滥。越来越多的公司在推行这种文化。在这种氛围下&#xff1a;“虽然我不懂&#xff0c;但是活儿是能出…

如何把多个文件(夹)平均复制到多个文件夹中

首先,需要用到的这个工具:度娘网盘 提取码:qwu2 蓝奏云 提取码:2r1z假定的情况是,共有20个兔兔的图片,想要平均的复制4个文件夹里,那么每个文件夹里面就有5个图片(如果是5个,那每个自然是4个,具体除数是多少,根据实际情况即可)打开工具,切换到 文件批量复制 版块找…

一键自动化博客发布工具,用过的人都说好(cnblogs篇)

使用一键自动化博客发布工具blog-auto-publishing-tools把博客发布到cnblogs上。cnblogs和其他的博客平台相比会比较复杂,需要设置的项目也比较多一些,弄懂了cnblogs的实现方式,那么你应该对selenium的整个框架使用已经烂熟于心了。 除了正常的标题,内容,摘要之外,cnblog…

如何把多个文件(夹)随机复制到多个文件夹中

首先,需要用到的这个工具:度娘网盘 提取码:qwu2 蓝奏云 提取码:2r1z先看文件的情况一共20个兔兔的图片,4个文件夹,把全部的图片随机的复制这些地方去打开工具,切换到 文件批量复制 版块找到右下角的 设置,点击打开勾选“随机复制”,把文件进行随机的复制选中全部的兔兔…

【微信小程序开发】微信小程序、大前端之flex布局方式详细解析

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

软件测试过程中的痛点思考

前几天无意中看到了TesterHome发起的《2023年度软件质量保障行业调查报告》,文中提到了几点调查结果和分析结论让我很感兴趣。针对这份调查报告,我想就下述三点结论谈谈我的一些理解和思考。一、测试参与度分析 在这一调查报告结论中,提到了需求评审、测试计划和测试评审是整…

如何把多个文件(夹)向上移动1层(或多层)(在批量复制前或后进行)

首先,需要用到的这个工具:度娘网盘 提取码:qwu2 蓝奏云 提取码:2r1z假定情况是,我要把下图里的4个文件夹内部的全部文件,合并到04的当前位置来(4个文件夹里面各有5个兔兔的图片)打开工具,切换到 文件批量复制 版块找到右下角的 更多 ,点击,来设置上移的情况勾选“来…

Qt元对象系统自带类型与注册类型的判断

通过isRegistered()方法判断 在Qt跨线程传参时,使用信号槽connect或者调用QMetaInvokeMethon时,传递的参数的类型通常要注意是不是已在Qt的元对象系统中注册过了,Qt提供了方法来判断类型是否被注册: bool QMetaType::isRegistered(int type)其中参数是枚举类型,参数例子: …

JAVA栈相关习题3

1.将递归转化为循环 比如&#xff1a;逆序打印链表 // 递归方式void printList(Node head){if(null ! head){printList(head.next);System.out.print(head.val " ");}} // 循环方式void printList(Node head){if(nullhead){return;}Stack<Node> snew Stack<…

Qt元对象系统自带类型与注册类型

通过isRegistered()方法判断 在Qt跨线程传参时,使用信号槽connect或者调用QMetaInvokeMethon时,传递的参数的类型通常要注意是不是已在Qt的元对象系统中注册过了,Qt提供了方法来判断类型是否被注册: bool QMetaType::isRegistered(int type)其中参数是枚举类型,参数例子: …

将ESP工作为AP路由模式并当成服务器

将ESP8266模块通过usb转串口接入电脑 ATCWMODE3 //1.配置成双模ATCIPMUX1 //2.使能多链接ATCIPSERVER1 //3.建立TCPServerATCIPSEND0,4 //4.发送4个字节在链接0通道上 >ATCIPCLOSE0 //5.断开连接通过wifi找到安信可的wifi信号并连接 连接后查看自己的ip地址变为192.168.4.…

数据分析——业务指标分析

业务指标分析 前言一、业务指标分析的定义二、业务问题构建问题构建的要求 三、业务问题的识别在识别问题的阶段对于企业内部收益者的补充 四、竞争者分析竞争者分析的内容竞争者分析目的案例 五、市场机会识别好的市场机会必须满足的条件市场机会案例 六、风险控制数据分析师常…

添砖Java之路其二——基本数据类型,scanner,字符拼接。

目录 基本数据类型&#xff1a; ​编辑 Scanner: 字符拼接&#xff1a; 课后小题&#xff1a; 基本数据类型&#xff1a; 如图可见&#xff1a;Java里面有八种基本数据类型。 注意&#xff1a;在其中我们需要注意的是int默认整型数据&#xff0c;double是默认浮点型数据。因…