在今天的文章中,我将带来链表的面试题。在数据结构的学习过程中,画图是尤为重要的,所以在这些题目的讲解的过程中,我以画图为主。温馨提示:由于图片过大,手机观看可能出现模糊不清的情况,建议在电脑观看该篇文章(点击图片,Ctrl+鼠标滑轮看全图)。
目录
- 1.移除链表的元素:[链接](https://leetcode.cn/problems/remove-linked-list-elements/description/)
- 2.反转链表: [链接](https://leetcode.cn/problems/reverse-linked-list/description/)
- 3.链表的中间结点[链接](https://leetcode.cn/problems/middle-of-the-linked-list/description/)
- 4.链表中倒数第k个结点:[链接](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&&tqId=11167&rp=2&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking)
- 5.合并两个有序链表:[链接](https://leetcode.cn/problems/merge-two-sorted-lists/description/)
- 6.链表分割:[链接](https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70?tpId=8&&tqId=11004&rp=2&ru=/activity/oj&qru=/ta/cracking-the-coding-interview/question-ranking)
- 7.链表的回文结构:[链接](https://www.nowcoder.com/practice/d281619e4b3e4a60a2cc66ea32855bfa?tpId=49&&tqId=29370&rp=1&ru=/activity/oj&qru=/ta/2016test/question-ranking)
- 8.相交链表:[链接](https://leetcode.cn/problems/intersection-of-two-linked-lists/description/)
- 9.环形链表:[链接](https://leetcode.cn/problems/linked-list-cycle/description/)
- 10.环形链表 II:[链接](https://leetcode.cn/problems/linked-list-cycle-ii/description/)
- 11.复制带随机指针的链表:[链接](https://leetcode.cn/problems/copy-list-with-random-pointer/description/)
- 其他链表题目:[牛客网](https://www.nowcoder.com/exam/oj) || [leetcode](https://leetcode.cn/tag/linked-list/problemset/)
1.移除链表的元素:链接
题目要求:给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
如该题目的示例1,我们需要删除链表中的存储数值为6的结点,那么我们应该如何做呢?
在删除的过程中,头结点可能是我们要删除的目标,所以我们定义新的头结点NewHead,来更新链表。头结点NewHead赋值为NULL。
我们需要定义新的尾结点Ptail来更新新头结点的尾。尾结点Ptail赋值为NULL。
在删除链表的结点时,我们需要定义结点Next来存储要删除的结点的下一个结点的地址,以免找不到下一个结点。结点Next赋值为NULL。
我们要定义结点cur来遍历旧的链表。结点cur赋值为旧链表的头结点。
特殊情况:
当旧链表的最后一个结点被删除后,并且倒数第二个结点没有被删除,由于链表的next存储着下一个结点的地址,那么倒数第二个结点的next为野指针。因为旧链表倒数第一个结点被删除,所以旧链表倒数第二个结点就为新链表的尾指针,也就是赋值给Ptail,此时Ptail的next为野指针,那么Ptail的next要置为空指针。
当旧链表的所有的结点都要被删除时,那么Ptail一直没有被赋值,此时,Ptail的值为NULL,那么不能对Ptail进行解引用来修改next的值。(不能对空指针进行解引用)
思路1:
代码:
struct ListNode* removeElements(struct ListNode* head, int val)
{
if(head == NULL)
return NULL;
struct ListNode* NewHead = NULL,*Ptail = NULL;
struct ListNode* cur = head;
while(cur != NULL)
{
struct ListNode* Next = cur->next;
if(cur->val != val) //判断是否为要删除的结点
{
if(Ptail == NULL) //第一次对头指针NewHead和尾指针Ptail进行赋值
{
NewHead = Ptail = cur;
}
else //修改尾指针Ptail的next值和尾指针Ptail向后走
{
Ptail->next = cur;
Ptail = Ptail->next;
}
cur = Next;
}
else //删除结点
{
free(cur);
cur = Next;
}
}
if(Ptail) //如果Ptail不是空指针,那么将Ptail的next值改为NULL,防止野指针的存在
Ptail->next = NULL;
return NewHead;
}
思路2:
在前面的想法下,我进行改进,将NewHead、ptail指向NULL改为指向哨兵位,在加上哨兵位后,我们可以直接在哨兵位尾插结点,不用在判断Ptail是不是指向空指针后才进行尾插。
在前面的想法中,我们需要将尾结点Ptail的next置空,然而可能由于原来的链表的所有结点都被删除了,Ptail没有变化,一直指向空指针,所以在将尾结点的next置空前,需要判断Ptail是不是指向空指针。在加上哨兵位后,我们就不需要判断了,因为Pail一开始指向哨兵位,而不是空指针。
在加上哨兵位后,代码将被大大的优化。
代码:
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* guard,*Ptail;
guard = Ptail = (struct ListNode*)malloc(sizeof(struct ListNode));
while(cur != NULL)
{
if(cur->val != val)
{
Ptail->next = cur;
Ptail = Ptail->next;
cur = cur->next;
}
else
{
struct ListNode* Next = cur->next;
free(cur);
cur = Next;
}
}
Ptail->next = NULL;
struct ListNode* NewHead = guard->next; //返回头结点,不要返回哨兵位
free(guard);
return NewHead;
}
2.反转链表: 链接
题目要求:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
如该题目的示例1,我们需要将把整个链表的结点给逆转过来。
思路1:
我们需要定义一个翻转链表后的头指针Rhead,初始化为NULL。
定义一个结点cur,遍历翻转前的链表。
定义一个结点next,存储着curr的下一个结点的地址。
代码:
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = cur = head,*Rhead = NULL;
while(cur != NULL)
{
struct ListNode* Next = cur->next;
cur->next = Rhead;
Rhead = cur;
cur = Next;
}
return Rhead;
}
思路2:
定义三个结点n1、n2、n3,分别指向翻转前的链表的空指针、第一个结点、第二个结点,结点n3存储着结点n2的下一个结点,然后将结点n2的next值改为n1的地址,三个结点向后走,循环下去,直到结点n2指向空指针。
代码:
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL) //判断头指针非空,不然初始化n3时,存在着对空指针解引用的问题
return NULL;
struct ListNode* n1,*n2,*n3;
n1 = NULL;
n2 = head;
n3 = n2->next;
while(n2 != NULL)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n2 != NULL)
n3 = n2->next;
}
return n1;
}
3.链表的中间结点链接
题目要求:给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
如该题的示例1,我们找到中间结点,即存储的值为3的结点,将它做为新头指针并返回,那么我们应该怎么做呢?
使用快慢指针,定义两个指针分别是fast、slow,指针slow和指针fast都指向链表的头结点,指针slow每次往后走一步,指针fast每次往后走两步,当指针fast的next为NULL或者指针fast为NULL,指针slow分别指向中间结点或者第二个中间结点(在中间结点的个数为2个的时候)。
链表结点的个数为奇数时(链表只有一个中间结点):
链表的结点个数为偶数时(链表有两个中间结点):
代码:
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* slow,*fast;
slow = fast = head;
while(fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
4.链表中倒数第k个结点:链接
题目要求:输入一个链表,输出该链表中倒数第k个结点。
如示例1,我们需要找到链表的倒数第1个结点,也就是存储的数值为5的结点。
思路1:依然使用快慢指针,让快指针fast先走k步,再让快指针fast和慢指针slow一起往后走,当快指针fast指向空指针时,慢指针slow刚好指向链表中倒数第k个结点。
特殊情况:k可能过大,超过了链表的长度,导致快指针往后走的距离大于链表的长度,造成越界。
假设我要找到倒数第二个结点。
代码:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
if(pListHead == NULL)
return NULL;
struct ListNode* slow,*fast;
slow = fast = pListHead;
while(--k) //k--总共循环k次,快指针fast往后走k步
{
fast = fast->next;
if(fast == NULL) //k的步长大于链表的长度
return NULL;
}
while(fast->next != NULL)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
思路2:依然使用快慢指针,让快指针fast先走k-1步,再让快指针fast和慢指针slow一起往后走,当快指针fast的next指向空指针时,慢指针slow刚好指向链表中倒数第k个结点。
假设我要找到倒数第二个结点。
代码:
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
if(pListHead == NULL)
return NULL;
struct ListNode* slow,*fast;
slow = fast = pListHead;
while(--k) //--k总共循环k-1次,快指针fast往后走k-1步
{
fast = fast->next;
if(fast == NULL) //k-1的步长大于链表的长度
return NULL;
}
while(fast->next != NULL)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
5.合并两个有序链表:链接
题目要求:将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
如示例1,我们需要将两个链表按照从大到小的顺序和成一个新的链表。
思路1:题目中已经分别给出指向两个链表的头结点的指针,我们需要再定义指向新链表头结点的指针NewHead和指向新链表尾结点的指针Ptail,并初始化为NULL。遍历题目给的两个链表,谁存储的值小就先尾插到新链表。
特殊情况:当两个链表其中一个为空时,直接返回非空的链表。
在遍历的过程中,其中一个链表遍历完,而另外一个链表还没有遍历完,那么直接将没有遍历完的链表对应的结点尾插到Ptail,因为在上一条特殊情况的处理下,两个链表都是非空,那么程序就有进入下面代码的while循环,所以Ptail不可能是NULL,不用判断Ptail,直接尾插。
代码:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
if(list1 == NULL)
return list2;
if(list2 == NULL)
return list1;
struct ListNode* NewHead = NULL,*Ptail = NULL;
while(list1 != NULL && list2 != NULL)
{
if(list1->val > list2->val)
{
if(Ptail == NULL)
{
NewHead = Ptail = list2;
}
else
{
Ptail->next = list2;
Ptail = Ptail->next;
}
list2 = list2->next;
}
else
{
if(Ptail == NULL)
{
NewHead = Ptail = list1;
}
else
{
Ptail->next = list1;
Ptail = Ptail->next;
}
list1 = list1->next;
}
}
if(list1 != NULL)
Ptail->next = list1;
if(list2 != NULL)
Ptail->next = list2;
return NewHead;
}
思路2:加入哨兵位,那么在进入while循环进行第一次判断时,就不用判断Ptail是否指向空指针了,并且也不用判断两个链表都是非空的,如果有一个链表为空,直接尾插非空链表,因为Ptail指向哨兵位,Ptail不再是空指针。
代码:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2)
{
struct ListNode* guard,*Ptail;
guard = Ptail = (struct ListNode*)malloc(sizeof(struct ListNode));
if(guard == NULL)
{
perror("malloc fail");
exit(1);
}
guard->next = NULL;
while(list1 != NULL && list2 != NULL)
{
if(list1->val > list2->val)
{
Ptail->next = list2;
Ptail = Ptail->next;
list2 = list2->next;
}
else
{
Ptail->next = list1;
Ptail = Ptail->next;
list1 = list1->next;
}
}
if(list1 != NULL)
Ptail->next = list1;
if(list2 != NULL)
Ptail->next = list2;
struct ListNode* NewHead = guard->next;
free(guard);
return NewHead;
}
6.链表分割:链接
题目要求:现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
如上面的链表中,我们要将小于3的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针,那么我们应该怎么做呢?
思路:定义一个指针,用来遍历题目给的链表。定义两个带哨兵位的新链表,一个尾插旧链表大于x的结点,一个尾插旧链表小于x、等于x的结点,最后,将存储大于x的结点的整个链表插到另外的存储着小于x、等于x的结点的链表的后面。
题目只要求小于x的结点排在其余结点之前,上面的图片修改一下,后面代码是正确的。
class Partition {
public:
ListNode* partition(ListNode* pHead, int x)
{
struct ListNode* greaterHead,*greaterTail = NULL;
struct ListNode* LessHead,*LessTail = NULL;
greaterHead = greaterTail = (ListNode*)malloc(sizeof(struct ListNode));
LessHead = LessTail = (struct ListNode*)malloc(sizeof(struct ListNode*));
greaterTail->next =LessTail->next = NULL;
struct ListNode* cur = pHead;
while(cur != NULL)
{
if(cur->val < x)
{
LessTail->next = cur;
LessTail = LessTail->next;
}
else
{
greaterTail->next = cur;
greaterTail = greaterTail->next;
}
cur = cur->next;
}
LessTail->next = greaterHead->next;
greaterTail->next = NULL;
struct ListNode* Phead = LessHead->next;
free(greaterHead);
free(LessHead);
return Phead;
}
};
7.链表的回文结构:链接
题目要求:对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。回文结构的例子:1->2->2->1。给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
思路:找到中间节点(链表个数为奇数)或者第二个中间结点(链表个数为偶数),反转该结点到尾结点的所有结点,将链表原来的头指针和反转后的头指针进行遍历前半段、后半段的链表,观察是不是链表的回文结构,详细看图。
链表结点个数为奇数的回文结构
链表结点个数为偶数的回文结构
代码:
class PalindromeList {
public:
struct ListNode* FindMidNode(struct ListNode* Head)
{
struct ListNode* slow,*fast;
slow = fast = Head;
while(fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
struct ListNode* ReverseList(struct ListNode* Head)
{
struct ListNode* cur = Head;
struct ListNode* RHead = NULL;
while(cur != NULL)
{
struct ListNode* Next = cur->next;
cur->next = RHead;
RHead = cur;
cur = Next;
}
return RHead;
}
bool chkPalindrome(ListNode* A)
{
struct ListNode* mid = FindMidNode(A);
struct ListNode* Head = ReverseList(mid);
while(Head->next != NULL)
{
if(A->val != Head->val)
return false;
A = A->next;
Head = Head->next;
}
return true;
}
};
8.相交链表:链接
题目要求:给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回NULL。
如图片的情况,两个链表之间存在着相交结点,我们需要判断是否存在这种情况。
思路:先让两个链表找到各自的尾结点,并且在找尾结点的过程,记下两个链表的长度。找到尾结点后,判断两个尾结点是否相同,如果不相同,证明两个链表没有相交,相反就有相交(原因看上图)。在相交的情况下,我们就要找到交点,求出两个链表的长度之差,然后让长度较长的链表的头指针走完这个差值,此时,两个链表的头指针与交点的距离是相等的,让两个头指针一起往后走,直到两者相等,那么该结点就是交点了。
代码:
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
struct ListNode* curA = headA,*curB = headB;
int lenA = 0,lenB = 0;
//判断尾结点是否相同
while(curA->next != NULL)
{
curA = curA->next;
lenA++;
}
while(curB->next != NULL)
{
curB = curB->next;
lenB++;
}
if(curA != curB)
return NULL;
//让长链表的头指针先走差值步数
int gap = abs(lenA - lenB);
struct ListNode* longList = headA,*shortList = headB;
if(lenB > lenA)
{
longList = headB;
shortList = headA;
}
while(gap--)
{
longList = longList->next;
}
//让两个指针一起往后走,相等就是第一个交点
while(longList != shortList)
{
longList = longList->next;
shortList = shortList->next;
}
return longList;
}
9.环形链表:链接
题目要求:给你一个链表的头节点 head ,判断链表中是否有环。
如该题目的示例1,我们需要判断该链表是否带环。
思路:利用快慢指针,慢指针每次走一步,快指针每次走两步,所以快指针一直在慢指针的前面,然后快指针会追上慢指针,也就是相遇,此时就可以证明该链表带环。
这时就会有人说了,上面的方法是否只适用于示例一的特殊情况?下面,我就来证明该想法的适合于所有的情况。
如上面的链表中,我们尝试用快慢指针再次相遇来判断该链表带环。
因为N最大也只是接近于环的长度,所以追击小于一圈。
代码:
bool hasCycle(struct ListNode *head)
{
struct ListNode* slow,*fast;
slow = fast = head;
while(fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
return true;
}
return false;
}
此时,会不会又有人提出疑问,快指针每次走3步,或者4步、或者5步、或者n步,慢指针不变化,在两个指针距离为N个结点时,快指针能不能追上慢指针。
在快指针每次走4步下
快指针每次走4步,慢指针每次走1步,那么它们的差值就是3
两个指针之间的距离变化
N为偶数
N
N-3
N-6
……
4
1
-2
两个指针指针之间的距离为-2是去什么情况呢?如下图
两个指针之间的距离为-2,即为fast指针超过slow指针两个结点,此时,假设环有C个结点,那么fast指针要重新追到slow,就要走C-2个结点,如果C-2依然为偶数,那么两个指针将永远不会相遇。
如果C-2是奇数,如:3、9、12,那么可能相遇,如果是7,那么不可能相遇。即C-2为奇数,两个指针可能相遇。
在上面的情况中,如果fast指针想要追上slow指针,那么要追的圈数大于一圈。
其他情况也是如此,存在着一些追不上的情况,只有快指针每次走两步,慢指针每次走一步,它们之间的差值为1时,才能在所有情况能追上。
10.环形链表 II:链接
**题目要求:**给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回NULL。
如示例1,我们需要找到入环的第一个结点,也就是存储着2的结点。
思路:引用上一题的思路,我依然找到快、慢指针的相遇点,然后再定义一个指针从头结点开始,一个指针从相遇点开始,开始向后走,最后将会在入环的第一个结点相遇。
至于为什么一个指针从头结点开始,一个指针从相遇点开始,开始向后走,最后将会在入环的第一个结点相遇,我来说明一下。
代码:
struct ListNode *detectCycle(struct ListNode *head)
{
struct ListNode* slow,*fast;
slow = fast = head;
while(fast != NULL && fast->next != NULL)
{
slow = slow->next;
fast = fast->next->next;
if(slow == fast)
{
struct ListNode* meet = slow;
struct ListNode* cur = head;
while(cur != meet)
{
cur = cur->next;
meet = meet->next;
}
return cur;
}
}
return NULL;
}
11.复制带随机指针的链表:链接
题目要求:给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节。构造这个链表的深拷贝。深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
如示例1,我们需要拷贝该链表,并且保证random的指向与原链表相同。
思路:在每个链表的非空结点后面链接一个新的结点,将该新结点的值改为相应的值,最近将所有链接的结点拆下来,构成一个新的链表,那么此时,链表的拷贝就结束了。
1.链接结点
2.设置链接结点的random
3.将链接的结点解下来,构成一个新链表
代码:
struct Node* copyRandomList(struct Node* head)
{
//链接结点
struct Node* cur = head;
while(cur != NULL)
{
struct Node* Next = cur->next;
struct Node* copy = (struct Node*)malloc(sizeof(struct Node));
copy->val = cur->val;
cur->next = copy;
copy->next = Next;
cur = Next;
}
//设置链接结点的random
cur = head;
while(cur != NULL)
{
struct Node* copy = cur->next;
if(cur->random == NULL)
{
copy->random = NULL;
}
else
{
copy->random = cur->random->next;
}
cur = cur->next->next;
}
//将结点解下来,链接成新的链表
struct Node* copyHead = NULL,*copyTail = NULL;
cur = head;
while(cur != NULL)
{
struct Node* Next = cur->next->next;
struct Node* copy = cur->next;
cur->next = Next;
cur = Next;
if(copyTail == NULL)
{
copyHead = copyTail = copy;
}
else
{
copyTail->next = copy;
copyTail = copyTail->next;
}
}
return copyHead;
}
其他链表题目:牛客网 || leetcode
今天,链表的题目讲解就到这里,关注点一点,下期更精彩。