【数据结构 C语言版】第三篇 单链表习题

写在前面

更新情况记录:

最近更新时间更新次数

参考博客与书籍以及链接:
(非常感谢这些博主们的文章,将我的一些疑问得到解决。)

参考博客链接或书籍名称
深入理解快慢指针算法 – 链表环路检测 - 知乎 (zhihu.com)
代码随想录
总目录:

说明:本篇文章知识脉络并非采用线性顺序,有所不会请看目录,根据知识点来查看。

未完待续,最核心的知识已经写完了。
线性表题目系列,预计两章:单链表、顺序表。

题目来源:牛客跟力扣(待更新)。

零、知识图谱

image-20221009154830585

一、单链表必会题

必会题来源于牛客力扣

1.移除链表元素

题目

力扣题目:203.移除链表元素

image-20221003214619815

题解

(1)可以在单链表元素之前设置一个哨兵头,这样的话删除链表的头结点跟其他结点的方法一致。

(2)先找到要删除结点的前一个结点,然后前一个结点的next更新为要删除结点的next,释放要被删除的结点。

image-20221003220853824

(3)来看看如何设置一个虚拟头(哨兵头)。依然还是在这个链表中,移除元素1。

203_链表删除元素6

(4)图解

image-20221003222527041

实现注意点

(1)不过oj题中基本不带头,题目也没有说明链表带头所以我们需要自己创建一个哨兵头。

struct ListNode * guard=(struct ListNode*)malloc(sizeof(struct ListNode));

需要注意的是,最后哨兵头需要被释放,头结点更新,头结点指向哨兵结点的下一个。

 head=guard->next;
 free(guard);

代码

struct ListNode* removeElements(struct ListNode* head, int val){
    struct ListNode* cur=head;
    //创造哨兵节点
    struct ListNode* guard=(struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* tail=guard;
    while(cur)
    {
        if(cur->val!=val)
        {
            //后一个节点更新
            tail->next=cur;
            tail=tail->next;
            //前一个节点更新
            cur=cur->next;
        }
        else
        {
            struct ListNode* del=cur;
            cur=cur->next;
            free(del);
        }
    }
    if(tail)
    tail->next=NULL;
    head=guard->next;
    free(guard);
    return head;
}
2.反转一个单链表

题目

力扣206. 反转链表

image-20221004064350279

题解(方法一)

(1)尾插变头插,取结点头插到新链表

image-20221004065541850

(2)代码细节过程图解

image-20221004070315875

方法一代码

struct ListNode* cur=head;
struct ListNode* newhead=NULL;
while(cur)
{
    struct ListNode* next=cur->next;
    cur->next=newhead;
    newhead=cur;
    cur=next;
}
return newhead;

题解(方法二)

(1)翻转方向

image-20221004071645679

(2)代码实现细节图解

image-20221004072211861

方法二代码

struct ListNode* reverseList(struct ListNode* head){
    struct ListNode* n1,*n2,*n3;
    //防止链表为空n3赋值放在循环里面
    n1=NULL,n2=head,n3=NULL;
    while(n2)
    {
        n3=n2->next;
        n2->next=n1;
        
        //迭代
        n1=n2;
        n2=n3;
    }
    return n1;
}   
3.链表的中间结点

题目

链表的中间结点

image-20221004072755957

题解

(1)通过快慢指针,可以只遍历一遍链表就能得出正确答案

(2)快指针一次走两步,慢指针一次走一步,快指针走完链表,慢指针刚好在一半处。

(3)图解

image-20221006155101974

代码

struct ListNode* middleNode(struct ListNode* head){
    //快慢指针
    struct ListNode* f=head,*s=head;
    //注意两个判断条件
    while(f&&f->next)
    {
        s=s->next;
        f=f->next->next;
    }
    return s;
}
4. 链表中倒数第k个结点

题目

[链表中倒数第k个结点_牛客题霸_牛客网 ](链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com))

题解

(1)方法还是快慢指针

(2)fast先走一步,fast跟slow再同时走,最后slow停在的地方就是倒数第一个。

类推,fast先走k步,fast跟slow再同时走,最后slow停在的地方就是倒数第几个。

(3)图解:
image-20221006163055677

注意

(1)为什么fast先走的循环中使用k–,使用–k可以吗?

代码while(k–),k–的含义:每次先用k,然后再自减。每次循环k的变化(以上图为例):3,2,1。当k==0的时候不执行。

当while(–k)时,–k的含义:每次用k的时候先自减。k的变化:2,1。导致就执行两次。

(2)链表可能为空需要判断。

代码

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k ) {
    struct ListNode *fast,*slow;
    fast=slow=pListHead;
    //fast先走n步
    while(k--)
    {
        //当链表为空的时候,返回空值
        if(!fast)
        {
            return fast;
        }
        fast=fast->next;
    }
    //fast跟slow一直走到fast为空的时候
    while(fast)
    {
        fast=fast->next;
        slow=slow->next;
    }
    return slow;
}
5.合并两个有序链表

题目

合并两个有序链表

image-20221006164059127

题解

(1)写一个哨兵头,比大小,小的只管尾插,不需要考虑头插。

(2)图解

image-20221006165031320

代码

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    struct ListNode* guard =(struct ListNode*)malloc(sizeof(struct ListNode));
    struct ListNode* cur1,*cur2,*newhead,*cur3;
    guard->next=NULL;
    cur1=list1,cur2=list2,newhead=cur3=guard;
    //小的尾插
    while(cur1&&cur2)
    {
        if(cur1->val>=cur2->val)
        {
            cur3->next=cur2;
            cur3=cur3->next;
            cur2=cur2->next;
        }
        else
        {
            cur3->next=cur1;
            cur3=cur3->next;
            cur1=cur1->next;
        }
    }
    //剩余的尾插
    if(cur1)
    {
        while(cur1)
        {
          cur3->next=cur1;
            cur3=cur3->next;
            cur1=cur1->next;
        }
    }
    else
    {
        while(cur2)
        {
            cur3->next=cur2;
            cur3=cur3->next;
            cur2=cur2->next;
        }
    }
    //释放哨兵结点,更新newhead的位置
    newhead=guard->next;
    free(guard);
    return newhead;
}
6.给定值x为基准将链表分割

编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结

点之前

题目

[链表分割](链表分割_牛客题霸_牛客网 (nowcoder.com))

image-20221006165558697

题解

(1)创建两个哨兵结点,一个哨兵结点(暂且叫它lessGuard)后面插入的是小于x值的结点,另一个哨兵结点(叫它greaterGuard)后面插入的是大于x值的结点。

(2)过程图解:

image-20221006185905606

注意

(1)greaterGuard指向的那条链表最后一个结点的next为什么要处理?

如果greaterGuard指向的那条链表的最后一个结点在原链表中不是最后一个结点的话,那么就会指向lessGuard指向的链表中的最后一个结点,如图所示:

image-20221006191754031

两个链表合并的话就会出现下面这张图的情况,即形成一个环:
image-20221006191940174

代码

    ListNode* partition(ListNode* pHead, int x) {
        // write code here
        
        //lessGuard:小于x的结点的链表的哨兵头,lessTail:用于操作链表
        //greaterGuard:大于x的结点的链表的哨兵头,greaterTail:用于操作链表
        //tail用于遍历原链表
        struct ListNode *lessGuard,*lessTail,*greaterGuard,*greaterTail,*tail;
        lessGuard=(struct ListNode*)malloc(sizeof(struct ListNode));
        greaterGuard=(struct ListNode*)malloc(sizeof(struct ListNode));
        lessTail=lessGuard;
        greaterTail=greaterGuard;
        tail=pHead;
        //结点根据大小尾插到相应的链表后面
        while(tail)
        {
            if(tail->val<x)
            {
                lessTail->next=tail;
                tail=tail->next;
                lessTail=lessTail->next;
            }
            else
            {
                greaterTail->next=tail;
                tail=tail->next;
                greaterTail=greaterTail->next;
            }
        }
        //最后两个链表合并
        lessTail->next=greaterGuard->next;
        //greaterTail->next后面应该是NULL
        //否则可能形成环
        greaterTail->next=NULL;
        pHead=lessGuard->next;
        //释放掉两个哨兵结点
        free(greaterGuard);
        free(lessGuard);
        return pHead;
    }
7. 链表的回文结构

题目

[链表的回文结构](链表的回文结构_牛客题霸_牛客网 (nowcoder.com))

image-20221006190713684

题解

(1)可以先用快慢指针,找出链表的中间点,然后将后半段链表反转,最后两个链表进行比较。

(2)这题可以看成是第2题与第3题的结合。

(3)图解

image-20221006193451380

代码

bool chkPalindrome(ListNode* A) {
        // write code here
        struct ListNode* f,*s;
        f=s=A;
        while(f&&f->next)
        {
            f=f->next->next;
            s=s->next;
        }
        
        struct ListNode *cur=s, *n = NULL;
        while(cur)
        {
            struct ListNode* next=cur->next;
            cur->next=n;
            n=cur;
            cur=next;
        }

        struct ListNode *cur1=A;
        while(n&&cur1)
        {
            if(n->val!=cur1->val)
            {
                return false;
            }
                n=n->next;
                cur1=cur1->next;
        }
        return true;
    }
8.相交链表

输入两个链表,找出它们的第一个公共结点。

题目

160. 相交链表

image-20221008092248171

题解

(1)a.先可以遍历一遍,如果两个链表尾结点的地址相同就说明两个链表相交。反之,则不想交。

b.通过a步骤的遍历,分别求出两个链表的长度,然后 长度长的链表先走差距步(两个链表的长度差)。走完之后,同时走,第一个地址相等的结点就是相交结点。

(2)图解:

image-20221008095020868

代码

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    //1.分别遍历
    struct ListNode* curA,*curB;
    curA=headA,curB=headB;
    //遍历判断条件是结点下一个不为空
    //防止空结点
    if(headA==NULL||headB==NULL)
    {
        return NULL;
    }
    //len要从1开始
    int lenA=1,lenB=1;

    while(curA->next)
    {
        lenA++;
        curA=curA->next;
    }
    while(curB->next)
    {
        lenB++;
        curB=curB->next;
    }
    //如果不相等就返回空
    if(curA!=curB)
    {
        return NULL;
    }
    //2.长的走差距步
    //默认长链表是A,短链表是B(可以少写判断)
    struct ListNode* longList = headA,*shortList = headB;
    if(lenB>lenA)
    {
        longList=headB;
        shortList=headA;
    }
    //差距步总不能是负的吧
    int gap = abs(lenA-lenB);
    while(gap--)
    {
        longList=longList->next;
    }
    //3.开始同时走
    while(longList!=shortList)
    {
        longList=longList->next;
        shortList=shortList->next;
    }
    return longList;
}
9.判断链表中是否有环

给定一个链表,判断链表中是否有环。

题目

141. 环形链表

image-20221008103530196

题解

(1)快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表其实位置开始运行,

如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。比如:陪女朋友到操作跑步减肥。

(2)注意:这题切不能遍历,一遍历就死循环。

(3)图解:

image-20221008175823322

注意

关于这题的问题请看这篇文章:深入理解快慢指针算法 – 链表环路检测 - 知乎 (zhihu.com)

代码

bool hasCycle(struct ListNode *head) {
    struct ListNode* fast,*slow;
    fast=slow=head;
    while(fast&&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        if(fast==slow)
        return true;
    }
    return false;
}
10. 返回链表开始入环的第一个结点

给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL

题目

142. 环形链表 II

image-20221008180259158

题解:

(1)可以用公式证明:

a.fast走的距离是2*slow走的距离。

b.假设进环前的长度为L,假设环的长度为C。

假设入口点到相遇点距离为X。

假设slow进环前,fast在环里面转了N圈,N>=1。

slow走的距离是L+X。

fast走的距离为L+N*C+X。

c.因此 2 * slow = L + N * C + X

化简一下: L=(N-1) * C + C - X。

image-20221008183729007

代码

struct ListNode *detectCycle(struct ListNode *head) {
    //公式法
    //1.找到相遇点
    struct ListNode* fast,*slow;
    struct ListNode* A=head,*B=NULL;
    fast=slow=head;
    if(fast==NULL)
    {
        return NULL;
    }
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(fast==slow)
        {
            B=fast;
            break;
        }
    }  
    //如果B还是NULL,说明不是环
    if(B==NULL)
    {
        return NULL;
    }
    //2.A与B走的长度相同
    while(A!=B)
    {
        A=A->next;
        B=B->next;
    }
    return B;
}
11.复制带随机指针的链表

给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点

或空结点。要求返回这个链表的深度拷贝。

题目

复制带随机指针的链表
image-20221009101856966

题解

1.遍历链表,在每一个结点后面复制本结点,然后复制结点尾插在本结点的后面。

2.更新random,复制结点的random在本结点的后一个(即这个结点的复制结点)。

3.最后将链表的复制结点拿出来形成一个新链表,将原本的链表还原。

4.图解:

image-20221009154219081

代码

struct Node* copyRandomList(struct Node* head) {
    //1.拷贝原结点,链接在原结点的后面
    // if(head==NULL)
    // return NULL;
      struct Node* cur=head;
      struct Node* next=NULL;
      while(cur)
      {
          next=cur->next;
          struct Node* copy=(struct Node*)malloc(sizeof(struct Node));
          copy->val=cur->val;

          cur->next=copy;
          copy->next=next;

          cur=next;
      }
      //2.更新每个拷贝节点的random
      cur = head;
      while(cur)
      {
          struct Node* copy=cur->next;
          //分两种情况
          //1.random结点为空
          //2.random结点不为空
          if(!cur->random)
          {
              copy->random=NULL;
          }
          else
          {
              copy->random=cur->random->next;
          }
          //迭代
          cur=cur->next->next;
      }
      //3.恢复原链表
      //搞一个链表头
      struct Node* guard=(struct Node*)malloc(sizeof(struct Node));
      guard->next=NULL;
      struct Node* newhead=guard;
      cur=head;
      while(cur)
      {
          struct Node* copy=cur->next;
          next=copy->next;

          //还原老链表
          cur->next=next;
          cur=next;
          //更新新结点
          newhead->next=copy;
          newhead=copy;
          newhead->next=NULL;
      }
      newhead=guard->next;
      free(guard);
      return newhead;
      }
     {
              copy->random=NULL;
          }
          else
          {
              copy->random=cur->random->next;
          }
          //迭代
          cur=cur->next->next;
      }
      //3.恢复原链表
      //搞一个链表头
      struct Node* guard=(struct Node*)malloc(sizeof(struct Node));
      guard->next=NULL;
      struct Node* newhead=guard;
      cur=head;
      while(cur)
      {
          struct Node* copy=cur->next;
          next=copy->next;

          //还原老链表
          cur->next=next;
          cur=next;
          //更新新结点
          newhead->next=copy;
          newhead=copy;
          newhead->next=NULL;
      }
      newhead=guard->next;
      free(guard);
      return newhead;
}

本节完。

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 16
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潮.eth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值