前言
- 链表相交 & 成环问题可以归为一类问题,在面试中出现频率较高;
- 在这篇文章里,我将梳理链表相交 & 成环问题的解法。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
系列文章
- 《算法面试题 | 链表问题总结》
- 《算法面试题 | 链表相交 & 成环问题》
- 《算法面试题 | 回溯算法解题框架》
- 《数据结构面试题 | 并查集 & 联合 - 查找算法》
- 《数据结构面试题 | 二叉树基础》
延伸文章
相关问题 | 提示 & 题解 |
160. 相交链表 Intersection of Two Linked Lists |
【题解】 |
141. 环形链表 Linked List Cycle |
【题解】 |
142. 环形链表 II Linked List Cycle II |
【题解】 |
61. 旋转链表 Rotate List |
【题解】 |
(扩展)457. 环形循环数组 Circular Array Loop |
【题解】 |
目录
1. 判断链表相交
160. Intersection of Two Linked Lists 相交链表【题解】
编写一个程序,找到两个单链表相交的起始节点。
解法1:哈希表
class Solution { fun getIntersectionNode(headA: ListNode?, headB: ListNode?): ListNode? { if (null == headA || null == headB) { return null } val set = HashSet<ListNode>() var pA = headA var pB = headB while (null != pA) { set.add(pA) pA = pA.next } while (null != pB) { if (set.contains(pB)) { return pB } pB = pB.next } return null } } 复制代码
复杂度分析:
- 时间复杂度:两个链表的节点最多访问一次,时间复杂度为O(m+n)
- 空间复杂度:以其中一个链表构建哈希表,空间复杂度为O(m)
解法2:链表成环
class Solution { fun getIntersectionNode(headA: ListNode?, headB: ListNode?): ListNode? { if (null == headA || null == headB) { return null } var pA = headA var pB = headB while (pA != pB) { // 退出的关键是:pA和pB指向同一个指针(不是值相等),或者都指向null pA = if (null == pA) headB else pA.next pB = if (null == pB) headA else pB.next } return pA } } 复制代码
复杂度分析:
- 时间复杂度:两个链表的节点最多访问一次,时间复杂度为O(m+n)
- 空间复杂度:只是用了常量级别变量,空间复杂度为O(1)
2. 判断链表成环
141. Linked List Cycle 环形链表【题解】
给定一个链表,判断链表中是否有环。
解法1:哈希表
class Solution { fun hasCycle(head: ListNode?): Boolean { val set = HashSet<ListNode>() var p = head while (null != p) { if (set.contains(p)) { return true } set.add(p) p = p.next } return false } } 复制代码
复杂度分析:
- 时间复杂度:链表的节点最多访问一次,时间复杂度为O(n)
- 空间复杂度:哈希表空间复杂度为O(n)
解法2:Floyd 判圈算法
class Solution { fun hasCycle(head: ListNode?): Boolean { var slow = head var fast = head while (null != fast && null != fast.next) { slow = slow!!.next fast = fast.next!!.next if (slow == fast) { return true } } return false } } 复制代码
复杂度分析:
- 时间复杂度:O(n)O(n)O(n)
- 空间复杂度:只是用了常量级别变量,空间复杂度为O(1)
3. 判断链表成环起点
142. Linked List Cycle II 环形链表 II【题解】
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。不允许修改给定的链表。
解法1:哈希表
与 第二节类似,略
解法2:Floyd 判圈算法
class Solution { fun detectCycle(head: ListNode?): ListNode? { // 1. 寻找双指针相交位置 var slow = head var fast = head var intersect: ListNode? = null while (null != fast && null != fast.next) { slow = slow!!.next fast = fast.next!!.next if (slow == fast) { intersect = slow break } } // 2. 寻找成环位置 if (null == intersect) { return null } else { var p = intersect var q = head while (p != q) { p = p!!.next q = q!!.next } return p } } } 复制代码
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:只是用了常量级别变量,空间复杂度为O(1)
4. 旋转链表
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
解法1:链表成环
先将链表闭合成环,再找到新表头,断开前驱节点的连接。需要注意 k 大于链表长度的情况
class Solution { fun rotateRight(head: ListNode?, k: Int): ListNode? { if (null == head || null == head.next || 0 == k) { return head } var cur = head // 1. 找到尾节点 var count = 1 while (null != cur!!.next) { cur = cur.next count++ } if (0 == k % count) { // 旋转后未变化 return head } // 2. 链表成环 cur.next = head // 3. 找到倒数第 k 个节点的前驱节点 cur = head for (index in 0 until count - (k % count) - 1) { cur = cur!!.next } // 4. 断开 preK val preK = cur val kP = preK!!.next preK.next = null return kP } } 复制代码
复杂度分析:
- 时间复杂度:扫描两趟,时间复杂度为 O(n)
- 空间复杂度:使用了常量级别变量,空间复杂度为 O(1)
5. (扩展) 判断环形循环数组
457. Circular Array Loop 环形循环数组【题解】
给定一个含有正整数和负整数的环形数组 nums。 如果某个索引中的数 k 为正数,则向前移动 k 个索引。相反,如果是负数 (-k),则向后移动 k 个索引。因为数组是环形的,所以可以假设最后一个元素的下一个元素是第一个元素,而第一个元素的前一个元素是最后一个元素。
确定 nums 中是否存在循环(或周期)。循环必须在相同的索引处开始和结束并且循环长度 > 1。此外,一个循环中的所有运动都必须沿着同一方向进行。换句话说,一个循环中不能同时包括向前的运动和向后的运动。
解法1:哈希表 + 记忆化
class Solution { fun circularArrayLoop(nums: IntArray): Boolean { val visits = BooleanArray(nums.size) { false } fun isCircle(from: Int): Boolean { if (visits[from]) { return false } val set = HashSet<Int>() var cur = from while (true) { // 路径标志 visits[cur] = true val nextIndex = nums.jump(cur) if (nums[nextIndex] * nums[cur] < 0) { // 逆向 return false } if (set.contains(cur)) { return cur != nextIndex } set.add(cur) cur = nextIndex } } for (index in 0 until nums.size) { if (isCircle(index)) { return true } } return false } /** * @return index of next jump */ fun IntArray.jump(curIndex: Int): Int { val nextIndex = curIndex + this[curIndex] % size if (nextIndex < 0) { return size + nextIndex } if (nextIndex >= size) { return nextIndex - size } return nextIndex } } 复制代码
复杂度分析:
- 时间复杂度:每个位置最多访问一次,时间复杂度为O(n)
- 空间复杂度:哈希表为O(n) 路径标志为O(n),总体为O(n)