链舞算法谱---链表经典题剖析

news2024/11/19 13:17:58

前言:探究链表算法的奥秘,解锁编程新世界!

  欢迎来到我的链表算法博客,这将是您深入了解链表算法,提升编程技能的绝佳机会。链表作为数据结构的重要成员之一,其动态性和灵活性在实现某些功能上发挥不可替代的功效。

本篇博客致力于揭秘链表算法的奥妙,分享实用的学习经验,仅供个人参考。我也希望您能独立实现代码完成相关的题型,还要尝试一题多解,发散思维。

如果对您有帮助,希望您能点赞关注作者,作者会持续输出新鲜干货,尽请期待!

前面,我们学习单链表的基本实现,比如链表的创建,销毁,增删查改等。

如果您不是很了解单链表的相关操作,可以读一下鄙人的单链表博客,或者自行学习。

数据结构篇其二---单链表(C语言+超万字解析)-CSDN博客

我们要注重理论实践结合,阅读他人的代码和相关题解(力扣网站每道题都有大佬(官方)的优秀题解供我们学习),更能熟悉掌握链表的核心知识。

本期精选链表算法的高级话题,选自力扣网站(后面附带刷题链接)。如反转链表,环形链表检测,链表相交,合并有序链表,移除链表元素,分割链表等一系列好题。

编程语言:C语言。

学习方法:

学习数据结构要多画图分析,一步一步来,微者速成。

一切条件判断,循环条件不知道怎么写的情况,都是因为图没有画清楚,逻辑没捋清楚。自己动手画比脑补来的快。

目录

前言:探究链表算法的奥秘,解锁编程新世界!

一、反转链表(简单)

思路一:(三指针法)取节点头插。

思路二:(三指针法)直接反转

思路三:(三指针法)思路一的优化版本

二、移除链表元素(简单)

思路一:(双指针法)直接删除值为val的节点。

思路二:创建新链表

 三、链表的中间节点(简单)

思路一:计算链表长度确定中间结点 

思路二:快慢指针法

 四、删除链表的倒数第k个节点(中等)

思路一:快慢指针法 

五、返回倒数第k个节点(简单)

 六、合并两个有序链表(简单)

思路一:创建新链表尾插

七、链表分割(较难)

思路一: 先分割再合并(简单的思路)

八、链表的回文结构(简单)

思路一:中间节点+反转链表+双指针法

思路二:数组(换种数据结构)+双指针(对撞指针)

九、相交链表(简单)

思路一:暴力遍历。

思路二:  (类似)快慢指针

十、环形链表(简单or难?)

思路一:快慢指针法

 结尾



一、反转链表(简单)

据说这是面试出镜率最高的链表题。

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

这里提供了三组示例供我们实际测试代码。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* reverseList(struct ListNode* head) {
    
}

代码通过注释的方式给了我们结构体的声明,接着让我们实现函数内部的逻辑。

先分析函数和返回类型吧。

返回值:struct ListNode*  结构体指针,这里返回反转链表后的头指针。

函数参数:就一个参数,传入源链表的头指针。

下面提供几种思路。

思路一:(三指针法)取节点头插。

链表的元素是节点。我们就有第一种想法,将每个节点一个一个取下来进行头插。

同时考虑多种情况不符合人脑逻辑,我们先考虑第一种示例,再完善整体的逻辑。

怎么做呢?先分析已知指针的指向。

只有一个头指针指向链表的第一个节点。我们要实现把节点依次取下来进行头插的操作。显然只有头指针是不够的,我们再创建节点(结构体)指针 。

那么创建几个结构体指针呢?

首先,我们处理链表问题,不会轻易动用头指针,除非链表的第一个节点改变了,才需要改变头指针指向。

所以要第一个头指针prev。如下图:

这样有了prev也指向第一个节点,就不要动head指针遍历链表了。

那么如何一个节点指针够了吗?

还是回归到问题上,我们要取下每个节点进行头插。

请阅读下面的代码和观察下面的图

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = head;//prev指向head指向的节点,即第一个节点。
    prev->next = NULL;//将结点指针指向的节点的指针域置空,意味着把节点取下来了。
}

 很遗憾只通过prev指针把第一个节点取下来了,但是第二个节点的位置找不到了。起不到头插的作用。

所以还需要个next的节点指针,指向取下节点的下一个节点。

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = head;//prev指向head指向的节点,即第一个节点。
    struct ListNode* next = head;//next也指向第一个节点。

    next = prev->next;//先让next指向下一个节点。
    prev->next = NULL;//将结点指针指向的节点的指针域置空,意味着把节点取下来了。
}

取第二个节点的情况

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = head;//prev指向head指向的节点,即第一个节点。
    struct ListNode* next = head;//next也指向第一个节点。

    next = prev->next;//先让next指向下一个节点。
    prev->next = NULL;//将结点指针指向的节点的指针域置空,意味着把节点取下来了。

    //取第二个节点
    prev = next;//先让prev指向原链表的第二个节点。
    next = prev->next;//代码同取下第一个节点的情况。
    prev->next = NULL;

    //执行头插操作
    prev->next = head;//连接第一个节点
    head = prev;//头插改变头指针的指向。


}

那么后面规律自然清晰可寻了,我们这里考虑结束的情况。

选择什么循环和循环条件是什么问题就迎刃而解了。 

按照前面的逻辑

prev跟上next,即prev=next;

next指向原先链表尾结点的指针指向部分,即next=NULL;

连接节点,prev->next=head;

 再调整头指针的指向,即head=prev;

最终结果出来了,这个时候的head和prev指向新链表(反转链表)的第一个节点。next指向空,可作为循环条件。

最终结果如图:

代码就可以写了

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode* prev = head;//prev指向head指向的节点,即第一个节点。
    struct ListNode* next = head;//next也指向第一个节点。

    next = prev->next;//先让next指向下一个节点。
    prev->next = NULL;//将结点指针指向的节点的指针域置空,意味着把节点取下来了。

    while (next)
    {   
        //取第二个及之后的节点
        prev = next;
        next = prev->next;
        prev->next = NULL;
        //头插
        prev->next = head;
        head = prev;
    }
    return head;


}

提交一下。

 

挫折才是算法题的常态,不急看看红字说什么。

原来是空链表的情况啊 。也就是题目所给示例3的情况。

最前面直接加个if判断就行了,空链表直接让他返回空指针就🆗了。

最终代码实现

struct ListNode* reverseList(struct ListNode* head) {
    if (head == NULL)
        return NULL;
    struct ListNode* prev = head;//prev指向head指向的节点,即第一个节点。
    struct ListNode* next = head;//next也指向第一个节点。

    next = prev->next;//先让next指向下一个节点。
    prev->next = NULL;//将结点指针指向的节点的指针域置空,意味着把节点取下来了。

    while (next)
    {
        prev = next;
        next = prev->next;
        prev->next = NULL;

        prev->next = head;
        head = prev;
    }
    return head;


}

 用时方面击败了100%的人,鼓励自己一下。

由于代码已经通过了,示例二我就不多说了,需要请您自行梳理。

思路二:(三指针法)直接反转

 直接改变指向如何实现呢?

三指针。三个指针能改变朝向,双指针只能改变一次,不能循环起来。

第一步:

断开节点1和节点2,节点一指向n1;

第二步:

按顺序n1指向n2,n2指向n3,n3指向下一个节点 (n3->next);

接下来判断循环结束条件。

需要注意一次只改变一个节点的指针部分的指向。节点1和节点2只是断开了,节点1指向了空,并没有让节点2指向节点1。

 循环结束条件为n2为NULL。

需要注意在连接最后一个节点时,n3已经为空指针了,不能再往后挪,即n3=n3->next;

补充:->结构体指针访问操作符,本质是指针解引用访问结构体的成员。而空指针不能解引用,会报错。

最后前面写个if语句,如果是空链表直接返回NULL。

struct ListNode* reverseList(struct ListNode* head) {
    if (NULL == head)//空链表
        return NULL;

    struct ListNode* n1 = NULL;
    struct ListNode* n2 = head;
    struct ListNode* n3 = n2->next;

    while (n2)
    {
        n2->next = n1;
        n1 = n2; 
        n2 = n3;
        if (n3)//尾结点改变连接朝向,n3已经为NULL,不能再解引用。
            n3 = n3->next;
    }

    return n2;
}

 

思路三:(三指针法)思路一的优化版本

思路一要修改原先的head,虽然和思路一思想一样,但重新创建一个新头指针,创建一个新链表,可读性好一些。

 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;
  }

二、移除链表元素(简单)

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

本题还是给了三组测试用例,我们先从第一组开始分析。

第一组样例:

思路一:(双指针法)直接删除值为val的节点。

要做到直接删除,至少要用两个相邻指针做到。

prev->next=pcur->next;//指向值为val的下一个节点

pcur=prev->next;//pcur指向


 

 

 结束条件:

 这里可用pcur作为循环结束的条件。

看不明白请自行画图分析。

​​ struct ListNode* removeElements(struct ListNode* head, int val) {
     struct ListNode* prev = NULL;
     struct ListNode* pcur = head;
     while (pcur)
     {
         if (pcur->val == val)//当前pcur所在节点值为val,执行删除
         {
             prev->next = pcur->next;
             free(pcur);
             pcur = prev->next;
         }
         else//pcur指向节点值不为val,prev,pcur整体往下一个节点走。保证它俩紧挨着。
         {
             prev = pcur;
             pcur = pcur->next;
         }
     }
     return head;//返回头指针
 }

接下来第二组和第三组测试用例

第二组给了空链表的示例,由于直接最前方写个if分支直接return NULL;

  if (NULL == head)
      return NULL;

第三组测试用例: 

 

由于第一个节点就符合条件,而此时prev还为NULL,prev->next的行为会报错。 

这种情况就要用单链表头插的思路,即head和pcur实现了

 这种全符合 val的情形,只有head和pcur在动,没prev什么事了。

🆗,总结一下先判断要不要进行头删,直到第一个节点不在删除就得动用prev和pcur来进行一般的删除(第一组用例的方法)。

综合一下三组用例:最终代码实现

struct ListNode* removeElements(struct ListNode* head, int val) {
    //空链表直接返回。
    if (NULL == head)
        return NULL;
    struct ListNode* prev = NULL;
    struct ListNode* pcur = head;
    //判断要不要头删。
    while (pcur->val == val&&pcur!=NULL)
    {
        head = pcur->next;
        free(pcur);
        pcur = head;
        if (pcur == NULL)//如果全是val的情况,全部执行头删,pcur将为空,判断一下
        {
            return NULL;
        }
    }
    //第一个节点不是val启动prev,pcur,遍历链表进行删除。
    while (pcur)
    {
        if (pcur->val == val)//当前pcur所在节点值为val,执行删除
        {
            prev->next = pcur->next;
            free(pcur);
            pcur = prev->next;
        }
        else//pcur指向节点值不为val,prev,pcur整体往下一个节点走。保证它俩紧挨着。
        {
            prev = pcur;
            pcur = pcur->next;
        }
    }
    return head;//返回头指针
}

 

看来情况考虑全了。

下面提供另一种思路: 

思路二:创建新链表

虽然说是创建新链表,实际并没有增加新的节点。

一个新链表(单链表)有头有尾。

 //创建新链表
 struct ListNode* newhead = NULL;
 struct ListNode* newtail = NULL;

接下来我们要遍历原链表,找到值不为val的节点取下来进行尾插。

  struct ListNode* pcur = head;//引入一个指针变量,遍历整个链表

 第一步:

由于第一个节点val值为1,取下来。

  newhead = pcur;
  newtail = pcur;

第二步:

pcur向后遍历,且让取下来的第一个节点指向空。因为单链表最后一个节点必须指向空,不然会出问题。

 pcur = pcur->next;//往下继续遍历。
newtail->next=NULL;//新链表尾指针指向空

第三步:

由于第二个节点val值为2,也取下来。看下图

  newtail->next = pcur;
  newtail = newtail->next;

第四步:

pcur继续向后遍历,且让取下来的第二个节点指向空。结合上图分析。

pcur=pcur->next;
newtail->next=NULL;

到这里规律就很明显了。

首先是pcur遍历原链表,pcur指向原链表的空指针说明循环结束。

pcur->val不满足指定val就尾插到新链表,此时又分两种情况,即第一次插入和后面的插入。第一次插入要修改头指针,使头指针指向第一个节点。后面进行尾插,只需要后一个节点连接前一个节点,并移动尾指针即可。

然后,若pcur->val满足指定val,就释放该节点,继续往后遍历。

需要注意的是,将节点取下来只是个形象的说法,本质就是新链表的头尾指针指向原先的节点。每次满足条件的节点取下来其指针部分指向要改为空。

如果觉得文字看不明白的话

请您自己画图配合看一下下面的代码吧。

struct ListNode* removeElements(struct ListNode* head, int val) {
    //该代码逻辑上包含了空链表,不用单独判断了。
    struct ListNode* newhead = NULL;
    struct ListNode* newtail = NULL;
    struct ListNode* pcur = head;
    while (pcur)//遍历原链表
    {
        if (pcur->val != val)//不满足指定val
        {
            if (NULL == newtail)//新链表第一次插入节点
            {
                newhead = pcur;
                newtail = pcur;
            }
            else//后面的插入,和单链表尾插相同的思路
            {
                newtail->next = pcur;//连接前一个节点
                newtail = newtail->next;//改变尾指针的指向
            }
            pcur = pcur->next;//往下继续遍历。
            newtail->next = NULL;//尾指针指向空,保证新链表(单链表)不出错
        }
        else
        {
            struct ListNode* del = pcur;//临时指针存储满足值为val的节点
            pcur = pcur->next;//继续遍历
            free(del);//释放满足条件的节点

        }
    }
    return newhead;
}

 三、链表的中间节点(简单)

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

思路一:计算链表长度确定中间结点 

这种思路很简单,引入int len;先遍历链表计算链表的长度,再遍历找到中间节点并打印。

链表的长度是节点的个数。

把链表元素(节点)想象成数组元素下标的形式

比如下面是奇数个节点就是 ,链表长度为5,中间长度就是5/2==2(整数除法)。就是对应下标为2的节点,就是3.

对于链表长度是偶数,节点个数是偶数的情况呢。

这里链表长度是6,中间长度是6/2=3,对应下标为三的节点,是4。符合题意。

int k = len / 2;就是中间长度了。

 struct ListNode* middleNode(struct ListNode* head)
  {
      if (head == NULL)//空链表
          return head;
      int len = 0;//长度变量
      struct ListNode* pcur = head;
      while (pcur)//遍历求链表长度
      {
          pcur = pcur->next;
          len++;
      }
      pcur = head;//重置pcur
      int k = len / 2 ;//中间结点到起点的长度。
      while (k--)
      {
          pcur = pcur->next;
      }
      return pcur;//返回中间节点的指针
  }

思路二:快慢指针法

思路一方法很好理解,但由于要计算长度,终归要先遍历一遍数组。然后找中间节点,相当于又遍历了一遍。

下面介绍快慢指针法

快慢指针,广泛应用于大量的算法场景。

先介绍背景,这种方法在解决此题时,不用求链表长度,只需遍历一遍。

快慢指针

以单链表为背景

快慢指针指的是移动步长,正如名称所示,快指针走的快(单次走的步数多),慢指针走得慢(单次走的少)

什么又叫走的步数多和走的步数少呢?

先说快慢指针的定义

struct ListNode {
    int val;
    struct ListNode *next;
};
  struct ListNode* fast = head;
  struct ListNode* slow = head;

快慢指针这里是结构体变量,很显然 fast和fast->next类型一样,可以进行赋值操作。

那么fast->next->next呢?也是结构体指针类型。

好像明白了!

fast=fast->next;//走一步。

fast=fast->next->next;//走两步。

同一循环下,慢指针走一步,快指针走两步。快指针到终点,结束循环,慢指针才走到链表的一半,就是中间节点处。

实践开始:

先考虑题中给的第一组示例。

struct ListNode* middleNode(struct ListNode* head)
{
    if (head == NULL)
    {
        return NULL;//空链表可用不上快慢指针,直接返回空即可
    }
    //定义快慢指针
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while (fast->next)
    {
        slow = slow->next;//慢走一
        fast = fast->next->next;//快走二
    }
    return slow;//慢指针所指向的就是中间节点
}

做到这儿大多人已经迫不及待地提交了。

只过了一个测试用例,太惨了。观察一下上面的图,题目中给定的测试用例二我们还没测呢。

 偶数情况下的循环结束条件是fast==NULL;

慢指针依旧指向中间节点。

struct ListNode* middleNode(struct ListNode* head)
{
    if (head == NULL)
    {
        return NULL;//空链表可用不上快慢指针,直接返回空即可
    }
    //定义快慢指针
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while (fast->next && fast)//补上偶数的情况
    {
        slow = slow->next;//慢走一
        fast = fast->next->next;//快走二
    }
    return slow;//慢指针所指向的就是中间节点
}

然后提交。

呃?怎么还是报错了,明明逻辑没有错啊?

好吧还是错了,问题肯定出在修改过的地方。

while (fast->next && fast)

逻辑与是从左向右计算表达式的结果的。若fast已经为NULL,它会先进行左边的判断fast->next,显然会报错。应该先判断fast是不是NULL,在判断fast->next。

while ( fast  && fast->next )

struct ListNode* middleNode(struct ListNode* head)
{
    if (head == NULL)
    {
        return NULL;//空链表可用不上快慢指针,直接返回空即可
    }
    //定义快慢指针
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while (fast && fast->next)//奇数个节点和偶数个节点的情况
    {
        slow = slow->next;//慢走一
        fast = fast->next->next;//快走二
    }
    return slow;//慢指针所指向的就是中间节点
}


🆗,思路二及原理讲解和可能遇到的问题就解决了。

 四、删除链表的倒数第k个节点(中等)

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

思路一:快慢指针法 

此题和链表的中间节点都可采用快慢指针法,只不过用快慢指针的方式不同。

快慢指针不一定比谁走得快,而可以认为谁走得远。


slow负责找倒数第k个节点,fast领先slow结束循环。

这里可以看到fast比slow多走了一步,可以用fast->next作为结束循环的条件。

那么这里有普遍的规律吗?

找倒数第二个节点,fast比slow多走一步。

找倒数第三个节点,fast同样指向尾结点,此时fast比slow多走两步。

找倒数第一个节点,fast同样指向尾结点,此时fast和slow相差0步



原来如此,找倒数第k个节点,fast要先走k-1步,然后再和slow一起走,符合快慢指针。

这里要执行删除操作,我们在定义一个prev指针循环找slow指向的前一个结点。

struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
    if (NULL == head)
        return NULL;
    struct ListNode* fast = head;
    struct ListNode* slow = head;
   
    int k = n-1 ;
    while (k--)//fast先走n-1步
    {
        fast = fast->next;
    }
    while (fast->next)//同时走
    {
        fast = fast->next;
        slow = slow->next;
    }
    struct ListNode* prev = head;//定义prev指针找slow的前一个结点。
    while (prev->next != slow)//找……ing
    {
        prev = prev->next;
    }
    //执行删除操作
    prev->next = slow->next;
    free(slow);

    //返回头指针。
    return head;

}

 只过了一个测试用例?嗯冷静!

根据链表节点删除的经验,这种情况十有八九没考虑头删的情况。

下面分析一下不满足的测试样例。

 struct ListNode* prev = head;//定义prev指针找slow的前一个结点。
    while (prev->next != slow)//找……ing
    {
        prev = prev->next;
    }

 明白了,这个时候prev和slow一样,要删掉头节点,这个循环不包括头删的情况。

那么用if_else语句分开讨论吧。

  struct ListNode* removeNthFromEnd(struct ListNode* head, int n) {
      if (NULL == head)
          return NULL;
      struct ListNode* fast = head;
      struct ListNode* slow = head;
     
      int k = n-1 ;
      while (k--)
      {
          fast = fast->next;
      }
      while (fast->next)
      {
          fast = fast->next;
          slow = slow->next;
      }


      //判断头删还是非头删的情况
      if (slow == head)
      {
          head = head->next;//修改头指针的指向即可。
      }
      else {
          struct ListNode* prev = head;//定义prev指针找slow的前一个结点。
          while (prev->next != slow)//找ing
          {
              prev = prev->next;
          }
          //执行删除操作
          prev->next = slow->next;
          free(slow);
      }

      //返回头指针。
      return head;

  }

时间上打败了全世界的人,加油!

总结

1.执行链表删除时考虑头删的情况。

2.考虑空链表的情况。

3.熟练快慢指针在此题的应用。

4.不用像作者那样试错,自己考虑完所有情况(画图分析),一遍过就完事。

遗漏的地方自己要独立分析完成。

五、返回倒数第k个节点(简单)

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

本题作为四题快慢指针的补充。

四题有过这幅图

下面我给出另一张图,分析一下区别。

 结论就是循环条件不同,前者以fast->next==NULL结束,后者以fast==NULL,结束。

还有一个区别:前者fast比slow先走k-1步,后者fast先走k步。

🆗,我们用后者的形式快速解决这道题。

int kthToLast(struct ListNode* head, int k) {
    if (head == NULL)//空链表踢出跑步比赛资格
    {
        return NULL;
    }
    struct ListNode* fast = head;
    struct ListNode* slow = head;
    while (k--)//fast先行k步
    {
        fast = fast->next;
    }
    while (fast)//fast和slow同时走,注意此时结束条件
    {
        fast = fast->next;
        slow = slow->next;
    }
    return slow->val;//返回倒数第k个节点的val值
}

be far ahead

写得最快的一题。

 六、合并两个有序链表(简单)

据说这是面试出镜率第二高的面试题。

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

leetcode给的函数原型。

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
    
}

函数参数是两个链表的头指针,返回合并后链表的头指针。

思路一:创建新链表尾插

不需要复杂的套路,只要给我一个新链表的头指针和尾指针,我就能解决这道题。

 由于目的是合并成升序表,原先的两个链表的节点两两对应比较,选择小的尾插,对应指针往后走一位。

如果是第一次尾插,需要注意newhead。

原先两个链表总有一个先走完;

再将剩下一个链表后面的节点相连即可。

nie

  struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {
      //其中一个表为空表,合并结果就是另外一个表(无论其是否是空表)
      if (list1 == NULL)
          return list2;
      if (list2 == NULL)
      {
          return list1;
      }
      //创建新链表的头尾指针
      struct ListNode* newhead = NULL;
      struct ListNode* newtail = NULL;
      //循环结束条件,任一链表被排布完全。
      while (list1 && list2)
      {
          if (list1->val < list2->val)//比较两个有序链表各自当前节点大小
          {
              if (newtail == NULL)//第一次尾插
              {
                  newhead = list1;
                  newtail = list1;
              }
              else//后续的尾插
              {
                  newtail->next = list1;
                  newtail = newtail->next;
              }
              list1 = list1->next;
              newtail->next = NULL;

          }
          else
          {
              if (newtail == NULL)//第一次尾插
              {
                  newhead = list2;
                  newtail = list2;
              }
              else//后续尾插
              {
                  newtail->next = list2;
                  newtail = newtail->next;
              }
              list2 = list2->next;
              newtail->next = NULL;
          }
      }
      //尾指针直接连接剩下没填完链表的节点
      if (list1 != NULL)
      {
          newtail->next = list1;
      }
      if (list2 != NULL)
      {
          newtail->next = list2;
      }
      return newhead;//返回新链表的头指针。
  }

七、链表分割(较难)

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

思路一: 先分割再合并(简单的思路)

 本题难点在于不能改变原来的数据顺序。

这里没给测试用例,我们这里假设原链表为{3 ,4,6,1,4,5,2,8},其中x为4。

那么按照题意将小于x和大于等于x的分割在两边。

3,1,2          4,6,4,5,8

注意顺序不能变。

其实上面已经给定了思路。就是将原链表分割成两个链表,再将两个链表的尾和头连接起来。

这里为了方便,先将哨兵位(头节点)的概念,因为我单链表没涉及哨兵位的东西,所以这里默认大家不知道的情况。

有时候,为了方便,我们会在第一个节点前附带一个节点,称这个节点为头节点(哨兵位)。

这里我们可以重新定义空链表了。

原先我们认为空链表不带任何节点,其实这句话是针对不带头节点的链表。

由于头节点的数据域不会存储有效的数据(有效情况可以利用这个存储比如链表的长度什么的),指针域会存储NULL(针对空表)。

那么广泛的空表可以认为没有任何有效数据的节点。

这里头指针不在指向第一个节点,而是指向头节点(哨兵位)。

头节点和第一个节点是不一样的东西(虽然文字上相同),这里把头节点称为哨兵位更合适,不易混淆。

为什么要介绍哨兵位的概念?

至少我们了解一点,再进行尾插和头插相关插入数据的操作时,如果是不带哨兵位的空链表,那么最初头指针指向NULL,这种就要分情况讨论(第一次插入和第二次插入)。而带哨兵为的单链表,至少有一个节点,像桥梁一样,方便我们进行尾插头插,而不用这么麻烦。

链表分割完了吗,还没有,必须确保每个节点的下一个指向。

由题意,我们要尾首连接这个两个链表 。

lesstail->next=greaterhead->next;

确保尾指针指向空

greatertail->next=NULL;

由于创建了两个哨兵位,我们最好销毁哨兵位。

先把头指针指向改了:

pHead=lesshead;

释放哨兵位:

free(lesshead);

free(greaterhead);

下面开始写代码吧

此题牛客网不提供C语言的方式,我们就选择C++完成此题,因为C++兼容C.

class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        struct ListNode* lesshead = (struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode* lesstail = lesshead;
        struct ListNode* greaterhead = (struct ListNode*)malloc(sizeof(struct ListNode));
        struct ListNode* greatertail = greaterhead;

        struct ListNode* pcur = pHead;
        //链表分割
        while (pcur)
        {
            if (pcur->val < x)
            {
                lesstail->next = pcur;
                lesstail = lesstail->next;
            }
            else
            {
                greatertail->next = pcur;
                greatertail = greatertail->next;
            }
            pcur = pcur->next;
        }
        //两个链表的连接和尾部置空
        lesstail->next = greaterhead->next;
        greatertail->next = NULL;
        pHead = lesshead->next;
        free(lesshead);
        free(greaterhead);
        return pHead;
    }
};

八、链表的回文结构(简单)

234. 回文链表 - 力扣(LeetCode)

先分享我一开始的愚蠢想法。

哎,这不简单嘛?搞两个对撞指针不就行了吗。

一个从头,另一个从尾部开始。

然后我才意识道单链表的单向性。不过并没无功,开始尝试就已经迈向了成功。

思路一:中间节点+反转链表+双指针法

正如小标题所说,我们要借助先前的题的函数了

一、反转链表

struct ListNode* reverseList(struct ListNode* head) {
    if (head == NULL)
        return NULL;
    struct ListNode* prev = head;//prev指向head指向的节点,即第一个节点。
    struct ListNode* next = head;//next也指向第一个节点。

    next = prev->next;//先让next指向下一个节点。
    prev->next = NULL;//将结点指针指向的节点的指针域置空,意味着把节点取下来了。

    while (next)
    {
        prev = next;
        next = prev->next;
        prev->next = NULL;

        prev->next = head;
        head = prev;
    }
    return head;


}

三、链表的中间节点

struct ListNode* middleNode(struct ListNode* head)
{
    if (head == NULL)//空链表
        return head;
    int len = 0;//长度变量
    struct ListNode* pcur = head;
    while (pcur)//遍历求链表长度
    {
        pcur = pcur->next;
        len++;
    }
    pcur = head;//重置pcur
    int k = len / 2;//中间结点到起点的长度。
    while (k--)
    {
        pcur = pcur->next;
    }
    return pcur;//返回中间节点的指针
}

需要注意反转链表的函数再反转链表后,尾指针会指向空。 如下图所示

找中间节点的函数只会返回中间节点的指针。

 这里可用迭代过程中,可用rightpcur==NULL终止这一过程。

struct ListNode* middleNode(struct ListNode* head)
{
    if (head == NULL)//空链表
        return head;
    int len = 0;//长度变量
    struct ListNode* pcur = head;
    while (pcur)//遍历求链表长度
    {
        pcur = pcur->next;
        len++;
    }
    pcur = head;//重置pcur
    int k = len / 2;//中间结点到起点的长度。
    while (k--)
    {
        pcur = pcur->next;
    }
    return pcur;//返回中间节点的指针
}


struct ListNode* reverseList(struct ListNode* head) {
    if (head == NULL)
        return NULL;
    struct ListNode* prev = head;//prev指向head指向的节点,即第一个节点。
    struct ListNode* next = head;//next也指向第一个节点。

    next = prev->next;//先让next指向下一个节点。
    prev->next = NULL;//将结点指针指向的节点的指针域置空,意味着把节点取下来了。

    while (next)
    {
        prev = next;
        next = prev->next;
        prev->next = NULL;

        prev->next = head;
        head = prev;
    }
    return head;


}
bool isPalindrome(struct ListNode* head) {
    struct ListNode* midhead = middleNode(head);
    struct ListNode* rightpcur = reverseList(midhead);

    struct ListNode* leftpcur = head;
    while (rightpcur)
    {
        if (rightpcur->val != leftpcur->val)
        {
            return false;
        }
        else
        {
            rightpcur = rightpcur->next;
            leftpcur = leftpcur->next;
        }
    }
    return true;
}

思路二:数组(换种数据结构)+双指针(对撞指针)

此题本质是判断会不会回文的问题。既然单链表数据结构不好判断。

我们就把单链表数据,拷贝一份放到数组上。

类似动态顺序表的写法,创建动态数组。

bool isPalindrome(struct ListNode* head) {
    int* arr = (int*)malloc(sizeof(int) * 4);//函数内部手动创建一个动态顺序表
    int size = 0;//当前有效数据
    int capacity = 4;//数据容量大小
    struct ListNode* pcur = head;
    while (pcur)//将单链表的数据拷贝到动态内存。
    {
        if (size == capacity)//看情况扩容
        {
            int newcapacity =2 * capacity;
            arr = (int*)realloc(arr, newcapacity*sizeof(int));
            capacity = newcapacity;
        }
        arr[size++] = pcur->val;
        pcur = pcur->next;
    }
    //动态数组下标当作指针使用
    int left = 0;//有效数据的左下标
    int right =size - 1 ;//有效数据的右下标
    while (left <= right)
    {
        if (arr[left] != arr[right])
            return false;//有一个对应位数据不等就不是回文。
        left++;
        right--;
    }
    return true;//都符合,是回文结构。
}

动态还要考虑扩容和节省空间,干脆不要空间了,节约时间,用静态顺序表吧。

bool isPalindrome(struct ListNode* head) {
    int arr[100000];//一次性开辟大的空间,刚好符合题目最大的数据个数
    int size = 0;//当前有效数据
    struct ListNode* pcur = head;
    while (pcur)//将单链表的数据拷贝到定长数组上
    {
        arr[size++] = pcur->val;
        pcur = pcur->next;
    }
    //定长数组下标当作指针使用
    int left = 0;//有效数据的左下标
    int right =size - 1 ;//有效数据的右下标
    while (left <= right)
    {
        if (arr[left] != arr[right])
            return false;//有一个对应位数据不等就不是回文。
        left++;
        right--;
    }
    return true;//都符合,是回文结构。
}

 

九、相交链表(简单)

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

解决这道题分两步:

1.判断链表是否相交。

2.若相交返回交点节点的指针,否则返回NULL。

那怎么判断链表相不相交呢?

图中所给示例即为链表相交的情况。

链表不相交如图。

链表不相交和链表相交有什么能够显著的区别?

 你说相交有交点,不相交没有交点呗。

有道理,但我们先判断是否相交,再确认交点是谁。

很明显,如果我们同时遍历两个链表,相交链表的尾结点是一样的,不相交的链表的尾结点不一样。

总结一下:判断相交即判断尾结点相等。

下面提供两种思路

思路一:暴力遍历。

struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) {
    if (headA == NULL && headB == NULL)
    {
        return NULL;
    }
    struct ListNode* pcur1 = headA;
    struct ListNode* pcur2 = headB;
    while (pcur1->next)
    {
        pcur1 = pcur1->next;
    }
    while (pcur2->next)
    {
        pcur2 = pcur2->next;
    }
    if (pcur2 != pcur1)//判断两个单链表尾结点是否相等。
        return NULL;
    pcur1 = headA;
    pcur2 = headB;

    //暴力查找
    while (pcur1)//嵌套while循环,A链表和B链表的所有节点一一比较。
    {
        pcur2 = headB;
        while (pcur2)
        {
            if (pcur2 == pcur1)
            {
                return pcur2;
            }
            pcur2 = pcur2->next;
        }
        pcur1 = pcur1->next;
    }
    //置空
    pcur1 = NULL;
    pcur2 = NULL;
    //找不到就返回空。
    return NULL;
}

其实这么些不用判断相不相交,我都暴力查找了。

时间复杂度o(n*n) 或o(n*m),删掉前面两个while循环也可,但是杯水车薪,稍微节约点时间。

思路二:  (类似)快慢指针

还是这张图

我们其实可以一眼看到这叫交点就是C1,要是这两个链表一样长就好了,这样同时走,第一次节点地址相同就确定交点了,可惜这不是一样长的链表。

先不要叹气,思路不就有了吗?

我们模拟成等长链表不就行了吗。

链表有长有短,定义两个指针指向各自的头节点,让长的链表指针先走,走到等长的位置,然后两个指针同时走就行了啊。这种是不是和四、五题的快慢指针有异曲同工之妙。

整理一下思路

1.判断是否相交。

2.计算链表的长度差。

3.判断哪个是长链表。

4.定义双指针,长链表先走,然后同时走,第一次相遇即交点。

开始写代码。

struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) {
    //判断空链表,任一一个为空必然不相交,虽然题目条件不包括空链表的情况。
    if (headA == NULL && headB == NULL)
        return NULL;
    struct ListNode* pcur1 = headA, * pcur2 = headB;
    int lenA = 1, lenB = 1;//记录长度变量

    //找尾结点,并计算长度
    while (pcur1->next)
    {
        pcur1 = pcur1->next;
        lenA++;
    }
    while (pcur2->next)
    {
        pcur2 = pcur2->next;
        lenB++;
    }
    if (pcur1 != pcur2)
    {
        return NULL;
    }
    int gap = abs(lenA - lenB);//abs为整型的绝对值函数,计算长度差距
    struct ListNode* fastlist = headA;//快指针
    struct ListNode* slowlist = headB;//慢指针
    //假设不成立
    if (lenA < lenB)
    {
        fastlist = headB;
        slowlist = headA;
    }
    //快指针先走gap步。
    while (gap--)
    {
        fastlist = fastlist->next;
    }
    //同时走
    while (fastlist)
    {
        fastlist = fastlist->next;
        slowlist = slowlist->next;
        if (fastlist == slowlist)
        {
            return fastlist;
        }
    }
    //为了防止不返回值,假装在这里返回空指针。
    //上面逻辑上肯定能返回值,非要搁这儿报错,只能在最后补上了。
    return NULL;
}

这道题搞定了。 

时间复杂度o(n).

十、环形链表(简单or难?)

141. 环形链表 - 力扣(LeetCode)

 

先说一下循环链表的概念, 前面的单链表称作单向不循环链表,不循环是什么意思呢?

单链表的尾结点指向NULL,即不循环。那么循环链表的概念就可以说,尾结点的下一个指针指向头节点的链表称为循环链表,如上图的示例一

而这里的示例一不难发现,尾结点的指针部分指向的是第二个节点,这就是一般的环形链表了。

下面我们关注此题如何解决?

很容易发现我们过去的迭代思路用不了了,因为对于在环形链表中循环,像之前那样写(如下代码),陷入死循环了。

while (pcur)
{
	pcur = pcur->next;
}

那具体怎么解决呢?判断走过一个节点两次?

好像不现实!再想想环形结构有什么特点。

环形结构是个圈吧。怎样判断自己的运动轨迹是圈?

在操场跑步的两个人,如果速度不一样,两个人一直跑,早晚会出现套圈的情况。而出现套圈这一情景,不就说明了操场的结构是环状结构吗。

对比在程序中,速度不一的两个人,不就是快慢指针吗?

快指针一次走两步,慢指针一次走一步。快指针先进环,慢指针后进环。两者出现相遇(套圈)就证明这是环状结构。

下面提供本题作者的唯一思路。其它方法自行搜leetcode题解

思路一:快慢指针法

bool hasCycle(struct ListNode* head) {
	if (head == NULL)//空链表取消比赛资格
		return false;
	struct ListNode* fast = head, * slow = head;//定义快慢指针
	while (fast)//若循环能结束,就说明非环状链表。
	{
		fast = fast->next->next;//快走二
		slow = slow->next;//慢走一
		if (fast == slow)//若是环状结构,快慢指针必向重逢。
		{
			return true;
		}
	}
	//非循环链表的情况。
	return false;
}

怎么又出错了,估计又出现空指针解引用的问题。

bool hasCycle(struct ListNode* head) {
	struct ListNode* fast = head, * slow = head;//定义快慢指针
	if (head == NULL)//空链表
		return false;
	
	while (fast&&fast->next)//若循环能结束,就说明非环状链表。
	{
		fast = fast->next->next;//快走二
		slow = slow->next;//慢走一
		if (fast == slow)//若是环状结构,快慢指针必向重逢。
		{
			return true;
		}
	}
	//非循环链表的情况。
	return false;
}

 

 结尾

鉴于笔者写到这里神志不清了,影响创作质量,于是分期写完单链表的练习。

给您总结一下相关方法:

1.快慢指针法(双指针)

2.双指针和三指针。

3.弗洛伊德判环法(环形链表)//感兴趣可以自行了解。

转化思想,将复杂问题转化成曾经解决的问题或者易上手的问题。

下期更有极高价值的题,比如约瑟夫,环形链表II等,敬请期待。

好了作者要休息,感谢您的观看。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1642204.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入理解 Java 并发:AbstractQueuedSynchronizer 源码分析

序言 在多线程编程中&#xff0c;同步机制是保障线程安全和协调线程之间操作顺序的重要手段。AQS 作为 Java 中同步机制的基础框架&#xff0c;为开发者提供了一个灵活且高效的同步工具。本文将通过对 AQS 源码的分析&#xff0c;解读 AQS 的核心实现原理&#xff0c;并深入探…

同步互斥问题模型

目录 一. 生产者-消费者问题二. 多生产者-多消费者问题三. 吸烟者问题四. 读者-写者问题五. 哲学家进餐问题 \quad 一. 生产者-消费者问题 \quad 问题分析 \quad 如何实现 \quad \quad V操作不会导致进程阻塞&#xff0c;因此两个v操作顺序可以交换。 \quad 二. 多生产者-多消费…

前端 -- 基础 表单标签 - 表单元素 < Select > < textarea >

在表单域中可以定义各种表单元素&#xff0c;这些表单元素就是允许用户在表单中输入或者选择的内容控件。 在之前&#xff0c;我们就讲过 表单元素 分了 三类来讲 &#xff1a; 1 input 输入表单元素 2 select 下拉表单元素 3 textarea 文本域元素 input 表单元素 我们…

AGI要闻:斯坦福李飞飞首次创业,瞄准“空间智能”;OpenAI下周发布搜索产品挑战谷歌;新的开源 AI 眼镜来了|钛媒体AGI | 最新快讯

多方消息证实&#xff0c;OpenAI将会在北京时间5月10日&#xff08;周五&#xff09;凌晨2点公布搜索引擎新产品消息。 斯坦福大学首位红杉讲席教授 李飞飞 通用人工智能&#xff08;AGI&#xff09;领域又公布了一系列重磅消息。 5月4日凌晨&#xff0c;据路透社&#xff0c…

宝塔怎么配置nginx

宝塔怎么配置nginx 1.找到nginx配置位置 2.修改nginx.conf文件 3.重启nginx

在视频中使用时间卷积和半监督训练进行三维人体姿态估计

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 摘要Abstract文献阅读&#xff1a;在视频中使用时间卷积和半监督训练进行三维人体姿态估计1、文献摘要2、提出方法2.1、时间扩张卷积模型2.2、半监督方法2.3、与传统…

Mac OS系统如何更新

用了好几年的Mac Book安装软件经常提示需要更高的系统版本&#xff0c;因此要升级系统版本&#xff0c;但是开始在系统设置里面找了一下没有找到升级的按钮&#xff0c;找了资料后才知道如何升级。有以下两种入口 一、App Store搜索MacOs&#xff0c;在出现的搜索结果中选择下载…

力扣例题(接雨水)

链接&#xff1a; . - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 思路&#xff1a; 判断一块地方是否可以接到雨水&#xff0c;只需要判断他是否有左右边界使他可以接到水 左右边界分别为此处左侧的最高点和右侧的最高点 同时此处可接雨水的高度为左右两…

离散数学之命题逻辑思维导图+大纲笔记(预习、期末复习,考研,)

大纲笔记&#xff1a; 命题逻辑的基本概念 命题与联结词 命题 命题是推理的基本单位 真命题&#xff0c;假命题 特征 陈述句 唯一的真值 是非真即假的陈述句 非命题 疑问句 祈使句 可真可假 悖论 模糊性 三个基本概念 复合命题 真值取决于原子命题的值和逻辑联结词 原子命题 逻…

微图乐 多种装B截图一键制作工具(仅供娱乐交流)

软件介绍 采用exe进程交互通信。全新UI界面&#xff0c;让界面更加清爽简约。支持zfb、VX、TX、Yin行、Dai款、游戏等图片生成&#xff0c;一键超清原图复制到剪辑板&#xff0c;分享给好友。适用于提高商家信誉度&#xff0c;产品销售额度。装逼娱乐&#xff0c;用微图乐。图…

14_Scala面向对象编程_属性

属性 1.类中属性声明 // 1.给Scala声明属性&#xff1b;var name :String "zhangsan"val age :Int 302.系统默认赋值 scala由于初始化变量必须赋值&#xff0c;为了解决此问题可以采用下划线赋值&#xff0c;表示系统默认赋值 , –但是此方法局限于变量&…

Vue ui 创建vue项目,详细使用攻略。

1.安装及启动 1.1 Vue ui 使用前提是全局安装vue.js 命令如下 npm install vue -g 1.2 安装过Vue.js 之后 随便在自己系统的一个地方打开命令面板 1.3 使用命令启动vue ui面板创建项目 vue ui 如图运行后显示这种就是启动成功&#xff0c;成功之后会弹出页面或者直接访问你的…

【业务场景】京东实际场景,频繁GC引起的CPU飙高问题的解决

目录 1.业务介绍 2.判断任务类型 3.CPU飙高的原因 1.业务介绍 本文的业务场景是京东零售线公开的一篇文章&#xff0c;文章内容详细介绍了京东零售线如何将广告相关的定时任务从半小时优化到秒级的&#xff0c;原文链接&#xff1a; 半小时到秒级&#xff0c;京东零售定时…

Pandas标签库

目录 1.创建对象 1.一维对象 1.字典创建法 2.数组创建法 2.二维对象 1.字典创建法 2.数组创建法 2.对象的索引 1.一维对象的索引 1.查询 2.切片 2.二维对象的索引 1.访问 2.修改 3.对象的变形 1.对象的转置 2.上下翻转和左右翻转 3.对象的重塑 4.一维对象的合…

使用Jellyfin创建媒体库

目录 前言 1.介绍Jellyfin 2.安装 3.设置时注意点 4.效果 5.内存占用 前言 分为客户端和服务器端&#xff0c;这里讲的是服务器端的安装 1.介绍Jellyfin Jellyfin 是一个免费开源的媒体服务器软件&#xff0c;它允许您管理和播放您的媒体文件。这些媒体文件可以包括电…

Vue3 + Vite + TypeScript + Element-Plus创建管理系统项目

官方文档 Vue3官网 Vite官方中文文档 创建项目 使用npm命令创建项目&#xff1a; npm create vitelatest输入项目名称&#xff1a; ? Project name:项目名选择vue&#xff1a; ? Select a framework: - Use arrow-keys. Return to submit.Vanilla > VueReactPrea…

AST原理(反混淆)

一、AST原理 jscode var a "\u0068\u0065\u006c\u006c\u006f\u002c\u0041\u0053\u0054";在上述代码中&#xff0c;a 是一个变量&#xff0c;它被赋值为一个由 Unicode 转义序列组成的字符串。Unicode 转义序列在 JavaScript 中以 \u 开头&#xff0c;后跟四个十六进…

【Linux】 OpenSSH_7.4p1 升级到 OpenSSH_9.6p1(亲测无问题,建议收藏)

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;CSDN博客专家   &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01…

【linuxC语言】vfork、wait与waitpid函数

文章目录 前言一、函数使用1.1 vfork1.2 wait1.3 waitpid 二、示例代码总结 前言 在Linux系统编程中&#xff0c;vfork()、wait() 和 waitpid() 函数是处理进程管理和控制流的重要工具。这些函数允许我们创建新进程、等待子进程结束并获取其退出状态&#xff0c;从而实现进程间…

【CAP探索者指南】掌握分布式世界的三角平衡术,一致性、可用性、分区容错性大揭秘!

关注微信公众号 “程序员小胖” 每日技术干货&#xff0c;第一时间送达&#xff01; 引言 在现代的微服务架构中&#xff0c;系统被拆分成了许多小型服务&#xff0c;每个服务可能有自己的数据库。这种架构带来了灵活性和可扩展性&#xff0c;但也引入了新的挑战&#xff0c;…