十道OJ题帮你深入认识链表

好了,之前我们写过单链表的文章,现在我们就通过OJ题的分析,对链表的认识更加充分!

 

题目一:删除链表中等于给定值 val 的所有节点

203. 移除链表元素 - 力扣(LeetCode)

8c60d82d5c6a4c44b9f52162248bdc0c.png

代码:

struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode* cur=head;
    struct ListNode* prev=NULL;
    while(cur)
    {
        if(cur->val !=val)
        {
            prev=cur;
            cur=cur->next;

        }
        else
        {
            if(prev==NULL)
            {
                 head=cur->next;
                free(cur);
                cur=head;
            }
            else
            {
                prev->next=cur->next;
                free(cur);
                cur=prev->next;
         
            }
        }
            
            
    }
    return head;
}

 分析:

2b1a83cff7cb4ccebbba901a17e2bd46.png

方法:双指针法:这种方法挺常用的,接下来都会用到这种方法。

1.创建两个指针,即prerv(先前的),cur(当前)-->方便跳转

2.如果cur指向的那个数等于你所指定删去的那个数字就跳过去:

即上图的倒数第二种情况。

prev的下一个就直接指向cur(此时cur还在你要删的数字上面)的next,这是不是就是上图的3数字位置了。

接着要向继续(cur与prev位置不重叠),再把cur指向后来prev的next位置(即4位置)。

3.如果cur指向的那个数不等于你所指定删去的那个数字,就一起跳到下一个链表就可以了。

4.除此之外,我们还需要考虑特殊情况。即第一个位置就是我要删除的数字。

此时,我们知道prev还是NULL的。那么就把它当作条件吧。

当prev为空时,就直接将头位置指向它(cur)的下一个就可以,最后再释放cur(还没有动过它,这也是为什么我们动head的原因),这就完成了特殊情况下的删除了。

 

另外:当然还有递归的方法。这里就不用那种方法来写了。以后学到某一板块的时候就写一篇专门用递归方法做OJ题的文章的,大家也可以敬请期待!(哈哈哈哈)

 

题目二:反转一个单链表。

206. 反转链表 - 力扣(LeetCode)

70c3302c095f45efbc5cb3607639f482.png

df33cb4fec3d41dbac2b9963c2f0d051.png

方法一:建新链表

代码: 

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

 方法解析:

1bb0d23cdb364d59b8eea967526f025b.png

解答:

1.首先我们要再while循环里面弄个指针next。为什么还要弄呢

因为它这个next是要随cur的变化而变化的,你如果不弄的话,cur会找不到下一个next, 像这样弄了,待我需要用到cur的next时,直接找到这个指针就可以去到cur的next了。

2.接着,将cur指向的这个指针移到新的节点里去。怎么弄呢?

即把cur的next指向newhead。就完成了

3.此时,新的链表的头是不是就发生改变了,我们要找到新链表的头,即cur成了新的头。

也就有了newhead=cur这一步。

4.接着完成了第一步的链表转移后,我们接着下一个,就cur=next,这就显得第一步的功效了。

5.直到cur为空时,进不去循环,即代表完成了反转任务了。

方法二:原地转

除了上面一种方法外,还有一种方法,下面展示代码:

struct ListNode* reverseList(struct ListNode* head) {
     if(head==NULL)
      return NULL; 

    struct ListNode*cur,*prev,*Next;
    cur=head;
    prev=NULL;
    Next=cur->next;
    while(cur)
    {

        //反转
        cur->next=prev;
        //迭代
        prev=cur;
        cur=Next;
        if(Next)
          Next=Next->next;

    }
    
    return prev;
}

思路: 

5058026408724919ab2649e98d2a6a4a.png

1.就是在它原来的链表改指定方向就可以了。

2.创建3个指针变量:prev,cur,Next。

3.当cur不为空时,进入循环。

先进行反转:从原来的向右指改为向左指。

再移动指针 三个向右移动。

4.从上图分析可知,当Next为空时,还可以转,只有当cur不为空时,才停止,这也是为什么我们第三点中,以cur不为空时才进入循环的原因。

5.经过检验,我们知道还有一种,链表为空时的特解,即链表 中没有元素。

这时候我们就只要设定如果符合这种情况时,直接返回NULL便可以解决。

(出现其他错误,就只有一点一点调试,最后一定会成功的)。

 

题目三:链表的中间节点

给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

876. 链表的中间结点 - 力扣(LeetCode)

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* slow=head;
    struct ListNode* fast=head;
    while(fast &&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;

    }
    return slow;
}

这里我们就使用一种叫快慢指针的解法了:

快慢指针即有一个走得快的指针,一个走得慢的指针

如下图:

c87e0670d04e4a68a17aadf2aee73ec9.png

我们发现当我们让fast指针一次走两边时,slow指针一次只走一步时,当fast指针走到最后时,slow指针刚好到中间。

有人又问了,那有偶数个呢?

2b56838af744490cb96725a39208aba7.png

 偶数个同样的,让它一样的思路。

下面解这道题就显得非常简单了。

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* slow=head;
    struct ListNode* fast=head;
    while(fast &&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;

    }
    return slow;
}

1.当快指针和快指针的下一个不为空时,就进入循环,

2.快指针一次走两步。

3.慢指针一次走一步。

4.最后返回慢指针指向的位置,即中间节点。

 

题目四:输入一个链表,输出该链表中倒数第k个结点

面试题 02.02. 返回倒数第 k 个节点 - 力扣(LeetCode)

 

ade64b4b3f3f46ed97b6c572e1e48016.png

解题代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
int kthToLast(struct ListNode* head, int k) {
    struct ListNode*cur=head;
    struct ListNode* prev=head;
    while(k)
    {
        cur=cur->next;
        k--;
    }
    while(cur)
    {
        cur=cur->next;
        prev=prev->next;
    }
    return prev->val;
}

 分析过程:

885222c28e764da4bba749f42d386d37.png

文字解读:

1.我们上面已经了解了快慢指针(也是双指针)了,其实,这个题目就跟快慢指针差不多的思想。

2.当我们先让cur指针先走k步的差距。

3.接着我们再让两个指针同时走,直到cur这个指针为空时,这个时候的prev指针指向的数就是它倒数第k个的节点了。 

 

题目五:将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的

21. 合并两个有序链表 - 力扣(LeetCode)

801ccca4b4dd408c83f34ca36915db2a.png

解题代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 #include<stdlib.h>
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    struct ListNode*cur1=list1;
    struct ListNode* cur2=list2;
    struct ListNode*guard=NULL,*tail=NULL;
    guard=tail=(struct ListNode*)malloc(sizeof(struct ListNode));
    tail->next=NULL;
    while(cur1 &&cur2)
    {
        if(cur1->val<cur2->val)
        {
            tail->next=cur1;
            tail=tail->next;
            cur1=cur1->next;
        }
        else
        {
            tail->next=cur2;
            tail=tail->next;
            cur2=cur2->next;
        }
    }
    if(cur1)
    {
        tail->next=cur1;
    }
    if(cur2)
    {
        tail->next=cur2;
    }
    struct ListNode* head=guard->next;
    free(guard);
    return head;
}

过程分析:

 

c59309477e9a4af9ad905c69291103b4.png

解读图:

1.首先我们在这里使用的是两个链表指针指向的位置的数字之间进行对比。决定要移动哪一个的基本方法。

2. 首先我们先创建两个指针,一个指针指向链表1的头,一个指向链表2的头。

3.接着我们由于是把数字移动到新的链表,所以我们还要再弄一个新的链表,即新建一个指针,这个指针为空。

4.为了让新链表的头不丢失,我们新链表插入时,不使用它本身,所以需要再用一个指针。即上面的tail代替找尾。

5.只要在cur1,cur2都不为空时,才能进入循环,不会出现空指针情况

6.如果那个链表指向的数字小,就先挪那一个数字。如果两个链表的数字都相等,我们就随便移动哪个链表都行.

7.最后如果某一链表已经移动完了,另一个链表还有数字的话,这时候就直接把它剩余的一起移动就行了,此时的cur也不再需要更新了。不属于它的了。

8.最后,我们需要销毁释放新链表的头?

为什么呢?因为题目中需要我们返回的是不带头的链表,而你又弄了个头,这是不是就属于画蛇添足了。因此需要释放。

此外,由于释放了后,我们就不可以再次使用那个指针了,因此,,在释放之前,我们还需要在弄一个head指针来代替它,返回原来头的下一个节点。

9.另外,当我们完成了上面的操作后,我们提交题目会发现出现一种情况,我们没有考虑:

出现问题了,

5035ef273e25477cab2cc74e8703c4c5.png

看用例,我们发现我们没有考虑链表1和2都是空的情况,因此,我们按照这个来顺着寻找,发现需要将新链表tail指针的下一个next为空。此时,题目就基本完成了。

总结:出现9情况时,我们往往时之前没有意料得到的,这就得出了只要我们先写出它的基本框架后,如果后面再出现问题了,就一点一点的调试就可以了。因此我们必须得学会如何调试。(当然目前我的水平还没有到达,我在努力朝着那个方向前进呢~)。 

 

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

链表分割_牛客题霸_牛客网

04ff143d31dd4806bebd575a9b5c55e1.png

解题代码:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x)
     {
    ListNode* lhead,*ghead,*ltail,*gtail;
    lhead=ltail=(struct ListNode*)malloc(sizeof(struct ListNode));
    ghead=gtail=(struct ListNode*)malloc(sizeof(struct ListNode));
   ltail->next=gtail->next=NULL;
   struct ListNode*cur=pHead;
    //分开两部分
   while(cur)
   {
      if(cur->val>=x)
      {
        gtail->next=cur;
        gtail=gtail->next;
      }
      else
      {
        ltail->next=cur;
        ltail=ltail->next;
      }
        cur=cur->next;
    }
        //合成新的链表
        ltail->next=ghead->next;
        gtail->next=NULL;
        pHead=lhead->next;
        free(lhead);
        free(ghead);
        return pHead;
    }


};

 需要注意的是:这题的代码没有c语言形式,只能选择c++了,但是呢,虽然我们还没有学习c++,但是一点都不妨碍写这题目。你就当作是c语言就可以了。只不过c++多了几行代码,目前看不懂。你可以先忽略它先。

题目分析过程:

2777777902234536b33b5818b16377e2.png

文字讲解:

1.根据题目的要求,我们将原本的链表分为两部分:一部分小于k,另一部分大于k,分完了之后,再重新把他们连在一起就行了。

2.有了以上的思路后

我们创建两个指针(需要malloc),为了容易辨别,即lesstail(小于的尾),greatertail(大于的尾)。这里我们采用的是带头哨兵的方法。

3.创建好了之后我们把 lesstail,greatertail这两个指针的下一个弄成空(以便后边插入)。然后再重新弄一个cur指向原来的头

4.其次,就进入了循环,即分成两个链表的形式了。(具体过程在上图已经说得很清楚了)。

5.分完后,开始合成一个链表,和释放头哨兵。

6.最后返回pHead就完成了基本的了。

 

题目七:链表的回文结构

链表的回文结构_牛客题霸_牛客网

a5ea87cacb524a2b9c4f02f0dbdc99fe.png
 

代码部分:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
//快慢排序
struct ListNode* middlenode(ListNode*head)
{
    struct ListNode*fast=head;
    struct ListNode*slow=head;
    while(fast && fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}
//反转链表
struct ListNode* reverselist(ListNode* head)
{
    struct ListNode*cur=head;
    struct ListNode* newhead=NULL;
    while(cur)
    {
        struct ListNode*next=cur->next;
        cur->next=newhead;
        newhead=cur;
        cur=next;     
    }
    return newhead;

}

class PalindromeList {
public:
    bool chkPalindrome(ListNode* head) {
        struct ListNode*mid=middlenode(head);
        struct ListNode*rhead=reverselist(mid);
        while(head && rhead)
        {
            if(head->val !=rhead->val)
            {
                return false;
            }
            else 
            {
                head=head->next;
                rhead=rhead->next;
            }
            
        }
        return true;
    }
};

注意:这里同样是没有c语言的。所以我们选c++的,其实没有什么区别的。

过程分析: 

1.其实对于判断是否是回文结构。我们来分析分析

示例:
1->2->3->2->1
我们是不是可以在3那里直到最后开始反转
即3->2->1
反转了之后是不是就是
1->2->3了。
然后我们再比较一下3那里直到前面的数字
即1->2->3
此时,我们会发现这两个是不是一样的,就说明它是回文结构了。
再看,3那个位置是什么位置?
是不是中间位置?
中间位置,你又想到了什么?
是不是我们上面写过的题?即快慢指针那道题。
接着我们反转的部分,是不是也很熟悉?
没错,也是我们上面写过的“反转一个单链表”这道题?

 对此!我们可按照这个思路写下去,再调用函数使用,是不是就显得比较简单了?

由于上面我们已经分析过了反转和中间节点的寻找具体过程了,这里我们就不再重复。

所以我们直接来到了后面:

1.将返回的中间节点赋值给叫mid的新定义的指针;再把这个mid头开始反转。

          struct ListNode*mid=middlenode(head);
          struct ListNode*rhead=reverselist(mid);

2.最后,再通过比较,如果对应的相等,就是回文结构,反之不是。

 

 

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

160. 相交链表 - 力扣(LeetCode)

 

61f2c5ae4120402083ec2d0bd04401ae.png

代码:

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
 //缩小差距
 #include<stdlib.h>

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
   struct ListNode*tialA=headA;
   struct ListNode* tailB=headB;
   int lenA=1;
   int lenB=1;
  //求A的长度
   while(tialA->next)
   {
    tialA=tialA->next;
    lenA++;
   } 
 //求B的长度
   while(tailB->next)
   {
    tailB=tailB->next;
    lenB++;
   }
    
   //算出差距步
   int gap=abs(lenA-lenB);
   struct ListNode* shortlist=headA;
   struct ListNode* longlist=headB;
   if(lenA>lenB)
   {
    shortlist=headB;
    longlist=headA;
   }
  //走完差距步
   while(gap--)
   {
    longlist=longlist->next;
   }

    //同时走,如果不相等,,就继续
   while(longlist !=shortlist)
   {
    
        longlist=longlist->next;
        shortlist=shortlist->next;
    
   }
  //相等就直接返回
        return longlist;
   

}

分析:1.首先,我们得想我们该如何找到它的环中第一个节点呢?

4256095fa86d42d297d5935ab96b1275.png

有人可能会说可不可以倒着找呢?比如先从c3开始,向左边找,直到出现分岔口(不同时)。

答案:这种是不行的。请看下面的解释

0c79478a5717448b9a0f5c8297614968.png

我们可以看到,上面情况是不存在的环。对吧?

可是还是可以使用你上面所说的方法?虽然它的数是相等的,但是它不存在环啊。

 

所以,正确的方法:还是有利用到我们的快慢指针的方法。

方法:

1.先分别求两个链表的长度。

2.接着,让长的链表先走。消除去原来的差距步

3.最后同时走,第一个地址相同的就是交点了。

接下来,看图分析:

4d7c0a7c579147119f0c1112dd7d2f95.png

 注意的是:代码的 abs()是一个用于求整数绝对值的函数,它定义在<stdlib.h>头文件中。

由于用得很少见,所以补充一下。

题目九: 给定一个链表,判断链表中是否有环

160. 相交链表 - 力扣(LeetCode)

e660d24b28b6483084809cc8e3082b53.png

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
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;
}

 分析

1.这里我们采用的思想也是快慢指针(这里可以看出二个指针是非常常用的)

2.我们让一个指针快走(一次走两步),一个指针一个走一步。

3.直到slow==fast时,就意味着循环了,就判断退出,就行了。

证明:

 但是呢?有人又问了:它凭什么就一定是呢?

为什么slow走一步,fast走两步,他们会相遇,会不会错过呢?

现在让我们来证明一下:

c43403cfdd0c471bacdff5cdb145b5d1.png

那么,相同道理,如果是slow每次走一步,而fast每次走X(X>=3)步,那么他们会相遇吗?会错过吗?

现在来用图解来证明一下:

b6ec704cf1f9472eac3727d254823cb7.png

这里仅仅弄了X等于3的示例,其他的按照这种方法证明也是可以的。这里就不一样去证明了哈。

 

题目十:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL

142. 环形链表 II - 力扣(LeetCode)

6eb3e761cc3a47839909d75b5f82244c.png

首先,在解这道题时,想必先去给讲解一下结论的证明,我们的大脑会更容易去接受一点。

证明:

其实有了我们上一道题的证明之后,证明这道也是一点类似的。

这里是跟上面的承接的:

 

2e50d1403f7645848cc606edc2afcd5f.png

可能有很多人证明到了这一步,就认为正确了。

虽然结果确实这样,但是证明是不严谨的。为什么呢?

试想,你怎么知道在相遇之前,fast转了多少圈呢?谁说它只转了1圈呢?

下面我给一情况,就能明白为什么原因?

 

 751ebccaa4b4431198d1c0849431cbda.png

请看!大家很清楚地看得到,如果fast只转了一圈的话,slow根本不可能就和fast相遇。

因此!我们现在来给出更加严谨的证明:

25d21f34dc8548e386cfe17c45eff4e3.png

当然如果实在没法理解,按照特殊的当成一圈的情况理解也是可以的!

结论:一个指针从起始点走,另一个指针从相遇点走,他们的相遇点是进环点

解题代码:

有了上面的证明铺垫后,我相信大家转换成代码的问题不大了!

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode *detectCycle(struct ListNode *head)
{
    //寻找相遇点
    struct ListNode*fast=head;
    struct ListNode*slow=head; 
    while(fast &&fast->next)
    {
        slow=slow->next;
        fast=fast->next->next;
        //找到了相遇点了
        if(slow==fast)
        {
            struct ListNode*meet=slow;
            struct ListNode*start=head;
            //寻找进环节点
            while(meet !=start)
            {
                meet=meet->next;
                start=start->next;
            }
           //返回
             return meet;
        }
    }
    return NULL;
}

这里就不过多讲解这道题目了哈。

 

好了,写了那么久,终于写完了。其实还有一道硬骨头题目的,待下次再给大家讲解了。(请期待)

最后,到了每次鸡汤部分:

希望我们都是最好的我们,永不丢失那份倒了仍爬起来的心。坚持下去!!!

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值