【算法自由之路】快慢指针在链表中的妙用(下篇)

news2024/12/24 20:51:55

【算法自由之路】快慢指针在链表中的妙用(下篇)

继上篇之后,链表这块还有两个相对较难的问题我们继续举例。

问题 1 给定具有 random 指针的 next 方向无环单链表,复制该链表

单听这个问题可能有点懵,这个链表结构我先给出

    private static class ListNode {
        int val;
        ListNode next;
        ListNode random;

        public ListNode(int val) {
            this.val = val;
        }
    }

注意这里有个要求 random 可以指向 null 或者链表内任意一个节点即 next 这个无环链上的任意一个节点

在不考虑空间复杂度的情况下,一个简单的思路,借助哈希表,在 next 方向上遍历链表并克隆节点,将原节点和克隆节点的映射存入 Map,即 Map<ListNode OldNode, ListNode CopyNode> 再次遍历链表,构建新链表的指针指向即可。时间复杂度 O(N) 空间复杂度 O(N)

舍弃哈希表的方法可以将空间复杂度优化为 O(1), 整体思路为:

  1. 将原链表复制,其复制节点挂到原节点的 next 指针上,人为构建一个规律性结构
  2. 遍历新链表,将复制节点的 random 指针设置正确
  3. 拆分新旧链表,恢复 next 指针的指向
package algorithmic.base;

// 随机链表复制
public class RandomListCopy {

    private static class ListNode {
        int val;
        ListNode next;
        ListNode random;

        public ListNode(int val) {
            this.val = val;
        }
    }

    public static ListNode copy(ListNode head) {

        if (head == null) {
            return null;
        }
        ListNode temp = head;
        // 首先沿着 next 将每个节点复制,并挂载在原节点的下一个
        while (temp != null) {
            ListNode next = temp.next;
            ListNode copyNode = new ListNode(temp.val);
            copyNode.next = next;
            temp.next = copyNode;
            temp = next;
        }
        // 调整复制节点的 random 指向
        temp = head;
        while (temp != null) {
            ListNode next = temp.next.next;
            temp.next.random = temp.random == null ? null : temp.random.next;
            temp = next;
        }
        // 拆分链表
        temp = head;
        ListNode copyHeadTemp = head.next;
        ListNode copyHead = head.next;
        while (copyHeadTemp != null) {
            ListNode next = copyHeadTemp.next;
            if (next == null) {
                temp.next = null;
                break;
            }
            ListNode copyNext = next.next;
            temp.next = next;
            copyHeadTemp.next = copyNext;
            temp = next;
            copyHeadTemp = copyNext;
        }

        return copyHead;
    }

    public static void outPrintLink(ListNode listNode) {
        while (listNode != null) {
            System.out.print(" c:" + listNode.val);
            if (listNode.random != null)
                System.out.print(" r:" + listNode.random.val);
            listNode = listNode.next;
        }
        System.out.println();
    }

    public static void main(String[] args) {
        ListNode a = new ListNode(1);
        ListNode b = new ListNode(2);
        ListNode c = new ListNode(3);
        a.next = b;
        a.random = b;
        b.random = c;
        b.next = c;
        c.random = a;
        ListNode copy = copy(a);
        outPrintLink(a);
        outPrintLink(copy);
    }

}

问题 2 给定两个有环或者无环的单链表,判如果两个链表相交返回第一个相交点,如果不相交返回 null

要求如果两个链表长度相加为 N 则时间复杂度为 O(N) 空间复杂度 O(1)

这是一道非常好的分类讨论问题,我们在思考的时候容易进入误区,会想象出一些本不存在的链表结构,比如链表有环,如果有环则一定不会出现中间成环后又伸出一个尾巴的情况

在这里插入图片描述

  1. 如果是两个无环链表那么它有两种情况: 相交或者不相交。

  2. 如果是一个有环链表和一个无环链表那他们一定不相交。

  3. 如果是两个有环链表则需要讨论:相交或者不相交。 如果相交需要讨论 相交点是否是一个点。

在这里插入图片描述

如何判断有环? 快慢指针,追击问题,如果快指针最终能追上慢指针则有环

**进一步,如果有环返回第一个入环节点,否则返回 null?**在快慢指针相遇时,快指针返回头部一次走一步,慢指针在相遇位置继续一次走一步,下一次相遇则为入环节点

**判断两个无环单链表是否相交?**链表尾节点是否相同

package algorithmic.base;

// 两个链表相交问题
public class TwoListCross {

    public static class ListNode {
        int val;
        ListNode next;

        public ListNode(int val) {
            this.val = val;
        }

        @Override
        public String toString() {
            return "ListNode{" +
                    "val=" + val +
                    '}';
        }
    }

    // 给定两个链表头节点,判断是否相交,如果相交返回第一个相交节点,否则返回 null
    public static ListNode isCross(ListNode head1, ListNode head2) {
        if (head1 == null || head2 == null) {
            return null;
        }
        ListNode ring1 = haveRing(head1);
        ListNode ring2 = haveRing(head2);
        // 两个无环链表相交问题
        if (ring1 == null && ring2 == null) {
            ListNode temp1 = head1;
            ListNode temp2 = head2;
            int count1 = 1;
            int count2 = 1;
            // 取两个链表最后一个节点,并计数
            while (temp1.next != null) {
                count1++;
                temp1 = temp1.next;
            }
            while (temp2.next != null) {
                count2++;
                temp2 = temp2.next;
            }
            // 尾节点不同说明不相交
            if (temp1 != temp2) {
                return null;
            }
            ListNode longList = count1 > count2 ? head1 : head2;
            ListNode shortList = count1 > count2 ? head2 : head1;
            int needFirstGo = Math.abs(count1 - count2);
            // 因为相交最后一定共用,长链表先走差值步,然后一起走,首次相遇点返回即可
            for (int i = 0; i < needFirstGo; i++) {
                longList = longList.next;
            }
            while (longList != shortList) {
                longList = longList.next;
                shortList = shortList.next;
            }
            return longList;

        } else if (ring1 != null && ring2 != null) {
            if (ring1 == ring2) {
                return ring1;
            } else {
                ListNode temp = ring1.next;
                while (temp != ring1) {
                    if (temp == ring2) {
                        // 如果是有环相交,两个链表入环点不同则返回任意一个都可以
                        return ring2;
                    }
                    temp = temp.next;
                }
                // ring1 走了一圈都没有遇到 ring2 说明两个有环链表不相交
                return null;
            }
        } else {
            return null;
        }
    }

    private static ListNode haveRing(ListNode head) {

        if (head == null || head.next == null || head.next.next == null) {
            return null;
        }

        ListNode slow = head.next;
        ListNode fast = head.next.next;
        while (slow != fast) {
            // 若有环,相交必追上
            if (fast.next == null) {
                return null;
            }
            slow = slow.next;
            fast = fast.next.next;
        }
        fast = head;
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }

        return fast;
    }

    public static void main(String[] args) {
        ListNode a = new ListNode(1);
        ListNode b = new ListNode(2);
        ListNode c = new ListNode(3);
        ListNode d = new ListNode(4);
        ListNode e = new ListNode(5);
        a.next = b;
        b.next = c;
        c.next = d;
        d.next = e;
        e.next = null;

        ListNode a2 = new ListNode(10);
        ListNode b2 = new ListNode(20);
        ListNode c2 = new ListNode(30);
        ListNode d2 = new ListNode(40);
        ListNode e2 = new ListNode(50);
        a2.next = b2;
        b2.next = c2;
        c2.next = d2;
        d2.next = e2;
        e2.next = null;
        // 无环无相交
        System.out.println(isCross(a, a2));
        // 无环有相交
        c.next = d2;
        System.out.println(isCross(a, a2));
        // 有环有无相交
        c.next = b;
        System.out.println(isCross(a, a2));
        // 有环有无相交2
        c2.next = b2;
        System.out.println(isCross(a, a2));
        // 有环同节点相交
        c2.next = b;
        System.out.println(isCross(a, a2));
        // 有环不同节点相交
        c.next = d;
        e.next = b;
        c2.next = c;
        System.out.println(isCross(a, a2));
    }

}

很有意思的一个问题,这里我重新总结一下考点

  1. 分类讨论:难点在于考虑清楚可能存在的情况,不要被进入思维误区考虑本不存在的可能性。
  2. 如何判断链表有环并返回入环第一个节点:有环可以使用快慢指针的追击,如果能追上说明一定有环,第一个入环节点是经典追击问题,快指针追击到后回到起点于慢指针保持同一速度,再次相遇即为首次入环点。
  3. 单链表相交如何确定第一个入环节点:首先借助 Set 结构是简单就能做到的,首先将一个链表的节点全部加入 Set ,遍历第二个链表,首次命中 Set 的即为所求。这个空间不省,省空间的做法是,因为相交最后一定共用,长链表先走差值步,然后一起走,首次相遇点返回即可

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/148526.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

PCB封装创建(CHIP类)

PCB封装要有以下内容 PCB焊盘管脚序号丝印阻焊1脚标识目录 CHIP类&#xff08;电阻 电容 电感 三极管&#xff09; 0805C 0805R 0805L SOT-23 1.CHIP类&#xff08;电阻 电容 电感 三极管&#xff09; 1.新建一个PCB元件库 打开PCB Library 以下以0805为例。 创建080…

“CAcModuleResourceOverride”: 未声明的标识符

本文迁移自本人网易博客&#xff0c;写于2011年10月8日首先是运行时提示&#xff1a;试图执行系统不支持的操作。添加CAcModuleResourceOverride resourceOverride; 后&#xff0c;编译出现如下错误&#xff1a;error C2065: “CAcModuleResourceOverride”: 未声明的标识符 添…

scikit-learn 普通最小二乘法

scikit-learn 普通最小二乘法什么是普通最小二乘法&#xff1f;参考文献什么是普通最小二乘法&#xff1f; 线性回归模型的数学表达式如下&#xff1a; y^(w,x)w0w1x1…wpx1\hat{y}(w, x)w_{0}w_{1} x_{1}\ldotsw_{p} x_{1}y^​(w,x)w0​w1​x1​…wp​x1​ 其中 w0,w1,...,w…

Java--集合

1、集合框架 集合框架被设计成要满足以下几个目标。 该框架必须是高性能的。基本集合&#xff08;动态数组&#xff0c;链表&#xff0c;树&#xff0c;哈希表&#xff09;的实现也必须是高效的。 该框架允许不同类型的集合&#xff0c;以类似的方式工作&#xff0c;具有高度的…

【自用】高频电子线路复习(更新中)

疫情原因 没有考试就放假回家了 返校后将先进行死亡考试周 七天考完九门 回校再进行极限复习只能说可以通过 而不利于绩点的提升 所以要从现在开始抽取一些时间进行学习 第七章 频率变换方法与电路分析 7.1 非线性电路包括 发送端的高频振荡器、倍频器、谐振功率放大器和调…

【ROS自定义文件】自定义头文件及源文件的调用

本文记录ROS中的自定义文件的调用&#xff0c;主要包括自定义头文件和源文件的使用。 1 自定义C头文件的调用 注意这个文件目录的结构&#xff0c;尤其是 hello.h 这个自定义的头文件在 include/plumbing_head 文件夹之下&#xff0c;这个会直接影响后续头文件的引用。 hello.…

尚医通-整合网关-Nuxt搭建前端环境(二十六)

目录&#xff1a; &#xff08;1&#xff09;整合服务网关 &#xff08;2&#xff09;前台用户系统-nuxt搭建前端环境 &#xff08;3&#xff09;前台用户系统-目录结构和封装axios &#xff08;1&#xff09;整合服务网关 前面的过程使用nginx请求转发 下面使用SpringClo…

ScheduledThreadPoolExecutor定时任务执行线程池分析

概述 ScheduledThreadPoolExecutor自然是继承了ThreadPoolExecutor&#xff0c;那么它也就是一个被定义了特定功能的线程池而已&#xff0c;本质上就是一个ThreadPoolExecutor。 代码分析 可以看到其继承了ThreadPoolExecutor&#xff0c;在new ScheduledThreadPoolExecutor…

【FPGA】Verilog 编码实现:与非门 | 或非门 | 异或门 | NAND/NOR/XOR 行为验证

写在前面&#xff1a;本章主要内容为了解和确认 NAND/NOR/XOR 门的行为&#xff0c;并使用Verilog实现&#xff0c;生成输入信号后通过模拟&#xff0c;验证每个门的操作&#xff0c;并使用 FPGA 来验证 Verilog 实现的电路的行为。 本章目录&#xff1a; Ⅰ. 前置知识 0x00…

C++ 排序大合集

目录 一、了解排序 1、内部 2、外部 二、排序的稳定性 三、插入排序 1、算法和操作 2、代码 四、选择排序 1、算法和操作 2、代码 五、冒泡排序 1、算法和操作 2、代码 六、堆排序 1、优先队列 2、排序代码 七、归并排序 1、定义 2、基本算法 &#xff08;1&#xff09;、分离 …

宝塔Linux面板安装MySQL数据库,并且开启远程链接

1.宝塔面板【软件商店】->【应用搜索】&#xff0c;搜索MySQL,然后点击安装想要的版本&#xff0c;我这边是安装的5.6版 2. 安装完后重置数据库管理员密码 3.Navicat Premium 15连接数据库 4.外网navicat工具无法连接数据库的处理办法 4.1输入 mysql -u root -p 后回车&a…

零基础入门反序列化漏洞

目录 前提知识 漏洞产生原理 常见的函数 序列化 反序列化 __sleep函数 私有和保护 __wakeup函数 反序列化漏洞举例 构造XSS漏洞 反序列化免杀后门 POP CHAIN(POP链) 前提知识 漏洞产生原理 serialize() 和 unserialize() 在 PHP内部实现上是没有漏洞的&#xf…

Cadence PCB仿真使用Allegro PCB SI配置电路板层叠结构的方法图文教程

⏪《上一篇》   🏡《总目录》   ⏩《下一篇》 目录 1,概述2,配置方法3,总结1,概述 本文详细介绍使用Allegro PCB SI软件配置电路板层叠结构的方法。 2,配置方法 第1步:打开待仿真的PCB文件,并确认软件为Allegro PCB SI 如果,打开软件不是Allegro PCB SI则可这样…

解决No module named tkinter

原因 今天准备使用tutle画个图&#xff0c;导入turtle后运行发现提示没有tkinter这个包&#xff0c;于是尝试pip install tkinter安装&#xff0c;结果当然是失败&#xff1a; 后面一番搜索之后发现tinter是python3自带的包&#xff0c;不能用pip安装&#xff0c;我这里安装的…

JS的六种继承方式

继承 什么是继承&#xff1f; JS里的继承就是子类继承父类的属性和方法 目的可以让子类的实例能够使用父类的属性和方法 抽象的表达就是&#xff1a;一个人有车&#xff0c;有房&#xff0c;那么他的儿子也可以去使用他的车子&#xff0c;住他的房子。 方法一&#xff1a;…

Seata流程源码梳理上篇-TM、RM处理

这一篇我们主要来分析下Seata的AT模式的流程处理。一、流程案例 1、案例源码 ​ 我们本地流程梳理用的是基于spring-cloud框架&#xff0c;注册中心是eurak&#xff0c;服务间调用的是feign&#xff0c;源码下载的是官网的&#xff08;当然你如果对dubbo更熟悉&#xff0c;也…

CSDN博客之星年度评选活动 - 2022

文章目录一、2022年CSDN博客之星评选活动报名二、2022年CSDN博客之星评选活动流程线上评分流程争议&#xff08;官方最后证实公布后会更新&#xff09;三、2022年CSDN博客之星评选规则四、2022年CSDN博客之星评分规则五、2022年CSDN博客之星活动奖品「博客之星」奖品「博客新星…

CInternetSession OpenURL没反应,不能捕获异常

本文迁移自本人网易博客&#xff0c;写于2013年10月22日CString sFileName;CInternetSession iSession;BOOL bRet FALSE;CStdioFile* pFileDown NULL;try{pFileDown iSession.OpenURL(szURL, 1, INTERNET_FLAG_TRANSFER_BINARY|INTERNET_FLAG_DONT_CACHE);}catch(...){CStri…

2023/1/8 Vue学习笔记-4-脚手架及相关属性配置

1 创建脚手架 &#xff08;1&#xff09;CLI就是 command line interface 的缩写。Vue CLI官网&#xff1a;Vue CLI &#xff08;2&#xff09;安装过程&#xff1a; &#xff08;PS&#xff1a; 提前安装过node.js了&#xff0c;没有安装的可以打开这个&#xff1a;Download …

什么是布隆过滤器?——超详细解析【建议收藏】

目录 1、什么是布隆过滤器&#xff1f; 2、实现原理 2.1、回顾哈希函数 2.1.1、哈希函数概念 2.1.2、散列函数的基本特性&#xff1a; 2.2、布隆过滤器数据结构 3、特点 3.1、支持删除吗&#xff1f; 3.2、优点 3.3、缺点 3.4、误判率 4、如何选择哈希函数个数和布…