代码随想录算法训练营第四天 | 24. 两两交换链表中的节点、19.删除链表的倒数第N个节点、面试题 02.07. 链表相交、142.环形链表II

本文介绍了链表中的双指针、递归和环检测技巧,包括两两交换链表节点、删除链表倒数第N个节点以及环形链表的判断与起始点查找。通过实例和代码展示了如何使用这些方法解决相关问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

day_04

传送门


1. 昨日回顾

  • 双指针法适用场景
    • 两个指针一前一后不相邻节点间的操作
    • 需要一个先行者确定位置,后发者处理情况
    • 需要一快一慢遍历时

  • 单指针适用场景
    • 一前一后相邻节点的操作
    • 一个指针遍历时就能处理的问题

  • 递归适用场景
    • 需要深入到链表尾部,再返回操作的类型
    • 从 NULL 开始,处理到 NULL 结束的类型

  • 好消息
    • 基本递归能处理的,都能用双指针循环的方式处理

2. 题解

24 - 两两交换链表中的节点

  • 思路及代码
    • C 语言

      • 循环
      struct ListNode* swapPairs(struct ListNode* head){
          typedef struct ListNode ListNode;
          
          ListNode* dummyHead = malloc(sizeof(ListNode));     //  虚拟头节点
          dummyHead -> next = head;                           //  头节点添加到链表里
          ListNode* cur = dummyHead;                          //  每组待交换节点的前一个节点
      
          ListNode* ptrPre;                                   //  每组交换未完成时的前一个节点
          ListNode* ptr;                                      //  ptrPre要指向的 next 节点
      
          while(cur -> next != NULL && cur -> next -> next != NULL){   //  条件终止情况
              ptrPre = cur -> next;                           //  存前面的节点信息
              cur -> next = cur -> next -> next;              //  
              ptr = cur -> next -> next;                      //  存储后面的节点地址
              cur -> next -> next = ptrPre;                   //  修改组内指针位置
              ptrPre -> next = ptr;                           //  
              cur = ptrPre;                                   //  移动 cur 到下一组的前一个指针
          }
      
          return dummyHead -> next;
      }
      

      指针介绍

      • 递归 (后期另寻时间实现)

19 - 删除链表的倒数第 N 个结点

  • 思路及代码

    • 双重循环暴力法( O(n^2) 不推荐哦 )
    • 快慢指针处理
    • 递归方式回退处理
  • 快慢指针处理

    • 过程如下
      1.  定义虚拟节点
      2. 定义一个快指针,先遍历 n+1 个节点(单链表处理的时候,需要拿到待处理节点的前一个节点)
      3. 慢指针在快指针到第 n+1 个后,两个指针开始同步移动
      4. 直到快指针 = NULL,就对满指针指向的节点进行处理
    
    • 为什么先遍历 n+1 个节点 ? 而不是 n ?
      图1
      图二
      图三

    • C语言实现代码如下

    struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
      typedef struct ListNode ListNode;
      
      ListNode* dummyHead = malloc(sizeof(ListNode));     //  虚拟头节点
      dummyHead -> next = head;                           //  头节点添加到链表里
    
      ListNode* fast = dummyHead;              //  定义快慢指针
      ListNode* slow = dummyHead;
    
      int i = 0;
      while(i < n+1 && fast != NULL){     //  移动快指针,快指针要移动到待删除的后一个才行
          fast = fast -> next;
          i++;
      }
    
      while(fast != NULL){
          fast = fast -> next;
          slow = slow -> next;
      }
    
      slow -> next = slow -> next -> next;
    
      return dummyHead -> next;
    }
    

142 - 环形链表 Ⅱ

法一 : hash表处理思路
  • 判断有无环 + 找环起始点的思路
    • 判断环是否存在 - hash表处理的过程
      1. 添加虚拟头节点,添加到链表里
    
      2. 遍历链表,并维护一个 hash表,表里的 k,v 存储ListNode节点,
          这是因为 : 节点里的 val 有可能相同,但是内存地址是不同的,节点是否相同看的是内存地址,而不是数值
          判断当前遍历到的节点是否已经存在于 hash表里
              T : 说明有环  
              F : 添加该节点到 hash 表里
          
      3. 如果遍历到 null,说明没环
    
    • 有环的时候,如何寻找环的起始点呢
      通过 hash 表可以获取环起点,是第一个已在表里的 ListNode
    
      新建指针,指向头节点,开始遍历,找到 ListNode == 环起点的ListNode
    
      ListNode 就是环的起始节点啦,此时直接返回就好
    
    • java 代码实现 - 其实可以直接用 set 集合的。。。
     public class Solution {
         public static ListNode detectCycle(ListNode head) {
             HashMap<ListNode, ListNode> hashMap = new HashMap<>();          //  创建 hash表来存储数据处理
     
             ListNode dummyHead = new ListNode();
             dummyHead.next = head;
             ListNode cur = dummyHead.next;              //  用来遍历的指针
     
             while (cur != null){
                 if (hashMap.containsKey(cur)){      //  如果这个节点已经存在,说明有环
                     return hashMap.get(cur);
                 }
     
                 hashMap.put(cur, cur);
                 cur = cur.next;
             }   //  while
     
             return null;
         }
     }
    

法二 : 双指针处理思路
  • 判断有无环 + 找环起始点的思路
    • 判断有无环 - 双指针处理的过程
          1. 添加虚拟头节点,添加到链表里
          2. 定义两个指针,一快一慢,fast 和 slow
          3. 确定 fast、slow 每次行走的步数
          4. fast、slow同时遍历,如果fast = null 了,说明没环(fast先遍历完链表)
          5. 如果遍历的途中发现 fast = slow,说明有环,且是第一次再
          
          - (不会出现fast跳过slow的情况吗?)
              不会! 这是因为 slow 进入环后,假设 slow 的位置是终点
              fast 在环里是落后于 slow 的
              且每次追赶 slow 的步数为 1,不会出现错过的情况
          
          - (为什么 fast、slow 第一次相遇的时候,slow 一定还没绕完第一圈环呢)
            两指针的速度差为一步,但两指针的距离差小于等于一个环的距离
            等于的情况 - slow 还没绕环就结束了
            小于的情况 - fast 以每次一步的速度追赶 dis / 1 = dis < 环长度
            所以才有 slow 一定还没绕完第一圈环的结论
      
    • 有环的时候,如何寻找环的起始点呢
        关于这个,推荐看卡哥的文章,文章地址已经贴在上面了
      
        这里关注两点
          1. fast 进入环后,可能绕环绕了不止一圈,所以是 n(y+z)
          2. 对等式(n-1)(z+y)+z+y-y = x的理解
              (n-1)(z+y)是指 fast 指针绕了 n-1 圈环
              z 是 fast、slow 第一次相遇时,fast 距离环起点的位置
              这个等式翻译一下就是
              fast 从 y 位置走 n-1 圈环,后再走z步就能到 x(环起点位置)
          现在问题转化为如何求得这些步数,上面的式子中,左右的整体量都是不知道的
          好消息是现在不是纯数学维度获取数值,就像物理学家可以使用除了纯数学以外的物理属性一样
          虽然都是未知量,但我们可以通过模拟来得到左右两边的整体值
          左边是fast指针的模拟,右边是从头开始的新指针,当左右两式相等,就可以找到环的起点了
          左右两边走指定的相同步数就能相遇,相同步数是奇?是偶? 无法判断
          那就让两边每次都走一步,直到相遇
      
    • C语言代码实现如下
    struct ListNode *detectCycle(struct ListNode *head) {
    
      typedef struct ListNode ListNode;
      ListNode* dummyHead = malloc(sizeof(ListNode));
      dummyHead -> next = head;
    
      ListNode* fast = dummyHead;
      ListNode* slow = dummyHead;
    
      //  如果快指针走到了 null,说明没有环,fast向前走两步,和两两交换的条件一样
      while(fast -> next != NULL && fast -> next ->next!=NULL){
          fast = fast -> next ->next;     //  fast 移动两步
          slow = slow -> next;            //  slow 移动一步
    
          if(fast == slow){   //  指针的好处,可以之间判断地址值是否一样,java要用equal才行
              //  进入环了,也就是说,到了 y 的位置
              ListNode* travel = dummyHead;
              while(fast != travel){
                  fast = fast -> next;
                  travel = travel -> next;
              }
              return travel;
          }
      }
    
      return NULL;
    }
    

教训 - 数值相同,不代表指针相同!


补充题目 - 链表香蕉

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值