学习过单链表增删查改的实现操作后,下面我们可以进一步进行到单链表的实战。实战刷题中不可能题题都是增删查改的实现,更多地都是巧妙地利用各种操作实现对链表的修改。本文章就带着大家一起学习单链表常出现且有一定难度的OJ题。
目录
1、移除链表元素
题目网址:
力扣https://leetcode.cn/problems/remove-linked-list-elements/description/
这道题目我们思考的点就是找到了和val一样的元素后如何找到他前一个元素并把它next指向的地址改为val所在结点的下一个。
所以这就要求我们同时设两个指针cur和prev,prev记录上一个的地址,cur记录当前所在的地址,这样我们就可以在比较的同时拥有上一个结点所在的地址了。
大致思路就是这样,cur先走,然后比较cur->val的值是否和val相等,不一样的话把cur赋值给prev,然后cur往前走,如果相等,cur再往前走,prev->next指向现在cur所在的地址,以此类推。
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* cur=head;
struct ListNode* prev=head;
while(cur)
{
if(cur->val!=val)
{
prev=cur;
cur=cur->next;
}
else if(cur->val==val)
{
if(cur==head)
{
head=cur->next;
free(cur);
cur=head;
}
else
{
prev->next=cur->next;
free(cur);
cur=prev->next;
}
}
}
return head;
}
2、反转链表
力扣https://leetcode.cn/problems/reverse-linked-list/description/
这道题思路很简单,大家肯定一下就想到了设立一个NULL的头结点然后不断尾插就可以了。但是一些细节会让我们陷入深思:我们在找到待插入的结点后,把他的next的值重新赋值了新的地址,那旧的地址旧找不到了,就无法进行下一步尾插操作了。这里我们想到了要多设立几个指针变量:
这里我们就设置了多个指针变量来记录地址,n1先指向NULL,n2指向第一个结点,n3指向第二个结点,然后让n2->next指向n1,再把n2赋值给n1,n3赋值给n2,继续往前走进行尾插操作:
struct ListNode* reverseList(struct ListNode* head){
if(head==NULL)
{
return NULL;
}
struct ListNode *n1=NULL,*n2=head,*n3=head->next;
while(n2)
{
n2->next=n1;
n1=n2;
n2=n3;
if(n3)
{
n3=n3->next;
}
}
return n1;
}
3、寻找链表的中间结点
力扣https://leetcode.cn/problems/middle-of-the-linked-list/description/
这道题目我们初次碰见的话很容易没有思路。最明显能想到的就是链表链表看有几个结点然后再把结点数/2(除以2)。这种方式可以实现,但不够简洁。
这里我们介绍一种简洁易懂的方法,利用快慢指针。
慢指针一次走一格,快指针一次走两格,快指针为NULL或者快指针->next为NULL时,此时慢指针所在位置就是该链表的中间位置,下面我们来看代码实现:
struct ListNode* middleNode(struct ListNode* head){
struct ListNode *slow,*fast;
slow=head;
fast=head;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
4、寻找链表倒数第k个结点
这道题和上一道题目有异曲同工之妙,我们最初会想到历遍链表然后记录结点个数,再减去k,再从头历遍到这个结点找到它。老样子,不够简洁!
我们能不能继续沿用快慢指针来实现呢?
答案是肯定的,我们设两个指针,让快指针先走k步,然后快慢指针一起走直到快指针为NULL或者快指针->next为NULL为之,非常巧妙地实现了!
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
// write code here
struct ListNode* fast,*slow;
fast=slow=pListHead;
while(k--)
{
if(fast==NULL)
{
return NULL;
}
fast=fast->next;
}
while(fast)
{
slow=slow->next;
fast=fast->next;
}
return slow;
}
5、合并两个链表并按顺序返回
力扣https://leetcode.cn/problems/merge-two-sorted-lists/description/
这道题大家的主流思想就是两个链表同时从头历遍,然后两个链表对应的结点的val值两两比较,小的值的结点连接在新的链表上,然后指针依次往下走。
几点注意的是:要考虑到一个链表为空的特殊情况,只需要返回另一个链表即可。
如果一个链表历遍完毕后,另一个还有好几个结点,需要把另一个链表的剩余结点连接到新链表上。
大家请看代码:
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
struct ListNode* head=NULL,*tail=NULL;
if(list1==NULL)
{return list2;}
if(list2==NULL)
{return list1;}
while(list1&&list2)
{if(list1->val<list2->val)
{
if(head==NULL)
{
head=tail=list1;
}
else
{
tail->next=list1;
tail=list1;
}
list1=list1->next;
}
else
{
if(head==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;
}
6、链表分割
这道题目在原链表身上修改非常麻烦,所以我们考虑另外开辟空间来存放链表。
我们的思路是开辟两个哨兵位的头结点分别存放比x数值小的结点并连接和存放比x数值大的结点并连接。历遍原链表,结点指向val的值小于x尾插到新开辟的第一个链表,否则尾插到第二个链表中。最后,把两个链表在连接起来成为一个完整的链表。当然,在程序结束前,由于返回的是有有意义数值的头结点的位置,我们还要释放掉哨兵位的头结点(它只是方便尾插的,不存放数值)。
ListNode* partition(ListNode* pHead, int x) {
struct ListNode* lessHead,*greaterHead,*lessTail,*greaterTail;
lessHead=lessTail=(struct ListNode*)malloc(sizeof(struct ListNode));
lessTail->next=NULL;
greaterHead=greaterTail=(struct ListNode*)malloc(sizeof(struct ListNode));
greaterTail->next=NULL;
struct ListNode* cur=pHead;
while(cur)
{
if(cur->val<x)
{
lessTail->next=cur;
lessTail=cur;
}
else
{
greaterTail->next=cur;
greaterTail=cur;
}
cur=cur->next;
}
lessTail->next=greaterHead->next;
greaterTail->next=NULL;
struct ListNode* newnode=lessHead->next;
free(lessHead);
free(greaterHead);
return newnode;
}
7、链表的回文结构
这道题目就比较考验大家对上面题目的理解和综合运用了。我们看到题目后会有几个疑问,由于是单链表我们无法从表尾往前和表头往后依次比较。其次,我们怎么找到要比较的地方等等。
总之,很容易让我们陷入一个思维死循环。
但是我们跳出这题目往前看,我们学过了寻找链表的中间结点,还学会了反转链表,那这道题目在我这一句话提醒下是不是有了思路呢?
我们可以找到中间结点后,然后让链表后半部分逆置,然后让其和前半部分依次比较,来判断存放的数是否相等。是不是非常巧妙呢?
bool chkPalindrome(ListNode* A) {
ListNode* slow=A;
ListNode* fast=A;
while(fast&&fast->next)
{
slow=slow->next;
fast=fast->next->next;
}
ListNode*n1=NULL;
ListNode*n2=slow;
ListNode*n3=slow->next;
while(n2)
{
n2->next=n1;
n1=n2;
n2=n3;
if(n3!=NULL)
{n3=n3->next;}
}
ListNode*p1=A;
ListNode*p2=n1;
while(p1&&p2)
{
if(p1->val!=p2->val)
{
return false;
}
else
{
p1=p1->next;
p2=p2->next;
}
}
return true;
}
8、找出链表的第一个公共结点
力扣https://leetcode.cn/problems/intersection-of-two-linked-lists/description/
对于这道题,我们看完实例后,首先会想到第一点就是判断这两个链表有没有交点。如何判断呢?第一种思路是历遍链表,比较地址,但是我们无法知道两个链表的长度,所以无法对应的一一比较。那么,我们确定一个链表的一个结点然后历遍另一个链表和这个结点比较呢?这样操作可行,但是时间复杂度太大了,我们也不建议采用,所以有没有简洁易懂而且复杂度低的方法呢?
这里有一个巧妙地操作,我们找到这两个链表的尾结点,然后比较是否一致就可以了。
下一步如果有交点怎么找到这个交点呢?其实我们在上一步找尾的时候,可以设两个计数变量len1和len2,我们就可以知道链表的长度,然后算出长度差,让长的链表先走这部分的长度差,然后和短的链表同时开始走,直到结点位置一样时就自然找到了交点。
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
struct ListNode* n1=headA;
int len1=1;
struct ListNode* n2=headB;
int len2=1;
while(n1->next)
{n1=n1->next;
len1++;}
while(n2->next)
{n2=n2->next;
len2++;}
if(n1!=n2)
{
return NULL;
}
int gap=abs(len1-len2);
struct ListNode* longList=headA;
struct ListNode* shortList=headB;
if(len1<len2)
{
longList=headB;
shortList=headA;
}
while(gap--)
{
longList=longList->next;
}
while(longList!=shortList)
{
longList=longList->next;
shortList=shortList->next;
}
return longList;
}
9、判断链表中是否有环
力扣https://leetcode.cn/problems/linked-list-cycle/description/
环形链表是典型的链表oj题,同时他的思路也是相对固定的,下面我们来仔细分析。
这一道是最基础的判断是否有环的问题。那么我们如何判断呢?最浅显的思路就是记录此刻所在的地址然后历遍链表看是否会再找到这个地址,这种思路如果我们所在的结点不在入环结点之后就无法实施,舍弃。
所以单纯的记录地址比对并不好实操,但是我们可以借鉴快慢指针的思路,把环形问题转换成一个追及问题。设有一快一慢指针指向头结点,快指针一次走一步,快指针一次走两步,在快指针不为NULL或者他的next也不为NULL的情况下,快指针肯定会先入环,我们只需要看慢指针是否会与快指针相遇就可以判断是否有环了。
那么我们是否可以让快指针一次走3步,慢指针一次走1步呢(或者其他情况)?答案是不可以的,2步和1步差是1,这样可以确保步数差为1,只要有环注定会相遇。如果是1步和3步,差为2步,慢指针进环后,如果第一次最接近后下一圈的差不是2的倍数那么永远相遇不了,所以我们都是默认2步和1步,这是必然遇到的,其他具有偶然性。
bool hasCycle(struct ListNode *head) {
struct ListNode* fast=head;
struct ListNode* slow=head;
while(fast&&fast->next)
{
fast=fast->next->next;
slow=slow->next;
if(fast==slow)
{
return true;
}}
return false;
}
10、找到链表入环的第一个结点
力扣https://leetcode.cn/problems/linked-list-cycle-ii/description/
这道题目我们会应用一个结论来做,我们先看结论:
所以我们就可以先像上题一样判断是否有环然后在按照证明的方法让两个指针分别从头和相遇点,每次各走一格,相遇的结点就为入环结点:
struct ListNode *detectCycle(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)
{
struct ListNode* meet=slow;
while(meet!=head)
{
meet=meet->next;
head=head->next;
}
return meet;
}
}
return NULL;
}
11、复制带随机指针的链表
力扣https://leetcode.cn/problems/copy-list-with-random-pointer/
这道题题目描述有点长,大概就是复制原链表的数值和结构。对于链表存放的数值复制并不难,难的是如何复制链表中random指向的地方,因为random指向是随机性的,我们如果一次次历遍显然不太现实,在这我们就给出了一种非常巧妙地方法。
用例1来举例, 我们历遍链表,然后在每一个结点后面创建一个新结点,然后将老链表和新链表链接成一个链表,如图所示。
构建好这个新链表后,我们来复制random指向的位置。其实就是根据老结点的指向,新结点指向下一个老结点指向的下一个位置,这样就类似复制了random。
最后一步,我们要将新老结点分开,重新恢复成两个链。
所有代码如下:
struct Node* copyRandomList(struct Node* head) {
struct Node* cur=head;
while(cur)
{
struct Node* newnode=(struct Node*)malloc(sizeof(struct Node));
newnode->val=cur->val;
//插入
newnode->next=cur->next;
cur->next=newnode;
cur=newnode->next;
}
cur=head;
//根据新结点,处理newnode结点的random
while(cur)
{
struct Node* newnode=cur->next;
if(cur->random==NULL)
newnode->random=NULL;
else
newnode->random=cur->random->next;
cur=newnode->next;
}
struct Node* newhead=NULL;
struct Node* newtail=NULL;
cur=head;
while(cur)
{
struct Node* newnode=cur->next;
struct Node* next=newnode->next;
if(newtail==NULL)
{
newhead=newtail=newnode;
}
else
{
newtail->next=newnode;
newtail=newnode;
}
cur->next=next;
cur=next;
}
return newhead;
}