算法面试题 | 链表相交 & 成环问题

简介: 算法面试题 | 链表相交 & 成环问题

前言


  • 链表相交 & 成环问题可以归为一类问题,在面试中出现频率较高;
  • 在这篇文章里,我将梳理链表相交 & 成环问题的解法。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


系列文章



延伸文章



相关问题 提示 & 题解
160. 相交链表
Intersection of Two Linked Lists
【题解】
141. 环形链表
Linked List Cycle
【题解】
142. 环形链表 II
Linked List Cycle II
【题解】
61. 旋转链表
Rotate List
【题解】
(扩展)457. 环形循环数组
Circular Array Loop
【题解】


目录


image.png


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. 旋转链表


61. Rotate List 旋转链表【题解】


给定一个链表,旋转链表,将链表每个节点向右移动 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)
目录
打赏
0
0
0
0
44
分享
相关文章
|
10天前
|
面试场景题:如何设计一个抢红包随机算法
本文详细解析了抢红包随机算法的设计与实现,涵盖三种解法:随机分配法、二倍均值法和线段切割法。随机分配法通过逐次随机分配金额确保总额不变,但易导致两极分化;二倍均值法优化了金额分布,使每次抢到的金额更均衡;线段切割法则将总金额视为线段,通过随机切割点生成子金额,手气最佳金额可能更高。代码示例清晰,结果对比直观,为面试中类似算法题提供了全面思路。
64 15
|
19天前
|
员工电脑监控系统中的 C# 链表算法剖析-如何监控员工的电脑
当代企业管理体系中,员工电脑监控已成为一个具有重要研究价值与实践意义的关键议题。随着数字化办公模式的广泛普及,企业亟需确保员工对公司资源的合理利用,维护网络安全环境,并提升整体工作效率。有效的电脑监控手段对于企业实现这些目标具有不可忽视的作用,而这一过程离不开精妙的数据结构与算法作为技术支撑。本文旨在深入探究链表(Linked List)这一经典数据结构在员工电脑监控场景中的具体应用,并通过 C# 编程语言给出详尽的代码实现与解析。
37 5
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
97 29
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
在数字化办公时代,公司监控上网软件成为企业管理网络资源和保障信息安全的关键工具。本文深入剖析C++中的链表数据结构及其在该软件中的应用。链表通过节点存储网络访问记录,具备高效插入、删除操作及节省内存的优势,助力企业实时追踪员工上网行为,提升运营效率并降低安全风险。示例代码展示了如何用C++实现链表记录上网行为,并模拟发送至服务器。链表为公司监控上网软件提供了灵活高效的数据管理方式,但实际开发还需考虑安全性、隐私保护等多方面因素。
27 0
公司监控上网软件架构:基于 C++ 链表算法的数据关联机制探讨
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
125 25
|
5月前
|
❤️算法笔记❤️-(每日一刷-141、环形链表)
❤️算法笔记❤️-(每日一刷-141、环形链表)
82 0
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
113 16
美团面试:百亿级分片,如何设计基因算法?
40岁老架构师尼恩分享分库分表的基因算法设计,涵盖分片键选择、水平拆分策略及基因法优化查询效率等内容,助力面试者应对大厂技术面试,提高架构设计能力。
美团面试:百亿级分片,如何设计基因算法?
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法

热门文章

最新文章