【初阶数据结构与算法】第四篇:链表面试题详解
⭐️本篇博客我要给大家分享一下算法中的链表例题。希望对大家有所帮助。
⭐️ 博主码云gitee链接:码云主页
目录
前言
🌏一、移除链表元素(传送门)
🌏二、反转链表(传送门)
🌏三、链表的中间节点 (传送门)
🌏四、链表中倒数第K个节点(传送门)
🌏五、合并两个有序链表(传送门)
🌏六、链表分割(传送门)
🌏七、链表回文(传送门)
🌏八、相交链表(传送门)
🌏九、 环形链表(传送门)
🌏环形指针扩展
🌏十、环形链表||(传送门)
🌏十一、复制带随机指针的列表(传送门)
🌏总结
前言
🌏一、移除链表元素(传送门)
🍤给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
示例 1:
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
🔑思路:
三种情况:常规情况,上面图中那种,还有就是连续的val,头就是val。
不管是哪种,就是pre和cur一个一个向后找,直到cur指向null,
当头是val的时候,free cur, 将head和cur移动到next
struct ListNode* removeElements(struct ListNode* head, int val){ struct ListNode* pre = NULL; struct ListNode* cur = head; while(cur != NULL){ //如果第一个就是val if(cur->val == val){ struct ListNode* next = cur->next; //如果pre为空则要删除第一个节点 if(pre != NULL){ free(cur); pre->next = next; cur = next; }else{ free(cur); head = next; cur = next; } //如果第一个不是val }else{ pre = cur; cur = cur->next; } } return head;}
🌏二、反转链表(传送门)
🍤给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
🔑思路1:
定义三个指针
首先要判断,head是不是NULL否则后面next指向空指针了
之后将cur->next指向pre,然后将cur和pre向后移动
此时移动next之前要判断是否,为空
则返回当前头指针pre即可
struct ListNode* reverseList(struct ListNode* head){ //判断head和head->next是不是空,防止空指针 if(head == NULL){ return NULL; } struct ListNode* pre = NULL; struct ListNode* cur = head; struct ListNode* next = head->next; while(cur != NULL){ cur->next = pre; pre = cur; cur = next; //next是不是空,防止空指针 if(next != NULL){ next = next->next; } } return pre;}
🔑思路2:
创建一个新的头指针 newhead,将cur指向newhead,再将newhead移动到cur,将cur移动到next,移动next之前要判断是否为空
struct ListNode* reverseList(struct ListNode* head){ if(head == NULL){ return NULL; } struct ListNode* cur = head; struct ListNode* next = cur->next; struct ListNode* newhead =NULL; while(cur != NULL){ cur->next = newhead; newhead = cur; cur = next; if(next!=NULL){ next = next->next; } } return newhead;}
🌏三、链表的中间节点 (传送门)
🍤给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
🔑思路:
运用快慢指针,将两个指针都从head开始,然后直到fast或者fast->next指向为空就找到了中间的节点了,注意,判断的时候,要注意fast->next要在fast之后,防止空指针
struct ListNode* middleNode(struct ListNode* head){ struct ListNode* fast = head; struct ListNode* slow = head; //不能换位置,从左往右先判断,否则判断为空指针 while(fast != NULL && fast->next != NULL){ slow = slow->next; fast = fast->next->next; } return slow;}
🌏四、链表中倒数第K个节点(传送门)
🍤输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
🔑思路:
依旧是利用快慢指针,首先,让fast移动K为,然后fast和slow之间的距离就是K,然后同时走,当fast为空的时候,slow指向的空间就是所求空间
struct ListNode* getKthFromEnd(struct ListNode* head, int k){ //如果是空则返回空 if(head == NULL){ return NULL } struct ListNode* fast = head; struct ListNode* slow = head; while(k--){ fast = fast->next; } while(fast != NULL){ fast = fast->next; slow = slow->next; } return slow;}
🌏五、合并两个有序链表(传送门)
🍤将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
🔑思路1:
取一个初始为空的新链表,比较指向list1和指向list2的节点值,小的先放入新链表,之后,小的节点指针向后移动。当有一个指针为空时,就将另一个链表直接连接至新链表之后。
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ //分别判断list1和list2是否为空 if(list1 == NULL){ return list2; } if(list2 == NULL){ return list1; } struct ListNode* head = NULL; struct ListNode* tail = NULL; while(list1 != NULL && list2 != NULL){ if(list1-> val val){ //判断头是不是空 if(tail == NULL){ head = tail = list1; }else{ tail->next = list1; tail = list1; } list1 = list1->next; }else{ if(tail == NULL){ head = tail = list2; }else{ tail -> next = list2; tail = list2; } list2 = list2->next; } } //遍历完后有判断是否还有没有遍历的 if(list1 != NULL){ tail->next = list1; } if(list2 != NULL){ tail->next = list2; } return head;}
🔑思路2:
思路1是不带哨兵的,下面写一个带哨兵的
由于创建哨兵,所以不用判断list1和list2是否为空了,因为我们的返回值是head->next,返回的是头,而不是哨兵。
最后记得释放内存
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){ //带哨兵位 struct ListNode* head, *tail; head = tail = (struct ListNode*)malloc(sizeof(struct ListNode)); head->next = NULL; while(list1 != NULL && list2 != NULL){ if(list1-> val val){ tail->next = list1; tail = list1; list1 = list1->next; }else{ tail -> next = list2; tail = list2; list2 = list2->next; } } //遍历完后有判断是否还有没有遍历的 if(list1 != NULL){ tail->next = list1; } if(list2 != NULL){ tail->next = list2; } //为方便释放内存,提前记录头 struct ListNode* list = head->next; free(head); //返回哨兵位下一位 return list;}
🌏六、链表分割(传送门)
🍤给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你不需要 保留 每个分区中各节点的初始相对位置。
示例 1:
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
🔑思路:
新链表less和新链表greater,它们初始都为空。然后用指针cur遍历一遍原链表,小于x的节点放入less链表中,大于x的放入greater链表中,最后再将less和greater连接起来,返回less的头指针
当less不为空时,less最后的节点要指向greater的头节点。并且将greater最后节点的指针指向空。因为greater最后的指向可能指向less的某个节点,连接less和greater的时候就会形成死循环
struct ListNode* partition(struct ListNode* head, int x){ //less和greter分别为小于val和大于val的头指针 struct ListNode* lesshead, *lesstail, *greterhead, *gretertail; lesshead = lesstail = (struct ListNode*)malloc(sizeof(struct ListNode)); greterhead = gretertail = (struct ListNode*)malloc(sizeof(struct ListNode)); //初始化 lesstail->next = gretertail->next = NULL; struct ListNode* cur = head; while(cur != NULL){ if(cur->val next = cur; lesstail = lesstail->next; }else{ gretertail->next = cur; gretertail = gretertail->next; } cur = cur->next; } lesstail->next = greterhead->next; //防止死循环 gretertail->next = NULL; struct ListNode* list = lesshead->next; //防止内存泄漏 free(lesshead); free(greterhead); return list;}
🌏七、链表回文(传送门)
🍤给定一个链表的 头节点 head ,请判断其是否为回文链表。
如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。
示例 1:
输入: head = [1,2,3,3,2,1]
输出: true
🔑思路:
找到中间节点,将它逆置之后,分别从head和rhead开始比较。
偶数情况:
奇数情况:
结果都一样,要注意的是反转之后,rhead节点的前一个指向的是rhead最后一个
//找中间节点struct ListNode* middleNode(struct ListNode* head){ struct ListNode* fast = head; struct ListNode* slow = head; //不能换位置,从左往右先判断,否则判断为空指针 while(fast != NULL && fast->next != NULL){ slow = slow->next; fast = fast->next->next; } return slow;}//逆置struct ListNode* reverseList(struct ListNode* head){ if(head == NULL){ return NULL; } struct ListNode* cur = head; struct ListNode* next = cur->next; struct ListNode* newhead =NULL; while(cur != NULL){ cur->next = newhead; newhead = cur; cur = next; if(next!=NULL){ next = next->next; } } return newhead;}bool isPalindrome(struct ListNode* head){ struct ListNode* midhead = middleNode(head); struct ListNode* rhead = reverseList(midhead); while(head != NULL && rhead != NULL){ if(head->val != rhead->val){ return false; } head = head->next; rhead = rhead->next; } return true;}
🌏八、相交链表(传送门)
🍤给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
题目数据 保证 整个链式结构中不存在环。
注意,函数返回结果后,链表必须 保持其原始结构 。
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
🔑思路:
先算出listA和listB长度,然后两个链表中较长的先走两者长度的差值,此时长的链表现在长度和短的一样,之后遍历如果发现相等则返回。
注意:编译器只会检查语法逻辑,执行逻辑不会检查,所以按照语法,我们要加上return,尽管没有返回从逻辑上来说
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) { struct ListNode* tailA = headA; struct ListNode* tailB = headB; //第一个节点跳过,所以长度从一开始 int lenA = 1; int lenB = 1; while(tailA->next != NULL){ tailA = tailA->next; lenA++; } while(tailB->next != NULL){ tailB = tailB->next; lenB++; } //尾节点是否相同 if(tailA != tailB){ return NULL; } //相交求交点,长的先走差值的步数,再一起走 struct ListNode* shortlist = headB; struct ListNode* longlist = headA; if(lenA next; } while(shortlist != NULL && longlist != NULL){ if(shortlist == longlist){ return longlist; } longlist = longlist->next; shortlist = shortlist->next; } //执行逻辑没有问题,但是没有返回值的话会有语法逻辑 return NULL;}
🌏九、 环形链表(传送门)
🍤给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
🔑思路:
快慢指针,如果在一个链表中,fast和slow能够相等,说明能找到,没有找到就说明没有环
bool hasCycle(struct ListNode *head) { struct ListNode *slow = head; struct ListNode *fast = head; while(fast && fast->next){ slow = slow->next; fast = fast->next->next; if(slow == fast){ return true; } } return false;}
🌏环形指针扩展
🍤如果fast一次走两步,slow一次走一步,它们一次会相遇吗?
一定能!每次fast追击后,fast和slow距离减一
🍤如果fast一次走3步,slow一次走1步,它们一次会相遇吗?
不一定,当两者距离是偶数一定能追上,因为每次fast和slow距离减少2,如果是奇数不行,fast会越过slow
🍤如果fast一次走4步,slow一次走一步,它们一次会相遇吗?
一样的,只有fast和slow之间的距离是fast每次移动和slow缩小距离的整数倍就能
🌏十、环形链表||(传送门)
🍤给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。
🔑思路:
先用快慢指针判断是否,有环,又环的话就记录快慢指针相遇的节点
设【链表头--入口点】L
【入口点--相遇点】X
【环的长度】C
慢指针走的路程为:L+X。
快指针走的路程为:L+X+C*N(其中N代表圈数,N>=1)。
快指针路程是慢指针路程的两倍
所以:L+X+C*N=2*(L+X)。(当L=10,C=2可以验证)
化简得:L=C*N-X
L=C*(N-1)+(C-X)
计算得出:
一个指针从head走,另一个指针从meet走,当两指针相等时,它们就指向环的入口点
struct ListNode *detectCycle(struct ListNode *head) { struct ListNode *slow = head, *fast = head; while(fast && fast->next){ slow = slow->next; fast = fast->next->next; if(slow == fast){ struct ListNode *meet = slow; //当meet和head相等的时候就是入口点 while(meet != head){ meet = meet->next; head = head->next; } return head; } } return NULL; }
🌏十一、复制带随机指针的列表(传送门)
🍤例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。示例 1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
🔑思路:
1.拷贝链表的每一个节点,将每个拷贝好的节点放在原节点后面
2.将copy中的random变得和原节点一样
3.拆解链表,把拷贝的链表从原链表中拆解出来
struct Node* copyRandomList(struct Node* head) {//1.拷贝链表并且将它放在原节点后面 struct Node* cur = head; while(cur != NULL){ struct Node* copy = (struct Node*)malloc(sizeof(struct Node)); copy->next = cur->next; cur->next = copy; copy->val = cur->val; cur = cur->next->next; } //2.将copy中random变为原节点一样 cur = head; while(cur != NULL){ struct Node* copy = cur->next; if(cur->random == NULL){ copy->random = cur->random; }else{ copy->random = cur->random->next; } cur = cur->next->next; } //3.解开链表,构成原节点 struct Node* copyhead = NULL, *copytail = NULL; cur = head; while(cur != NULL){ struct Node* copy = cur->next; struct Node* next = copy->next; if(copyhead == NULL){ copyhead = copytail = copy; }else{ copytail->next = copy; copytail = copy; } cur->next = next; cur = next; } return copyhead;}