链表相关题解二:两两交换链表中的节点;删除链表的倒数第n个节点;链表相交;环形链表

本文介绍了在LeetCode上练习的四道链表题目,包括两两交换节点、删除倒数第N个节点、链表相交和环形链表的检测。强调了虚拟头节点在解决链表问题中的重要性,以及如何使用快慢指针来处理相关问题。文章提供了详细的解题思路和部分代码实现。
摘要由CSDN通过智能技术生成

今天在leetcode上刷了四道题,分别是24.两两交换链表中的节点;19.删除链表的倒数第N个节点;面试题02.07.链表相交;142.环形链表||。

四道题目都是很好的学习巩固链表相关知识以及实际代码操作的问题。我们依旧使用到了虚拟头节点的方法,由此可见,当遇到链表增删相关的题目时,虚拟头节点是相当好用的解题方法。

并且我们还需要清晰一点,链表相交节点是指针相等,而不是数值相等

题目中也涉及了环形链表,在如何判断链表是否有环以及环入口节点上,考察的就是思路,下面我也会将详细思路附上。

再附上题解之前,我先向大家推荐代码随想录,百度即可搜索到,是一个非常好用的学习平台,诸多题解在上面都有非常详细的解答,并且附有多种语言版本。

那么话不多说,上题!

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

题目描述:

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

示例 1:

输入:head = [1,2,3,4]

输出:[2,1,4,3]

示例 2:

输入:head = []

输出:[]

示例 3:

输入:head = [1]

输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100] 内

  • 0 <= Node.val <= 100

解题思路及心得:

在做这道题的时候,还是采用虚拟头节点的方法较为简单 我们需要搞清楚的就是在两两交换时,具体交换的步骤是什么样的,画一个图就非常清晰明了了,我们初始化一个指针cru作为虚拟头结点

截取自代码随想录的思路图,我们可以看一下:

操作后,链表如下:

更直观一些

我们重点需要注意的依旧是赋值的顺序问题,根据我们画图的步骤来进行操作,避免出现断链的情况。

代码如下:

class Solution {
    public ListNode swapPairs(ListNode head) {
        ListNode dummyhead = new ListNode(-1);//设置一个虚拟头结点
        dummyhead.next = head;//将虚拟头结点指向head,方便后续操作
        ListNode cru = dummyhead;
        ListNode temp;//临时节点,保存两个节点后面的节点
        ListNode firstnode;//临时节点,保存两个节点中的第一个节点
        ListNode secondnode;//临时节点,保存两个节点的第二个节点
        while(cru.next != null && cru.next.next != null){
            firstnode = cru.next;
            secondnode = cru.next.next;
            temp = cru.next.next.next;
            cru.next = secondnode;
            secondnode.next = firstnode;
            firstnode.next = temp;
            cru = firstnode;
        }
        return dummyhead.next;
    }
}

当然,我们也可以用递归的方法,代码更加简洁,最重要的是搞清楚递归的逻辑 代码如下:

class Solution {
    public ListNode swapPairs(ListNode head) {
      if(head == null || head.next == null) return head;
      ListNode next = head.next;
      ListNode newnode = swapPairs(next.next);
      next.next = head;
      head.next = newnode;
      return next;
    }
}

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

题目描述:

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

示例 1:

输入:head = [1,2,3,4,5], n = 2

输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1

输出:[]

示例 3:

输入:head = [1,2], n = 1

输出:[1]

提示:

  • 链表中结点的数目为 sz

  • 1 <= sz <= 30

  • 0 <= Node.val <= 100

  • 1 <= n <= sz

解题思路及心得:

这道题考察的还是删除链表中的指定节点,我们依旧采用虚拟头结点的方法,即定义一个节点放在链表头节点的前方充当一个虚拟的头节点,这样方便我们后续的删除操作。

解体的难点在于如何找到倒数第n个节点那么我们就可以反向思考,使用双指针法,设置一个快指针和慢指针,都先指向虚拟头节点,我们先让快指针向后移动n次,再将快慢指针同时向后移动,当快指针指向null时,那么慢指针也就指向了倒数第n个节点。

思路有了,我们就来考虑如何编写代码,我们要注意的是,想要删除一个节点,我们一定要将指针指向他的前一个节点,让前一个节点的指针域指向被删除节点的后一个节点,才能完成删除操作。那么我们就可以让快指针先向前移动n+1次,在让快慢指针同时移动,当快指针指向null时,慢指针也就指向了被删除节点的前一个节点。那么此时判断移动结束的边界值条件即快指针=null。 我们还可以换一个思路,依旧让快指针先向前移动n次,再快慢指针同时移动,当快指针指针指向尾节点时,慢指针也指向了将要被删除节点的前一个节点,只不过此时判断的边界值条件就变成了快指针.next=null了。

代码如下:

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummyhead = new ListNode(0);
        dummyhead.next = head;
        ListNode slowIndex = dummyhead;
        ListNode fastIndex = dummyhead;
        //只要快慢指针相差n个节点即可 
        for(int i = 0;i < n;i++){
            fastIndex = fastIndex.next;
        }
        while(fastIndex.next != null){
            fastIndex = fastIndex.next;
            slowIndex = slowIndex.next;
        }
        slowIndex.next = slowIndex.next.next;
        return dummyhead.next;
        
    }
}

这段代码用的是第二种思路,如果还是不太能理解的话,可以画一画图,很容易就可以想明白了。

面试题02.07.链表相交

题目描述:

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

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

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

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

示例 1:

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3

输出:Intersected at '8'

解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。

从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。

在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1

输出:Intersected at '2'

解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。

从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。

在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2

输出:null

解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。

由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。

这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m

  • listB 中节点数目为 n

  • 0 <= m, n <= 3 * 104

  • 1 <= Node.val <= 105

  • 0 <= skipA <= m

  • 0 <= skipB <= n

  • 如果 listA 和 listB 没有交点,intersectVal 为 0

  • 如果 listA 和 listB 有交点,intersectVal == listA[skipA + 1] == listB[skipB + 1]

做题思路及心得:

这道题简单来说就是求两个链表相交节点的指针,但在这里我们需要注意一点,交点并不是指数值相等,而是指节点的指针相等。在最开始时我也犯了把交点当作数值相等的节点,导致了整个过程的错误,那么我们理清思路,开始进行代码编写 在编写代码之前,我们要清楚一点,在比较节点是否相等时,一定要让两链表的尾部对齐,不然无法进行正确的查找

代码如下:

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
       ListNode cruA = headA;
       ListNode cruB = headB;
       int lenA = 0, lenB = 0;
       //判断链表A的长度
       while(cruA != null){
           lenA++;
           cruA = cruA.next;
       }
       //判断链表B的长度
       while(cruB != null){
           lenB++;
           cruB = cruB.next;
       }
       //因为cruA和cruB已经移动,需要重新赋值
       cruA = headA;
       cruB = headB;
       //判断链表A,B哪个长,并将cruA设置为最长链表的头,lenA为其长度
       if(lenB > lenA){
           int lentemp = lenA;
           lenA = lenB;
           lenB = lentemp;
           ListNode crutemp = cruA;
           cruA = cruB;
           cruB = crutemp;
       }
       //求两链表的长度差
       int x = lenA - lenB;
       //将两链表的尾部对齐
       while(x-- > 0){
           cruA = cruA.next;
       }
       //遍历cruA和cruB,遇到相同的就返回
       while(cruA != null){
           if(cruA == cruB){
               return cruA;
           }
           cruA = cruA.next;
           cruB = cruB.next;
       }
       return null;
    }
}

142.环形链表||

题目描述:

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

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

不允许修改 链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1

输出:返回索引为 1 的链表节点

解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

输入:head = [1,2], pos = 0

输出:返回索引为 0 的链表节点

解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

输入:head = [1], pos = -1

输出:返回 null

解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104] 内

  • -105 <= Node.val <= 105

  • pos 的值为 -1 或者链表中的一个有效索引

做题思路及心得:

这道题的思路比较复杂,首先第一个需要思考的点是,如何判断有环呢? 我们可以使用快慢指针法,分别定义快指针和慢指针,都指向头节点,然后快指针每次移动两个节点,慢指针每次移动一个节点,如果两指针在途中相遇,那么说明有环。

那么我们就要思考,为什么他们一定会相遇,首先我们可以想明白,他们如果可以相遇,那么一定是在环内,因为没有环的话,慢指针永远无法追上快指针。那么再来思考,为什么他们一定会相遇呢?因为当进入环内后,快指针相对于慢指针来说,是每次都以一个节点的速度逐渐靠近慢指针,那么他们就一定会相遇,不存在跳过的情况。

接下来我们就要思考,怎么找出环入口节点? 那么这里我借用代码随想录中给出的详细思路,有疑问的小伙伴也可以去代码随想录中查看,有非常详细的解答

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为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+y): x + y = n (y + z) 因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。 所以要求x ,将x单独放在左面:x = n (y + z) - y , 再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢? 先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。 当 n为1的时候,公式就化解为 x = z, 这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。 也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。 让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

当n大于1时,其实原理是一样的,只不过index1在环内多走了(n-1)1圈

代码如下:

public class Solution {
    public ListNode detectCycle(ListNode head) {
       //定义快慢指针
        ListNode slow = head;
        ListNode fast = head;
        //快指针一次移动两个位置,慢指针每次移动一个位置
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(slow == fast){//说明有环
                ListNode index1 = fast;//相遇节点
                ListNode index2 = head;//头节点
                //两个指针,从头结点和相遇节点开始各走一步,直到相遇,相遇节点即为环入口节点
                while(index1 != index2){
                    index1 = index1.next;
                    index2 = index2.next;
                }
                return index1;
            }
        }
        return null;
    }
}

那么今天的分享就是这些,希望可以对大家有所帮助!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值