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

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

两两交换链表中的节点

题目:24. 两两交换链表中的节点  
题目链接:https://leetcode.cn/problems/swap-nodes-in-pairs/
文章讲解:https://programmercarl.com/0024.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.html
视频讲解: https://www.bilibili.com/video/BV1YT411g7br/
状态:AC

自己看到题目的第一想法

链表题目,画图理解!
使用3个结点,pre,cur,next去整体进行运算,每交换一次,三个结点的位置移动。

  1. 增加一个dummyHead节点,方便pre的定义。
  2. 通过中间变量temp去保存下一个交换节点的前序节点。

----------------二刷记录----------------
二刷时,刚开始也想着建立一个虚拟pre节点,然后做交换,但是没想明白交换之后节点该怎么移动,后面看了下之前的代码,pre节点的位置应该是在每次交换完之后的下一个待交换位置的前序节点。

public ListNode swapPairs(ListNode head) {
    // 看之前自己的比较 先定义3个节点
    // pre cur next 交换一次3个节点后再往前走到下一个待换的位置
    if(head == null || head.next == null){
        return head;
    }
    ListNode initNode = new ListNode(-1,head);
    ListNode pre = initNode;
    ListNode cur = head;
    ListNode next = cur.next;
    while(cur != null && next != null){
        // 用cur和next进行交换,pre记录下一个待交换节点的前序
        // 交换完之后next的next位置就找不到了,因此先暂存一下
        ListNode tmp = next.next;
        // 开始交换cur和next
        next.next = cur;
        cur.next = tmp;
        pre.next = next;

        pre = cur;
        cur = tmp;
        if(tmp != null){
            next = cur.next;
        } 
    }
    return initNode.next;
}

看完代码随想录之后的想法

自己实现过程中遇到哪些困难

还是边界条件的问题,审题非常重要,判空等等。以及每个循环逻辑的方法内都需要做好判空逻辑。

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

19.删除链表的倒数第N个节点
题目链接:https://leetcode.cn/problems/remove-nth-node-from-end-of-list/
文章讲解:https://programmercarl.com/0019.%E5%88%A0%E9%99%A4%E9%93%BE%E8%A1%A8%E7%9A%84%E5%80%92%E6%95%B0%E7%AC%ACN%E4%B8%AA%E8%8A%82%E7%82%B9.html
视频讲解:https://www.bilibili.com/video/BV1vW4y1U7Gf/
状态:前几次写错了,重新画图+改改代码AC了

自己看到题目的第一想法

用快慢指针做,一前一后,题目既然明确了倒数第n个结点,可以先将fast指针移动n-1个结点,然后fast和slow同时开始移动,等fast移动到了最后一个结点,那slow这个结点就是要被移除的结点。(倒数第n个节点的话,fast和slow的初始位置要相差n-1个位置,如果n = 1那slow的位置和fast的位置就是一致)
想完想法后开始画图,链表类的问题基本上画图能整理出思路,也能寻找到一些边界条件。

-------------------二刷记录---------------------
看了一遍自己写的时候的第一次的想法,整体使用快慢指针去往前查找,fast指针先往前移动n,然后fast指针和slow指针同时往前移动直到slow指针移动到末尾。关键逻辑是slow指针要在被删除节点的上一个节点停止,并且最好使用一个dummyNode作为最前的node用于处理头节点被删除的情况。
代码:

public ListNode removeNthFromEnd(ListNode head, int n) {
    // 删除倒数第n个的话,可以用快慢指针,先移动指针到n,然后在前后指针开始移动
    // 删除节点的话slow最好移动到待删除的前面一个位置
    // 使用虚拟头节点的话能处理第一个节点被删除的情况
    ListNode preHead = new ListNode(0,head);
    ListNode fast = preHead;
    ListNode slow = preHead;
    while(n-- > 0){
        fast = fast.next;
    }
    while(fast.next != null){
        fast = fast.next;
        slow = slow.next;
    }
    slow.next = slow.next.next;
    return preHead.next;
}

看完代码随想录之后的想法

代码随想录的整体思路差不多,但是他移动的slow节点做了一些变化,整体从dummyNode开始移动,并且slow是移动到了要被删除结点的上一个结点,这样和我的代码相比,减少了temp这个中间变量的存储(我的代码逻辑是使用temp存储slow的上一个结点)。

// 我的代码
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        // 删除倒数第n个的话,可以用快慢指针,先移动指针到n,然后在前后指针开始移动
        ListNode preHead = new ListNode(0,head);
        ListNode fast = head;
        ListNode slow = head;
        // 处理边界条件,head的size为1时
        if(head.next == null){
            return null;
        }
        // 倒数第n个节点的话,fast和slow的初始位置要相差n-1个位置,如果n = 1那slow的位置和fast的位置就是一致
        while(n - 1 > 0){
            fast = fast.next;
            n--;
        }
        // 保存slow的前序节点,因为需要删除slow,并将slow的前序节点指向最后一个fast节点
        ListNode temp = preHead;
        while(fast != null && fast.next != null){
            fast = fast.next;
            temp = slow;
            slow = slow.next;
        }
        temp.next = slow.next;
        return preHead.next;
    }
}

// 代码随想录上的代码
public ListNode removeNthFromEnd(ListNode head, int n){
    ListNode dummyNode = new ListNode(0);
    dummyNode.next = head;

    ListNode fastIndex = dummyNode;
    ListNode slowIndex = dummyNode;

    // 只要快慢指针相差 n 个结点即可
    for (int i = 0; i <= n  ; i++){ 
        fastIndex = fastIndex.next;
    }

    while (fastIndex.next != null){
        fastIndex = fastIndex.next;
        slowIndex = slowIndex.next;
    }

    //此时 slowIndex 的位置就是待删除元素的前一个位置。
    //具体情况可自己画一个链表长度为 3 的图来模拟代码来理解
    slowIndex.next = slowIndex.next.next;
    return dummyNode.next;
}

自己实现过程中遇到哪些困难

还是边界条件的问题,虽然有思路了,但是一定要想好边界条件的影响,并且刚开始并没有想到用一个dummyHead作为头节点,所有链表类的题目都可以思考下用dummyHead作为虚拟头节点,这个结点不管是后续遍历的边界条件判断,还是返回链表头位置,都是帮助非常大的。

链表相交

题目:面试题 02.07. 链表相交
题目链接:https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/
文章讲解:https://programmercarl.com/%E9%9D%A2%E8%AF%95%E9%A2%9802.07.%E9%93%BE%E8%A1%A8%E7%9B%B8%E4%BA%A4.html
状态:开写!

自己看到题目的第一想法

这题目之前看过一次代码随想录了,整体思路就是有相交的话肯定后面有一段是完全一致的,那就先比较两个链表的长度,让长的那个链表遍历到和短的链表的长度一样长为止。
然后在一起做遍历,发现想等就return。(这里的问题是我一直用val比较而不是指针比较,因此错了好几次。最后又去看了下随想录)
-----------------二刷记录---------------------
二刷回忆起了之前的刷题思路,链表相交尾部肯定一致,那只需要求出两个链表长度的差值,然后一个链表先往前走,走到相等长度时2个节点一起走

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    // 链表相交尾部肯定一致,那只需要求出两个链表长度的差值
    // 然后一个链表先往前走,走到相等长度时2个节点一起走
    if(headA == null || headB == null){
        return null;
    }
    Integer lengthA = 0;
    Integer lengthB = 0;
    ListNode curA = headA;
    ListNode curB = headB;
    while(curA != null || curB != null){
        if(curA != null){
            lengthA++;
            curA = curA.next;
        }   
        if(curB != null){
            lengthB++;
            curB = curB.next;
        }  
    }
    curA = headA;
    curB = headB;
    int count = Math.abs(lengthA-lengthB);
    while(count-- > 0){
        if(lengthA > lengthB){
            curA = curA.next;
        }else if(lengthA < lengthB){
            curB = curB.next;
        }
    }
    while(curA != null && curB != null){
        if(curA == curB){
            return curA;
        }else{
            curA = curA.next;
            curB = curB.next;
        }
    }
    return null;
}

看完代码随想录之后的想法

这道题主要还是审题,如果没好好审题也想不到直接用哪个结点相等去返回,虽然有交点,但是交点后面的值应该都相等。但是这里考虑到如果直接用引用来进行比较的话,那就不用考虑交点后面的值会不会想等了,因为引用相等,在同一个地址,那后面的链表内容肯定想等。

自己实现过程中遇到哪些困难

主要还是没关注到比较时应该用引用来比较,而不是用值来比较。用引用来比较的话,得出的结果肯定是链表后面的结点都是一致的。有交点。

环形链表 II

题目:142. 环形链表 II
题目链接:https://leetcode.cn/problems/linked-list-cycle-ii/
文章讲解:https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html
视频讲解:https://www.bilibili.com/video/BV1if4y1d7ob/
状态:环形链表,套圈跑步!开写

自己看到题目的第一想法

刚开始想写双指针,后面没想出规律。直接用暴力解法,搞一个HashMap,key为结点,value为结点对应的索引。一直迭代链表,如果map中的key出现了相同的结点,则直接返回当前结点为第一个环形结点。没想到直接AC了。。

-------------二刷记录------------------
因为知道这个题目还是有一定难度并且涉及到一些数据推理,因此直接看了代码随想录的答案。使用快慢指针来判断是否有环。
整体将环形链表分为3段,x、y、z。把起始节点到入环的第一个节点的距离认为x,快节点和慢节点从入环的第一个节点到碰到的距离认为y,碰到的位置到入环的第一个节点的距离为z。相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数*2:(x + y)*2 = x + y + n (y + z)。公式最终转换为x=(n-1)(y+z)+z。 n>=1 当n=1时,x=z。这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
代码实现:

public ListNode detectCycle(ListNode head) {
    // 环形链表,快慢指针遍历,来判断是不是环形链表。如跑步套圈如果是环形的话,快的那个每次逼近一步迟早会追上慢的那个
    // 然后相交节点的处理使用的是数学公式推导
    // 把起始节点到入环的第一个节点的距离认为x,快节点和慢节点从入环的第一个节点到碰到的距离认为y,碰到的位置到入环的第一个节点的距离为z。
    // 相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
    // 因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:(x + y) * 2 = x + y + n (y + z)
    // 公式最终转换为x=(n-1)(y+z)+z。 n>=1 当n=1时,x=z
    // 这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
    ListNode fast = head;
    ListNode slow = head;
    while(fast != null && fast.next != null){
        slow = slow.next;
        fast = fast.next.next;
        if(slow == fast){
            // 从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。
            ListNode index1 = head;
            ListNode index2 = fast;
            while(index1 != index2){
                index1 = index1.next;
                index2 = index2.next;
            }
            return index2;
        }
    }
    return null;
}

看完代码随想录之后的想法

双指针在自己写的时候其实没有想清楚为什么一个指针走一步、一个指针走两步可以用于作为判断环形链表的条件。后面在代码随想录里提到快指针走两步,慢指针走一步,其实就是快指针是一个节点一个节点靠近慢指针的。理解了为什么用双指针可以判断环形链表。
后面代码随想录用一些数学上的思想去抽象该问题,将整个行走路线分为x,y,z三段。需要画图去看。

  1. 相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
  2. 根据快指针走两步慢指针走一步行进特性,整理出公式(x + y) * 2 = x + y + n (y + z)
    因为要求的是x,因此上面的公式整理完后x = (n - 1) (y + z) + z,n=1时,x=z,表明从头结点出发一个指针,从相遇节点也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。用上面的方法写代码。

自己实现过程中遇到哪些困难

按照代码随想录的思想去写代码整体就比较简单了。

今日收获&学习时长

收获:

  1. 双指针法的运用进一步巩固。
  2. 链表后面是否相连,可以直接用引用来判断,而不是用值来做判断。
  3. 环形链表这道题需要结合部分数学的思想去抽象。找出数学上的解法,然后再转换为代码。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值