likes
comments
collection
share

【面试高频题】热门数据结构面试题合集(链表)

作者站长头像
站长
· 阅读数 16

本文正在参加「金石计划」

链表

链表一直是面试数据结构中的重中之重,今天通过 555 道与链表相关的题目来进行学习。


92. 反转链表 II

给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

示例 1: 【面试高频题】热门数据结构面试题合集(链表)

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

示例 2:

输入:head = [5], left = 1, right = 1
输出:[5]

提示:

  • 链表中节点数目为 n
  • 1 <= n <= 500
  • -500 <= Node.val <= 500
  • 1 <= left <= right <= n
朴素解法

为了减少边界判断,我们可以建立一个虚拟头结点 dummy,使其指向 head,最终返回 dummy.next。

这种「哨兵」技巧能应用在所有的「链表」题目。

黄色部分的节点代表需要「翻转」的部分:

【面试高频题】热门数据结构面试题合集(链表)

之后就是常规的模拟,步骤我写在示意图里啦 ~

【面试高频题】热门数据结构面试题合集(链表)

代码:

class Solution {
    public ListNode reverseBetween(ListNode head, int l, int r) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;

        r -= l;
        ListNode hh = dummy;
        while (l-- > 1) hh = hh.next;

        ListNode a = hh.next, b = a.next;
        while (r-- > 0) {
            ListNode tmp = b.next;
            b.next = a;
            a = b;
            b = tmp;
        }
        
        hh.next.next = b;
        hh.next = a;
        return dummy.next;
    }
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)

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

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

进阶:你能尝试使用一趟扫描实现吗?

示例 1:

【面试高频题】热门数据结构面试题合集(链表)

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

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

提示:

  • 链表中结点的数目为 sz
  • 1<=sz<=301 <= sz <= 301<=sz<=30
  • 0<=Node.val<=1000 <= Node.val <= 1000<=Node.val<=100
  • 1<=n<=sz1 <= n <= sz1<=n<=sz
快慢指针

删除链表的倒数第 n 个结点,首先要确定倒数第 n 个节点的位置。

我们可以设定两个指针,分别为 slowfast,刚开始都指向 head。

然后先让 fast 往前走 n 步,slow 指针不动,这时候两个指针的距离为 n

再让 slowfast 同时往前走(保持两者距离不变),直到 fast 指针到达结尾的位置。

这时候 slow 会停在待删除节点的前一个位置,让 slow.next = slow.next.next 即可。

但这里有一个需要注意的边界情况是:如果链表的长度是 L,而我们恰好要删除的是倒数第 L 个节点(删除头节点),这时候 fast 往前走 n 步之后会变为 null,此时我们只需要让 head = slow.next 即可删除。

代码:

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        if (head.next == null) return null;
        ListNode slow = head, fast = head;
        while (n-- > 0) fast = fast.next;
        if (fast == null) {
            head = slow.next;
        } else {
            while (fast.next != null) {
                slow = slow.next; fast = fast.next;
            }
            slow.next = slow.next.next;
        }
        return head;
    }
}
  • 时间复杂度:需要扫描的长度为链表的长度。复杂度为 O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)

141. 环形链表

给你一个链表的头节点 head,判断链表中是否有环。

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

如果链表中存在环 ,则返回 true 。 否则,返回 false 。

示例 1:

【面试高频题】热门数据结构面试题合集(链表)

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

输出:true

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

提示:

  • 链表中节点的数目范围是 [0,104][0, 10^4][0,104]
  • −105<=Node.val<=105-10^5 <= Node.val <= 10^5105<=Node.val<=105
  • pos-1 或者链表中的一个 有效索引 。

进阶:你能用 O(1)O(1)O(1)(即,常量)内存解决此问题吗?

快慢指针

这是一道「快慢指针」的模板题。

使用两变量 slowfast 分别代表快慢指针,slow 每次往后走一步,而 fast 每次往后两步,两者初始化均为 head

若链表无环,则 fast 必然会先走到结尾,算法正常结束;若链表有环,当 slow 来到环入口时,fast 必然已经在环中的某个位置,此时再继续进行,由于存在速度差,发生相遇必不可能是 slow 追上 fast,而只能是 fast 从后方追上 slow,由于速度差为 111,因此最多会消耗不超过环节点个数的回合,两者即会相遇。

代码:

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next; fast = fast.next.next;
            if (slow == fast) return true;
        }
        return false;
    }
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)

142. 环形链表 II

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

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。

注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

【面试高频题】热门数据结构面试题合集(链表)

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

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

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

提示:

  • 链表中节点的数目范围在范围 [0,104][0, 10^4][0,104]
  • −105<=Node.val<=105-10^5 <= Node.val <= 10^5105<=Node.val<=105
  • pos 的值为 -1 或者链表中的一个有效索引

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

快慢指针

起始使用 slowfast 作为慢快指针(slow 每次走一步,fast 每次走两步),起始均为 head

fast 顺利走到结尾,说明链表无环,直接返回 null

若两者成功相遇,说明链表有环。我们定义链表起点到环入口距离为 x,环内节点数量为 y。那么从链表起点到环入口的任意路径都能归纳成 x+k×yx + k \times yx+k×y(其中 kkk 为大于等于 000 的任意值)。

当两者首次相遇时,假设 slow 走的距离为 d1,而 fast 走的距离为 d2,根据速度差定义可知 d2=d1×2d2 = d1 \times 2d2=d1×2。同时根据 141. 环形链表 结论,两者必然在环中相遇,且必然是 fast 在环内从后面追上 slow,因此 d2 相比于 d1 必然是多了 y 的整数倍,即有 d2=d1+m×yd2 = d1 + m \times yd2=d1+m×y(其中 mmm 为圈数),即可推导出 d1=m×yd1 = m \times yd1=m×y

同时根据链表起点到环入口的任意路径均表示为 x+k×yx + k \times yx+k×y,我们知道如果 slow 再走 x 步会到达环入口,同时链表起点到环入口也是 x 步,因此我们可以复用 fast,将其复位到链表起点,和 slow 一起每次往前一步,当两者再次相遇,必然是同时位于环入口。

同时该做法容易拓展成求 x 和求 y 等问题。

代码:

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode base = head;
        ListNode slow = head, fast = head;
        boolean ok = false;
        while (!ok && fast != null && fast.next != null) {
            slow = slow.next; fast = fast.next.next;
            if (slow == fast) ok = true;
        }
        if (!ok) return null;
        fast = head;
        while (slow != fast) {
            slow = slow.next; fast = fast.next;
        }
        return slow;
    }
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(1)O(1)O(1)

总结

与数组相比,同为线性数据结构的链表具有 O(1)O(1)O(1) 插入、删除的特性,这对于一些删除后进行元素挪动,或新增后触发扩容的算法题而言,具有降低复杂度的意义。