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

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

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

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

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

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

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

数据结构篇其二---单链表(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等,敬请期待。

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

联发科技发布天玑9300+旗舰5G生成式AI芯片 | 最新快讯

5 月 7 日消息&#xff0c;联发科技今天举办了天玑开发者大会 2024。大会上&#xff0c;联发科技开启了“天玑 AI 先锋计划”&#xff0c;联合业界生态企业发布了《生成式 AI 手机产业白皮书》&#xff0c;分享了生成式 AI 端侧部署的解决方案“天玑 AI 开发套件”。同时&#…

界面组件DevExpress Reporting中文教程 - 如何按条件显示页面水印?

DevExpress Reporting是.NET Framework下功能完善的报表平台&#xff0c;它附带了易于使用的Visual Studio报表设计器和丰富的报表控件集&#xff0c;包括数据透视表、图表&#xff0c;因此您可以构建无与伦比、信息清晰的报表。 从防止未经授权的使用到建立所有权和真实性&am…

java语言数据结构(单链表)

前言 不得承认java应用的广泛&#xff0c;所以毅然决定java版本的数据结构和算法专题还是要坚决更新。每日更新2题&#xff0c;希望学习的小伙伴可以关注一波跟上&#xff0c;评论区欢迎讨论交流。 实现原理 节点&#xff08;Node&#xff09;&#xff1a;链表的基本构建单元…

【Git】Git学习-07:git diff查看差异

学习视频链接&#xff1a;【GeekHour】一小时Git教程_哔哩哔哩_bilibili 默认比较工作区和暂存区的差异 工作区和版本库的内容比较 git diff HEAD / git diff --staged 暂存区和版本库内容比较 git diff --cached 比较特定两个版本的不同 git diff 版本id 版本id HEAD表示提交的…

【Git】Git学习-17:git rebase,且解决合并冲突

学习视频链接&#xff1a;【GeekHour】一小时Git教程_哔哩哔哩_bilibili​编辑https://www.bilibili.com/video/BV1HM411377j/?vd_source95dda35ac10d1ae6785cc7006f365780 理论 git rebase 目标分支&#xff1a;把当前分支的提交&#xff0c;从与目标分支的共同主祖先处断开…

Ubuntu 24.04 LTS 安装 touchegg 开启触控板多指手势

文章目录 〇、概述一、安装 touchegg二、安装 gnome-shell 扩展 X11 Gestures三、安装可视化配置工具 touche 〇、概述 之前为了让笔记本支持多指手势&#xff0c;我安装的是 fusuma&#xff0c;安装教程详见 这篇文章 &#xff0c;考虑到 fusuma 安装过程繁琐且不支持可视化配…

Redis单机安装

1.编译 cd redis安装目录 makemake install2.修改配置文件redis.conf #端口修改 port 6379 #后台进程启动 yes daemonize yes # daemonize no #注释掉 为了可以远程连接 #bind 127.0.0.1 #设置密码 requirepass pwd3.启动 ./redis-server ../redis.conf查看进程 [rootlocal…

地盘紧固的关键技术——SunTorque智能扭矩系统

底盘紧固件是汽车底盘系统中不可或缺的一部分&#xff0c;它们负责连接和固定各个部件&#xff0c;确保车辆行驶的安全和稳定。底盘紧固件的开发涉及到多个环节和关键技术&#xff0c;下面SunTorque智能扭矩系统将详细介绍底盘紧固件开发流程和关键技术。 一、底盘紧固件开发的…

舞台演出因全息纱幕投影有哪些新变化?

如今&#xff0c;随着时代的演进&#xff0c;我们对舞台体验的追求愈发深入&#xff0c;渴望在这片艺术天地中&#xff0c;感受到超越日常的震撼与感动&#xff0c;而这种追求&#xff0c;也正与现代技术的飞速发展不谋而合。其中&#xff0c;全息纱幕投影技术以其独特的魅力&a…

Mysql 基础 order by ,as ,limit,case,asc、desc

order by 、as 、limit 、asc&#xff08;ascending 正序 默认&#xff09; desc&#xff08;descending 倒序&#xff09; SELECT 学号, 姓名, 成绩, CASEWHEN 成绩 > 90 THEN 优秀WHEN 成绩 > 80 THEN 良好WHEN 成绩 > 60 THEN 及格ELSE 不及格 END as 成绩级别 FRO…

数组刷题集

文章目录 概要26. 删除有序数组中的重复项代码Python 189. 轮转数组代码Python 小结 概要 记录一些数组相关的刷题集。 数组数据结构早就写过了&#xff0c;一直没有写相关的刷题集。今天补上。 26. 删除有序数组中的重复项 leetcode26题 题目如上图&#xff1b;也可以去leet…

Vince9120雅思小作文笔记——P1 Intro(前言)

文章目录 链接P1 Intro&#xff08;前言&#xff09;字数限制题型综述&#xff08;problem types overview&#xff09;1. **柱状图&#xff08;Bar Chart&#xff09;** - 描述不同类别在某个或多个变量上的数据量比较。2. **线图&#xff08;Line Graph&#xff09;** - 展示…

【只显示某一层,其它层隐藏】- PCB板工艺及AD软件配置

在pcb文件视图下&#xff0c;按下字母L&#xff0c;弹出如下框&#xff0c; 选择view options 在single layer mode 勾选ON&#xff0c;就可以隐藏其他层 单层显示效果 方法二&#xff1a;shitfs快捷键

SH-PEG-SH,聚乙二醇二巯基广泛用于生物学应用、纳米技术和材料研究中

【试剂详情】 英文名称 SH-PEG-SH 中文名称 聚乙二醇二巯基&#xff0c;双硫醇PEG&#xff0c; 双巯基聚乙二醇&#xff0c;双巯基封端聚乙二醇 外观性状 白色固体粉末 分子量 0.4k&#xff0c;0.6k&#xff0c;1k&#xff0c;2k&#xff0c;3.4k&#xff0c;5k&#x…

前端开发攻略---手写bind函数,看这篇就够了。

1、演示 2、bind函数介绍 在JavaScript中&#xff0c;bind() 函数用于创建一个新函数&#xff0c;其 this 关键字设置为提供的值&#xff0c;而不是调用时绑定的值。这对于在事件处理程序中绑定函数的上下文或者在类中绑定方法的上下文非常有用。 bind() 方法的语法如下&#x…

LeetCode //C - 85. Maximal Rectangle

85. Maximal Rectangle Given a rows x cols binary matrix filled with 0’s and 1’s, find the largest rectangle containing only 1’s and return its area. Example 1: Input: matrix [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“…

怎样通过PHP语言实现远程控制一路开关

怎样通过PHP语言实现远程控制一路开关呢&#xff1f; 本文描述了使用PHP语言调用HTTP接口&#xff0c;实现控制一路开关&#xff0c;一路开关可控制一路照明、排风扇等电器。 可选用产品&#xff1a;可根据实际场景需求&#xff0c;选择对应的规格 序号设备名称厂商1智能WiFi…

全面升级!一套基于Spring Boot 3+JDK17的实战项目!

最近把mall项目升级支持了Spring Boot 3JDK17&#xff0c;今天就来介绍下mall项目做了哪些升级&#xff0c;包括依赖的升级、框架的用法升级以及运行部署的改动&#xff0c;目前Spring Boot 3版本代码在mall项目的dev-v3分支下&#xff0c;希望对大家有所帮助&#xff01; mall…

压力测试-JMeter常用插件、服务器硬件监控

1.写在前面 在前一篇文章中&#xff0c;我们已经对jmeter有了一个入门的学习。 掌握了JMeter安装、入门、结果分析等内容&#xff0c;详情可查看这里&#xff1a;压力测试-JMeter安装、入门、结果分析 对于jmeter默认的插件&#xff0c;往往不太够&#xff0c;例如&#xff…

Python检查代码质量库之flake8使用详解

概要 Flake8是一个流行的Python库,用于检查代码质量和风格一致性,它集成了PyFlakes、pep8、Ned Batchelder的McCabe script等工具。Flake8可以帮助开发者发现代码中的错误,保持代码风格的一致性,是每个Python开发者工具箱中的重要组成部分。 安装 安装Flake8非常简单,可…