链表类题目专题训练 -- LeetCode上6道与链表相关的题

这个专题中的题目是我跟随代码随想录的刷题计划,在LeetCode上做的与链表相关的题目,用于加深对链表的理解!

下面的内容将会有每一道题目的题意、在代码随想录中对应的参考文章、我的思路以及我所写的Java代码,希望对你有帮助!

目录

1 - LeetCode 203 移除链表元素 -- 虚拟头节点

2 - LeetCode 707 设计链表 -- 实现链表的基本操作

3 - LeetCode 206 反转链表 -- 递归法和双指针法

4 - LeetCode 19 删除链表的倒数第N个节点 -- 双指针法

5 - LeetCode 面试题 02.07. 链表相交 -- 双指针法

6 - LeetCode 142 环形链表 II -- 判断链表是否存在环并找出环的入口


1 - LeetCode 203 移除链表元素 -- 虚拟头节点

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-linked-list-elements

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

示例 1:

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
示例 2:

输入:head = [], val = 1
输出:[]
示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]
 

提示:

列表中的节点数目在范围 [0, 104] 内
1 <= Node.val <= 50
0 <= val <= 50

参考文章:听说用虚拟头节点会方便很多?

思路:

这个题目考验的是链表的删除操作,主要需要考虑的点在于当头节点需要进行删除时应该怎么做。这里用到了虚拟头节点,即new一个新的链表节点作为虚拟头节点,然后将原链表的头节点赋给虚拟头节点的next节点,这样子,原链表的头节点就变成了新链表的第二个节点,就可以将原链表的头节点与其他链表中的结点一样一视同仁进行判断与删除操作,最后再返回虚拟头节点的next节点作为输出即可。

在写本题的时候犯了两个错误。

1、需要注意while循环的判断语句应该是判断rnNode.next(rnNode代表当前节点)是否为空指针,如果为空指针则跳出循环。这里while循环的判断语句不应该写判断rnNode是否为空指针。

2、注意,判断是否需要删除节点是通过rnNode.next的值进行判断的,因此这个时候应该已经确认rnNode是不需要进行删除的。当rnNode.next.val是要删除的值时,进行删除操作,rnNode.next = rnNode.next.next,之后,不应该进行rnNode = rnNode.next的赋值操作,因为这个时候新的rnNode.next是否需要删除还未判断,只有当rnNode.next.val不是要删除的值时,才能进行rnNode = rnNode.next的操作,因为这个时候已经确认rnNode.next不需要进行删除了。

本题Java代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeElements(ListNode head, int val) {
        ListNode ansNode = new ListNode(0, head);
        ListNode rnNode = ansNode;
        while(rnNode.next!=null){
            if(rnNode.next.val==val){
                rnNode.next=rnNode.next.next;
            }else{
                rnNode=rnNode.next;
            }
        }
        return ansNode.next;
    }
}

2 - LeetCode 707 设计链表 -- 实现链表的基本操作

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/design-linked-list

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
 

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3
 

提示:

所有val值都在 [1, 1000] 之内。
操作次数将在  [1, 1000] 之内。
请不要使用内置的 LinkedList 库。

参考文章:一道题目考察链表五个常见操作!​

思路:

这个题目主要是实现链表一些常见的基本操作,需要理解链表的基本概念还有操作的实现原理。这里我的写法使用到了虚拟头节点。需要注意的是没想到居然是在类MyLinkedList中再声明ListNode类,我一开始以为是直接将MyLinkedList类来当做链表中的节点,就写的有点乱,看了文章没想到居然是上述那样写的,写起来就简单多了。

本题Java代码:

class MyLinkedList {

    class ListNode{
        int val;
        ListNode next;
        ListNode(int val, ListNode next){
            this.val = val;
            this.next = next;
        }
    }

    private ListNode head;
    private int size;

    public MyLinkedList() {
        head = new ListNode(0, null);
        size = 0;
    }
    
    public int get(int index) {
        if(size <= index)return -1;
        ListNode rnNode = head.next;
        while(index-- != 0){
            rnNode = rnNode.next;
        }
        return rnNode.val;
    }
    
    public void addAtHead(int val) {
        ListNode newNode = new ListNode(val, head.next);
        head.next = newNode;
        size++;
        //print();
    }
    
    public void addAtTail(int val) {
        ListNode rnNode = head;
        ListNode newNode = new ListNode(val,null);
        while(rnNode.next != null){
            rnNode = rnNode.next;
        }
        rnNode.next = newNode;
        size++;
        //print();
    }
    
    public void addAtIndex(int index, int val) {
        if(size < index)return;
        else if(index < 0)addAtHead(val);
        ListNode rnNode = head;
        while(index-- != 0){
            rnNode = rnNode.next;
        }
        ListNode newNode = new ListNode(val, rnNode.next);
        rnNode.next = newNode;
        size++;
        //print();
    }
    
    public void deleteAtIndex(int index) {
        if(size <= index)return;
        ListNode rnNode = head;
        while(index-- != 0){
            rnNode = rnNode.next;
        }
        rnNode.next = rnNode.next.next;
        size--;
        //print();
    }

    //用于打印链表
    private void print(){
        ListNode rnNode = head.next;
        while(rnNode != null){
            System.out.print(rnNode.val+" ");
            rnNode = rnNode.next;
        }
        System.out.println();
    }
}

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * MyLinkedList obj = new MyLinkedList();
 * int param_1 = obj.get(index);
 * obj.addAtHead(val);
 * obj.addAtTail(val);
 * obj.addAtIndex(index,val);
 * obj.deleteAtIndex(index);
 */

3 - LeetCode 206 反转链表 -- 递归法和双指针法

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reverse-linked-list

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
 

示例 1:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]


示例 2:
输入:head = [1,2]
输出:[2,1]


示例 3:

输入:head = []
输出:[]
 

提示:

链表中节点的数目范围是 [0, 5000]
-5000 <= Node.val <= 5000


思路:

最开始我自己的写法是使用递归的方法,核心代码(rListTail代表当前反转链表的尾节点):

reverse(rnNode.next);
rListTail.next = rnNode;
rListTail = rnNode;

在每次递归当中,先将当前节点的next节点作为递归函数的参数进行递归,生成当前的反转链表(即原链表在当前节点后面的节点反转得到的反转链表),然后再将当前反转链表的尾节点的next节点设为当前节点,最后再将当前节点作为当前反转链表的尾节点。

在这道题中出现了两次问题,一次是因为忘记将尾节点的next节点设为null,原链表的head节点最后变成反转链表的尾节点后,应该将它的next节点设为null,不过我在后边也修改成了在每次修改rListTail的值后将rListTail的next节点设为null。

另一个问题是要注意题目输入的链表有可能是空链表,这时候只要返回null就可以了。

递归法Java代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    private ListNode rListHead,rListTail;//分别表示当前反转链表的头节点和尾节点
    private void reverse(ListNode rnNode){
        if(rnNode == null){
            //这个时候传入的原始链表是空链表
            rListHead = null;
            return;
        }
        if(rnNode.next == null){
            //这时候意味着已经遍历到了原链表的尾节点,此时要将其设为当前反转链表的头节点和尾节点
            rListHead = rnNode;
            rListTail = rnNode;
            return;
        }
        reverse(rnNode.next);
        rListTail.next = rnNode;
        rListTail = rnNode;
        rListTail.next = null;//别忘了将当前反转链表尾部的next指针设为null
    }
    public ListNode reverseList(ListNode head) {
        reverse(head);
        return rListHead;
    }
}

另一种解法(双指针法):

参考文章:​​​​​​听说过两天反转链表又写不出来了?

看了参考文章后发现还可以使用双指针来做这道题,而且代码量也更少,也没有递归法抽象,更容易理解。

以下这个图很形象地展现了使用双指针的过程(图片转自代码随想录)

图片

 双指针法Java代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null, cur = head, tmp;
        while(cur != null){
            tmp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = tmp;
        }
        return pre;
    }
}

4 - LeetCode 19 删除链表的倒数第N个节点 -- 双指针法

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list

给你一个链表,删除链表的倒数第 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个就让前指针先往前前进n个,然后再让前后指针共同前进,当前指针指向的结点为空节点时,删除后指针的儿子即可。

注意点:

1、使用虚拟头节点,这是为了让删除第一个节点的操作更加方便

2、注意后指针在最后指向的并不是要删除的节点,而是要删除节点的前一个节点,这是为了在进行删除操作时,直接修改后指针指向的节点的next节点即可,更加简便。

本题Java代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode virtualHead = new ListNode(0, head);
        ListNode fast = head, slow = virtualHead;
        while(n--!=0){
            fast = fast.next;
        }
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;
        return virtualHead.next;
    }
}

5 - LeetCode 面试题 02.07. 链表相交 -- 双指针法

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 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]
 

进阶:你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案?

参考文章:链表相交,找出交点

思路:

这个问题最需要注意的地方在于,相交部分的链表节点地址是一样的,而不是仅节点数值相等,可以说是题意没有说清楚吧。思路是先分别求出两链表的长度,然后相减得到差值,将长度较长的链表先遍历到等同于差值数的地方,然后再两个链表同时遍历,看当前节点的地址是否相同,若不同则接着遍历,直到找到地址相同的节点或者遍历完两个链表。参考图解过程(图片转载自代码随想录):

在示例图中,链表A长度较长,与链表B的长度相差为4,所以让curA往右移动4个节点,然后再与curB一起移动并判断当前两指针所指的节点地址是否相同。

图片

图片

 本题Java代码:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = 0, lenB = 0;
        ListNode rn = headA;
        while(rn != null){
            rn = rn.next;
            lenA++;
        }
        rn = headB;
        while(rn != null){
            rn = rn.next;
            lenB++;
        }
        int gap;
        ListNode cur1, cur2;
        if(lenA > lenB){
            gap = lenA - lenB;
            cur1 = headA;
            cur2 = headB;
        }else{
            gap = lenB - lenA;
            cur1 = headB;
            cur2 = headA;
        }
        while(gap--!=0){
            cur1 = cur1.next;
        }
        while(cur1 != null){
            if(cur1 == cur2){
                return cur1;
            }else{
                cur1 = cur1.next;
                cur2 = cur2.next;
            }
        }
        return null;
    }
}

6 - LeetCode 142 环形链表 II -- 判断链表是否存在环并找出环的入口

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/linked-list-cycle-ii

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 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 或者链表中的一个有效索引
 

进阶:你是否可以使用 O(1) 空间解决此题?


参考文章:不仅要确定环,还要找到入口!

思路:

这道题目还是看参考文章中的内容吧,因为解释起来有比较多的内容。

概括起来主要有两步:

1、先建一个快指针和慢指针,快指针每次往前走2步,慢指针每次往前走1步,如果两个指针最后能相遇,则意味着链表存在环,此时要记录下两个指针的相遇点(为什么说存在环则两个指针一定能相遇?参考高中物理中的匀速追赶问题)

2、得到快慢指针的相遇点后,再在链表的头节点和相遇点分别各设立一个指针,然后两个指针每次都往前移动1个单位,最后两个指针会在环的入口处相遇。至于为何两个指针为何会刚好在环的入口处相遇,见参考文章中的推导过程

本题Java代码:

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head, slow = head;
        while(fast != null && fast.next != null){
            slow = slow.next;
            fast = fast.next.next;
            if(fast == slow)break;
        }
        if(fast == null || fast.next == null)return null;//没有环
        ListNode index1 = head, index2 = fast;
        while(index1 != index2){
            index1 = index1.next;
            index2 = index2.next;
        }
        return index1;
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值