⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。
学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭将基于 Java / Kotlin 语言,为你分享常见的数据结构与算法问题,及其解题框架思路。
本文是数据结构与算法系列的第 2 篇文章,完整文章目录请移步到文章末尾~
前言
-
链表相交 & 成环问题可以归为一类问题,在面试中出现频率较高;
-
在这篇文章里,我将梳理链表相交 & 成环问题的解法。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
相关问题 | 提示 & 题解 |
---|---|
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
}
}
复杂度分析:
- 时间复杂度:两个链表的节点最多访问一次,时间复杂度为
- 空间复杂度:以其中一个链表构建哈希表,空间复杂度为
解法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
}
}
复杂度分析:
- 时间复杂度:两个链表的节点最多访问一次,时间复杂度为
- 空间复杂度:只是用了常量级别变量,空间复杂度为
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
}
}
复杂度分析:
- 时间复杂度:链表的节点最多访问一次,时间复杂度为
- 空间复杂度:哈希表空间复杂度为
解法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
}
}
复杂度分析:
- 时间复杂度:
- 空间复杂度:只是用了常量级别变量,空间复杂度为
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
}
}
}
复杂度分析:
- 时间复杂度:
- 空间复杂度:只是用了常量级别变量,空间复杂度为
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
}
}
复杂度分析:
- 时间复杂度:扫描两趟,时间复杂度为
- 空间复杂度:使用了常量级别变量,空间复杂度为
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
}
}
复杂度分析:
- 时间复杂度:每个位置最多访问一次,时间复杂度为
- 空间复杂度:哈希表为路径标志为,总体为
解法2:快慢指针
class Solution {
fun circularArrayLoop(nums: IntArray): Boolean {
val n = nums.size
val U = 1000 * n // 保证取模后不变
for (i in nums.indices) {
if (nums[i] > U) continue
// 快慢指针
var fast = nums.next(i)
var slow = i
// println("i=$i, slow=$slow, fast=$fast")
// -2,1,-1,-2,-2
while (nums[slow] * nums[fast] > 0 && nums[slow] * nums[nums.next(fast)] > 0) { // 往一个方向
nums[slow] += U
nums[fast] += U
slow = nums.next(slow)
fast = nums.next(nums.next(fast))
// println("slow=$slow, fast=$fast")
if (slow == fast) {
// [-1,-2,-3,-4,-5,6]
if (slow != nums.next(slow)) return true // 要求循环长度大于 1
break
}
}
}
return false
}
private fun IntArray.next(index : Int) : Int {
return ((index + this[index]) % size + size) % size
}
}
推荐阅读
数据结构与算法系列完整目录如下(2023/07/11 更新):
Java & Android 集合框架系列文章: 跳转阅读
LeetCode 上分之旅系列文章:跳转阅读
⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~