【LeetCode】->链表->通向链表自由之路

本文通过解析LeetCode上的链表题目,包括删除倒数第n个节点、两个有序链表合并、链表环检测、单链表反转和求链表中间节点,详解链表操作的关键技巧,如快慢指针法,帮助读者掌握链表操作,迈向“链表自由”。

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

Ⅰ 前言

在我数据结构与算法的链表讲解中,我留下了几个链表必须要掌握的操作,掌握看了它们,就基本可以实现 “链表自由” 了。

在这里插入图片描述
关于链表的详细讲解,可以跳转到我下面的文章。

【数据结构与算法】->数据结构->链表->LRU缓存淘汰算法的实现

这几个操作分别对应了 LeetCode 上的几道题,我们一起来看看。

Ⅱ 删除链表倒数第 n 个结点(#19)

这道题的具体描述如下👇

在这里插入图片描述
这个题我最开始的想法是用散列表先将这些结点按顺序存储起来,这样可以直接根据键值来找到需要删除的点,并且,也只需要遍历一次,就可以将所有结点存入散列表中,后面直接根据键值来取就好了。

想法是好的,最后时间性能上打败了百分之六十多的人,让我意识到这个程序写得是多么蠢。

这个题其实还可以用一个小技巧来做,就是快慢指针。我们先让一个快指针走 n 次,然后这时候慢指针和快指针一起移动,这样当快指针移动到终点,也就是这条链的尾结点的时候,慢指针刚好到要删除的地方。

为了方便对链表进行操作,我引入了一个哨兵,这样如果要删除的结点是头结点,也不需要做特殊处理。我们直接来看代码。👇

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/*
 * @lc app=leetcode.cn id=19 lang=java
 *
 * [19] 删除链表的倒数第N个节点
 */

// @lc code=start
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode pre = new ListNode(0);
        ListNode fastPoint = pre;
        ListNode slowPoint = pre;
        pre.next = head;

        while (n != 0) {
            fastPoint = fastPoint.next;
            n--;
        }

        while (fastPoint.next != null) {
            fastPoint = fastPoint.next;
            slowPoint = slowPoint.next;
        }

        slowPoint.next = slowPoint.next.next;

        return pre.next;
    }
}
// @lc code=end


可以看到性能提高了非常多。

在这里插入图片描述

Ⅲ 两个有序链表的合并 (#21)

我先将这道题的具体描述贴上。

在这里插入图片描述
这道题大家很容易想到利用 while 循环,然后对两个有序链表进行逐节点的比较,这个思路比较简单,我一开始也是想不到什么好方法,只能用这种笨蛋解法。我先将我的笨蛋解法贴出来,思路很简单,大家看一下就很容易理解了。

/*
 * @lc app=leetcode.cn id=21 lang=java
 *
 * [21] 合并两个有序链表
 */

// @lc code=start
/**
 * 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 mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode head = new ListNode();
        ListNode tmp = head;
        ListNode p = l1;
        ListNode q = l2;

        while (p != null && q != null) {
            ListNode cur = new ListNode();
            if (p.val < q.val) {
                cur.val = p.val;
                p = p.next;
            } else {
                cur.val = q.val;
                q = q.next;
            }
            tmp.next = cur;
            tmp = cur;
        }

        while (p != null) {
            ListNode cur = new ListNode(p.val);
            tmp.next = cur;
            tmp = cur;
            p = p.next;
        }
        
        while (q != null) {
            ListNode cur = new ListNode(q.val);
            tmp.next = cur;
            tmp = cur;
            q = q.next;
        }

        return head.next;
    }
}
// @lc code=end


思路就是先对两个有序链表进行比较,直到一个链表到尽头了。那么如果还有链表没有被遍历完,因为是顺序的,所以直接将它余下的元素加入到最后的链表中即可。

这个笨蛋解法的耗时是多少呢?

在这里插入图片描述
可以看到,时间复杂度和空间复杂度都非常高。

我们这个解法每一次比较之后,都要建立一个新的结点,在 while 循环中不仅浪费了很多时间,也浪费了大量的空间,其实没必要,我们再看看一个更聪明的解法,用递归来解决这个问题。

题目的部分我不再给出,只贴出作答的部分👇

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        }
        if (l2 == null) {
            return l1;
        }
        if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

代码比前面的少了非常多,我们再看看性能👇

在这里插入图片描述
通过这个递归,最后返回的就是最小的结点,我们也不需要再建立额外的空间,直接将原先的结点连接起来就好。

Ⅳ 链表中环的检测(#141)

同样的,我先把题目贴出。

在这里插入图片描述

在这里插入图片描述

看到这个题我首先想到的就是用一个set集合或者hash table来存储,遍历整个链表,如果有相同的结点存进去,那就说明有环,返回 true,否则返回 false。但是,同样的,我首先可以想到的算法一定不是最好的算法。

我们注意题目里的进阶要求,空间复杂度为 O(1)。那就肯定不可以用 set 集合了,那样复杂度就变成 O(n) 了。

这时候,我们就要用到一个非常巧妙的解决方法,也是我们上面提到过的 快慢指针。那这个快慢指针要怎么用呢?

大家肯定对我们初中学过的追及问题有印象吧?如果你比你追及的对象速度快,那你就一定可以追到它。对应到这道题,我们让快指针每次往前走两格,慢指针每次往前走一格,如果链表中有环的话,快慢指针最后就会进入环中,相当于两个人开始在操场跑圈,那快的人一定会追上慢的人,两个人一定能相见。

放在这个题中,也是同样的思路。我们最后就检测快指针有没有追到慢指针,如果追到了,说明一定有环,如果没有追到,那链表中一定没有环。根据这个思路,我们来写代码。

/*
 * @lc app=leetcode.cn id=141 lang=java
 *
 * [141] 环形链表
 */

// @lc code=start
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;

        while (fast != null && fast.next != null) {
            if (fast.next == slow) {
                return true;
            }
            fast = fast.next.next;
            slow = slow.next;
        }
        return false;
    }
}
// @lc code=end


这里我还想直接截个图,因为不仅我的编辑器太漂亮了,这个解法也狠漂亮,非常简洁。

在这里插入图片描述

Ⅴ 单链表反转(#206)

具体题目如下👇

在这里插入图片描述
进阶的要求是用迭代或者递归来解决,那我们就不要考虑用一个容器比如 ArrayList 来存放然后反转这种算法了,我们直接来看比较复杂的迭代和递归。

这两个算法在我之前的文章中写过,大家有兴趣可以跳转过去看。

【程序员必修数学课】->基础思想篇->迭代法

【程序员必修数学课】->基础思想篇->递归(上)->泛化数学归纳

【程序员必修数学课】->基础思想篇->递归(下)->分而治之&从归并排序到MapReduce

首先我们先看递归。

/*
 * @lc app=leetcode.cn id=206 lang=java
 *
 * [206] 反转链表
 */

// @lc code=start
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode cur = reverseList(head.next);
        head.next.next = head;
        head.next = null;

        return cur;
    }
}
// @lc code=end


最复杂的地方是下面这句

在这里插入图片描述

这其实就是一个反转的过程。

在这里插入图片描述
拿上图 4 个这个结点来举例子。

head.next.next 指的就是 4.next.next 也就是 5.next,它等于 head,也就是指向了自己,这个语句就实现了结点 5 指向了 4,然后我们再把 head.next = null,也就是 4 指向 5 给断开,这就实现了反转。

在这里插入图片描述
整个递归的过程,返回的 cur 就是 5,也就是最后一个结点,这样所有的反转完成之后,返回的 5 就相当于是这个反转链表的头结点了,也就完成了整个的反转过程。

我们来看看递归的性能👇

在这里插入图片描述
时间是很快,但是空间消耗是很大的,因为每一层递归都要消耗系统堆栈空间,所以占用的内存很大。

我们再来看迭代。

public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        ListNode tmp = null;

        while (cur != null) {
            tmp = cur.next;
            cur.next = pre;
            pre = cur;
            cur = tmp;
        }

        return pre;
    }

迭代法实现的思想就是每遍历到一个结点,用 cur 标记,然后 pre 是 cur 前面的结点,使得 cur 指向 pre,这样遍历完,到 cur 指向 null 的时候,pre 正好指向尾结点,这样就实现了反转。

迭代法的思路不难理解,大家可以用代码做个参考。

迭代法实现的性能也是很强大,空间消耗比递归法少很多👇

在这里插入图片描述

Ⅵ 求链表的中间节点(#876)

我们先看看这道题的具体描述。

在这里插入图片描述
我们要求一个链表的中间结点。经过前面几道题,我们知道了有一个很巧妙的遍历方法,就是快慢指针,这道题简直就是为快慢指针而设的。

要求中间结点,那我们令快指针和慢指针都指向头结点,然后开始遍历。快指针一次走两格,慢指针一次走一格,这样直到快指针遍历到链表末端。如果是奇数个,那快指针刚好指向尾结点,fast.next = null。如果是偶数个,那快指针就指向了尾结点的下一个结点,也就是空,即 fast = null

所以,我们可以根据这两个边界条件,来判断快指针是否遍历完了整个链表。当快指针遍历完链表之后,由于它每次都比慢指针多走一步,所以慢指针最后指向的就是我们要求的中间结点。

我们直接来看代码👇

/*
 * @lc app=leetcode.cn id=876 lang=java
 *
 * [876] 链表的中间结点
 */

// @lc code=start
/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;

        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }

        return slow;
    }
}
// @lc code=end


为方便大家看,我再截个图👇

在这里插入图片描述
性能测试如下👇

在这里插入图片描述
OK,这就是链表的几个重要操作,掌握熟练这些题,我们就在通向链表自由之路上前进了一大截。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值