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

目录

24.两两交换链表的节点

方法一     正常画图

方法二   递归

19.删除链表的倒数第N个节点

方法一:暴力解法

 方法二:快慢指针 1.1

 方法三  快慢指针1.0

 160.链表相交

方法一   先统计两个链表的长度

方法二,双指针解决

 142.环形链表II

判断链表是否有环

如果有环,如何找到这个环的入口


24.两两交换链表的节点

【文章讲解】代码随想录 (programmercarl.com)

【24. 两两交换链表中的节点|leetcode题目】24. 两两交换链表中的节点

【Carl视频讲解】帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点_哔哩哔哩_bilibili

24. 两两交换链表中的节点   题目

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

思路

ac51fb66a8c74e978cbe57a0daa42a3e.png

方法一     正常画图

e94746faf2fa49a69cc11d7f20872ece.png

3e33eb3830704e5b9553d10ecac717ab.png

af4e52d5a8f04d568711d70c7b405eb8.png

9f66caa571d54c38907ed29b4a2554cb.png

代码 

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode virtualNode = new ListNode(-1);
        virtualNode.next = head;
        ListNode cur = virtualNode; //操作第一个节点的 前一个节点
        ListNode temp ;      //第二个节点
        ListNode fistNode;   //第一个节点
        ListNode thirdNode;  //第三个节点
        while(cur.next != null && cur.next.next != null){//不能head为空、不能一组节点中只有一个
            //画图
            temp = cur.next.next;                //保存第二个节点
            fistNode = cur.next;                 //保存第一个节点
            thirdNode = cur.next.next.next;      //保存第三个节点
            cur.next = temp;  //virtualNode 连 2号节点
            temp.next = fistNode;  //2号节点 连 1号节点
            fistNode.next = thirdNode;  //1号节点 连 3号节点
            cur = cur.next.next;  //cur 后移两位  cur只在0,2,4..
        }
        return virtualNode.next;//head已变
    }
}

a46bf5361c6a4123abc154bcc7048038.png

方法二   递归

【递归详解.24.两两交换链表中的节点】LeetCode每日打卡.24.两两交换链表中的节点_哔哩哔哩_bilibili

使用递归来解决该题,主要就是递归的三部曲:

  1. 找终止条件:当递归到链表为空或者链表只剩一个元素的时候,没得交换了,自然就终止了。
  2. 找返回值:返回给上一层递归的值应该是已经交换完成后的子链表。
  3. 单次的过程:因为递归是重复做一样的事情,所以从宏观上考虑,只用考虑某一步是怎么完成的。我们假设待交换的俩节点分别为head和next,next的应该接受上一级返回的子链表(参考第2步)。就相当于是一个含三个节点的链表交换前两个节点,就很简单了,想不明白的画画图就ok

ad9b9c598ab3454ca555e125f332a022.png

d20935272c0f40f196a375d494b7635d.png

代码 

class Solution {
    public ListNode swapPairs(ListNode head) {
   //递归
    if(head == null || head.next == null){ //找终止条件
        return head;
    }
   //单次的过程:因为递归是重复做一样的事情,所以从宏观上考虑,只用考虑某一步是怎么完成的单次的过程
    ListNode next = head.next;//2号节点
//swapPairs(next.next)迟早等于null,等于null后,return next(第二个节点) 赋给前值swapPairs(next.next)
    head.next = swapPairs(next.next);//1号节点 连 下一组的4号节点   后写!
    next.next = head;//2 连 1
    return next;  //找返回值 第二个节点  先写!   
    }
}

2480ef6ac7344e25941132987778476a.png

总结

正常写法 是1号节点连接下一组的3号节点,递归 是1号节点连接下一组的4号节点。

19.删除链表的倒数第N个节点

【文章讲解】代码随想录 (programmercarl.com)

【LeetCode:19.删除链表倒数第N个节点|leetcode题目】19. 删除链表的倒数第 N 个结点

【Carl视频讲解】链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点_哔哩哔哩_bilibili

【5分钟学会:快慢指针】【LeetCode 每日一题】19. 删除链表的倒数第 N 个结点 | 手写图解版思路 + 代码讲解_哔哩哔哩_bilibili

19.删除链表倒数第N个节点   题目

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

  • 遇到无法定位头节点,头节点可能被移动删除,导致定位失效的时候可以考虑用虚拟头节点。
  • 使用虚拟头节点:主要是方便,在于我们不需要对操作的节点,是不是头节点进行特殊判断,而可以采用统一的一个方式进行删除操作

方法一:暴力解法

遍历整个链表,倒数第 n 个结点,其实删除节点是 size - n + 1,其前一个节点是size - n。

class Solution {
   public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode virtualNode = new ListNode(-1);
    virtualNode.next = head;
    //计算链表长度
    int size = 0;
    ListNode cur = head; //临时变量
    while(cur != null){
        size++;
        cur = cur.next;
    }
    //找倒数第n个的前一个节点            
    cur = virtualNode;
    for(int i = 0; i < size - n; i++){ //倒数第n个节点的前一个节点 = size - n处的节点
        cur = cur.next;
    } 
    cur.next = cur.next.next;
    return virtualNode.next; //可能head已被删
 }
}

fe3996c2af16405486b5d1e2f441e1c2.png

 方法二:快慢指针 1.1

思路

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。最后删掉slow所指向的节点就可以了

986e822198d349c09527f265df6af2eb.png

170938b08479412f80b18b01121a4fdb.png

cd81af1d31864214a03a261f971f071e.png

f9cee7dc7f3344b78281381a3416f899.png

class Solution {
   public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode virtualNode = new ListNode(-1);
    virtualNode.next = head;
    //快慢指针
    ListNode fast = virtualNode, slow = virtualNode;
    //快指针先走n步   到达第 n 个节点
    for(int i = 0; i < n; i++){
        fast = fast.next;
    }
    //然后快慢指针 同时走 size - n 步  使快指针到第size个节点(尾部)   慢指针刚好到 倒数第n个节点的前一个节点
    while(fast.next != null){
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return virtualNode.next;
 } 
}

b137c4cf3cb74b69a90f365efd097cad.png

 方法三  快慢指针1.0

方法二和方法三的主要区别是 fast 最后的指向不同。

方法二是最终指向链表最后一个节点,而方法三是最终指向链表最后一个节点的next域(null)

其实,也可以这样理解方法二操作的是链表长度数量的节点,方法三操作的是链表长度数量的节点 + 最后一个空域

a92b71be03a04376bcbcaa4177995a13.png

f47b5f9d287f49d2be6946329448785f.pngc99c124473ae4d199ad2b0b2f098563f.png

444d8dfb92e04714906c31a0a0a0aced.png 代码

class Solution {
   public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode virtualNode = new ListNode(-1);
    virtualNode.next = head;
    //快慢指针
    ListNode fast = virtualNode, slow = virtualNode;
    n++; //防止n 超过链表长度,在for循环后 再添一步fast = fast.next,fast已为空,就操作空指针了
    //快指针先走 n + 1 步   到达第 n + 1 个节点
    for(int i = 0; i < n; i++){
        fast = fast.next;
    }
    //画图
    //然后快指针走 size - (n+1) + 1 步  使快指针指向第size个节点(尾部) 的next (null处)   
    //  慢指针也走 size - (n+1) + 1 步, 使慢指针刚好到 倒数第n个节点的前一个节点
    while(fast != null){
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return virtualNode.next;
 } 
}

d8c601998df34b97b05e1eaf87ddf1da.png

 160.链表相交

【文章讲解】代码随想录 (programmercarl.com)

【LeetCode:160.链表相交|leetcode题目】面试题 02.07. 链表相交

【视频讲解】Leetcode刷题 160.相交链表 Intersection of Two Linked Lists_哔哩哔哩_bilibili

160.链表相交      题目

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null。

图示两个链表在节点 c1 开始相交:

9b9e18d978584d26b024bb241d720072.png

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

方法一   先统计两个链表的长度

可以先统计两个链表的长度,如果两个链表的长度不一样,就让链表长的先走,直到两个链表长度一样,这个时候两个链表再同时每次往后移一步,看节点是否一样,如果有相等的,说明这个相等的节点就是两链表的交点,否则如果走完了还没有找到相等的节点,说明他们没有交点,直接返回null即可,来画个图看一下。

7f2fe694c0f44065b92ebee58081de3b.png

9d2a240d463f4f3e9ba755ba203c03e3.png

链表的长度情况

如果2个链表的长度相等的话,进行遍历时,将同时指针指向头节点。且当2个指针地址相同时,就是相交结点,

但是当链表长度不相等,循环中,a b两指针的运动步数又相同,所以a b指针肯定是不能同时到达相交结点。

然后解决这个问题的方法就是让a b在同一个循环中。让两个链表有相同的循环次数。

如果两个链表如果相交的话,两个指针一定某时刻同时指向同一个节点,即地址相同。所以有两种思想,一种思想是对齐2个链表的尾部。因为从链表尾部进行遍历判断比较难,所以我们使循环中,2个链表尾部对齐,且长度相等,为链表长度小的。

还有一种思想就是a指针先遍历完, a链表再遍历 b链表, b指针遍历完 b列表再遍历 a链表,这样,他们在循环中 循环次数就是相同的,如果两个链表相交,一定会在遍历后一部分的时候2个指针同时指向同一个节点。

使循环中 链表ab长度相等   有两种方法

一种方法是2个链表谁长谁减小长度,走到 两链表长度相等的位置

另一种方法是谁长谁就是a链表(始终操作长链表),然后a链表遍历到相等的位置

b9c6880cd2cf4b3b8581b4426e70f32b.png

//统计链表A和链表B的长度
int lenA = length(headA), lenB = length(headB);
//统计链表的长度
private int length(ListNode node) {
    int length = 0;
    while (node != null) {
        node = node.next;
        length++;
    }
    return length;
}

对了,统计链表A和链表B的长度,用这种解法,就可以在计算链表长度的时候,少写一步(改变临时指针cur的指向)

整体代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
      // 使同长  计算链表长度法
      int sizeA = 0, sizeB = 0;
      ListNode curA = headA;
      ListNode curB = headB;
      while(curA != null){// 求链表A的长度
          sizeA++;
          curA = curA.next;
      }  
      while(curB != null){// 求链表B的长度
          sizeB++;
          curB = curB.next;
      }
      curA = headA;
      curB = headB;

      //方法一         使同长
    //如果节点长度不一样,节点多的先走,直到他们的长度一样为止
    //   while(sizeA != sizeB){
    //       if(sizeA > sizeB){
    //           sizeA--;
    //           //如果链表A长,那么链表A先走
    //           curA = curA.next;
    //       }else{
    //           sizeB--;
    //           //如果链表B长,那么链表B先走
    //           curB = curB.next;
    //       }
    //   }

      //方法二      使同长
    //始终最长的链表是A
    // 让curA为最长链表的头,lenA为其长度
      if (sizeB > sizeA) {
        //1. swap (sizeA, sizeB);
        int tmpLen = sizeA;
        sizeA = sizeB;
        sizeB = tmpLen;
        //2. swap (curA, curB);
        ListNode tmpNode = curA;
        curA = curB;
        curB = tmpNode;
      }
    // 求长度差
       int gap = sizeA - sizeB;
    // 让curA和curB在同一起点上(末尾位置对齐)
       while (gap-- > 0) {
          curA = curA.next;
      }
    //然后开始比较,如果他俩不相等就一直往下走
      while(curA != curB){
          curA = curA.next;
          curB = curB.next;
      }
      
    //走到最后,最终会有两种可能,一种是headA为空,
    //也就是说他们俩不相交。还有一种可能就是headA
    //不为空,也就是说headA就是他们的交点
      return curA;
    }
}

994d5d87aa84405fb7c126b8f62047cc.png

方法二,双指针解决

要是能实现一种算法让两个指针分别从A和B点往C点走,两个指针分别走到C后,又各自从另外一个指针的起点,也就是A指针第二次走从B点开始走,B指针同理,这样,A指针走的路径长度 AO + OC + BO 必定等于B指针走的路径长度 BO + OC + AO,这也就意味着这两个指针第二轮走必定会在O点相遇,相遇后也即到达了退出循环的条件

4dd532c7bbc34867b7d63a64ff30a521.png

推理过程如下

我们还可以使用两个指针,最开始的时候一个指向链表A,一个指向链表B,然后他们每次都要往后移动一位,顺便查看节点是否相等。如果链表A和链表B不相交,基本上没啥可说的,我们这里假设链表A和链表B相交。那么就会有两种情况,

一种是链表A的长度和链表B的长度相等,他们每次都走一步,最终在相交点肯定会相遇。

一种是链表A的长度和链表B的长度不相等,如下图所示

5cb527915f2142fb9bdcee4638c642b0.png

虽然他们有交点,但他们的长度不一样,所以他们完美的错开了,即使把链表都走完了也找不到相交点。

我们仔细看下上面的图,如果A指针把链表A走完了,然后再从链表B开始走到相遇点就相当于把这两个链表的所有节点都走了一遍,同理如果B指针把链表B走完了,然后再从链表A开始一直走到相遇点也相当于把这两个链表的所有节点都走完了

所以如果A指针走到链表末尾,下一步就让他从链表B开始。同理如果B指针走到链表末尾,下一步就让他从链表A开始。只要这两个链表相交最终肯定会在相交点相遇,如果不相交,最终他们都会同时走到两个链表的末尾,我们来画个图看一下

282fbdc1f1f24407a592d4a9013a704c.png

0f02b43d9c2d455eb7cbe3d4f3eb1afd.png

3e44af2907a344a98f8bb5c7ffbf4798.png

如上图所示,A指针和B指针如果一直走下去,那么他们最终会在相交点相遇

代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
      //走 A + B 链表法
      ListNode curA = headA;
      ListNode curB = headB;
        while(curA != curB){ // 相等时 退出
        //如果指针curA不为空,curA就往后移一步。
        //如果指针curA为空,就让指针curA指向curB(注意这里是headB不是curB,curB目前可能不处于头节点位置。)
            curA = curA == null ? headB : curA.next;
        //指针curB同上
            curB = curB == null ? headA : curB.next;
        }    
        //curA要么是空,要么是两链表的交点 
        return curA;
    }
}

c902ea7a8a3947b097c5a1412ce255e6.png

 142.环形链表II

【文章讲解】代码随想录 (programmercarl.com)

【LeetCode:142.环形链表II|leetcode题目】142. 环形链表 II

【Carl视频讲解】把环形链表讲清楚! 如何判断环形链表?如何找到环形链表的入口? LeetCode:142.环形链表II_哔哩哔哩_bilibili

【环内相遇|动画】141.环形链表.gif (568×402) (bcebos.com)

【换入口相遇|动画】142.环形链表II(求入口).gif (572×414) (bcebos.com)

142.环形链表II  题目

  • 给定一个链表的头节点  head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
  • 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
  • 不允许修改 链表

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

b79eb57946484a0bbbe5e5b47b21b26b.png

判断链表是否有环

使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

93e3ec27f9d6424db1eb6601bb4b81a4.png

为什么一定会相遇

快指针每次移动两个节点,慢指针每次移动一个节点。快指针先进入环,慢指针后进入。快指针相当于追慢指针的过程,快指针相对于慢指针来说,每一次以一个节点的移动速度靠近慢指针,所以一定会相遇,不会直接跳过慢指针。如果快指针每次移动3步,则每一次以两个节点的移动速度靠近慢指针,可能会跳过。

判断是否有环的时间复杂度

如果链表不存在环,快指针移动到链表末尾空域结束,需要O(n)的时间,如果链表存在环,快指针和慢指针的差距每一轮缩小一步,所以需要n轮

cbf45632daeb4e28b625a222021125e7.png

如果有环,如何找到这个环的入口

  • 当fast == slow时, 两指针在环中 第一次相遇 。下面分析此时fast 与 slow走过的 步数关系 :
  • 设链表共有 a+b 个节点,其中 链表头部到链表入口 有 a 个节点(不计链表入口节点),链表环 有 b 个节点;设两指针分别走了 fs 步,则有:
  1.      fast 走的步数是slow步数的 2 倍,即 f=2s
  2.      fast 比 slow多走了 n 个环的长度,即f=s+nb;( 解析: 双指针都走过 a 步,然后在环内绕圈直到重合,重合时 fast 比 slow 多走 环的长度整数倍 )
  3.      以上两式相减得:f=2nbs=nb,即fast和slow 指针分别走了 2nn 个 环的周长

目前情况分析:

  • 如果让指针从链表头部一直向前走并统计步数k,那么所有 走到链表入口节点时的步数 是:k=a+nb(先走 a 步到入口节点,之后每绕1 圈环( b 步)都会再次到入口节点)。
  • 而目前,slow 指针走过的步数为 nb 步。因此,我们只要想办法让 slow 再走 a 步停下来,就可以到环的入口。
  • 但是我们不知道 a 的值,该怎么办?依然是使用双指针法。我们构建一个指针,此指针需要有以下性质:此指针和slow 一起向前走 a 步后,两者在入口节点重合。那么从哪里走到入口节点需要 a 步?答案是链表头部head。

722b4ea77c8d44799b473cc890796d74.png

  • 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n

为什么一圈内会相遇

因为慢指针走一圈,快指针走了两圈,而快指针和慢指针的距离肯定在一圈之内

4eda0d907f644654911cba8330c04271.png

公式中有n,快指针多转n圈,且圈数n  最少为1圈,因为y、z 的分界点是相遇点,快指针最少多走(z + y)个节点

632aad77f8d84c23ac3ddaae29166c4d.png

为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?

证明一圈内会相遇

9b18d302c6204dc6a6d91b650a818b8a.png

slow进环的时候,fast一定是在环的任意一个位置

那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。

因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。

也就是说slow一定没有走到环入口3,而fast已经到环入口3了。

这说明什么呢?

fast没到环入口3之前,slow已经与fast相遇了

那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去

代码

public ListNode detectCycle(ListNode head) {
        //快慢指针
        ListNode fast = head;
        ListNode slow = head;
        //先判断有无环,后找环入口节点
        while(fast != null && fast.next != null){//fast走在前面,一次走两步,若有环,fast不可能为空
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){ //若有环 , fast 和 slow 相遇                
               ListNode index1 = slow; //从相遇结点
               ListNode index2 = head; //从头结点
               while(index1 != index2){ //找环入口节点,相等退出
                  index1 = index1.next;
                  index2 = index2.next;
                }
               return index1;
            }
        }
        return null;
    }
}

 dafb0d090bb1470091c06973d8eacaf7.png

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值