Java 算法篇-链表的经典算法:判断回文链表、判断环链表与寻找环入口节点(“龟兔赛跑“算法实现)

news2024/11/13 14:56:00

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍
 

 

 

文章目录

        1.0 链表的创建

        2.0 判断回文链表说明

        2.1 快慢指针方法

        2.2 使用递归方式实现反转链表方法

        2.3 实现判断回文链表 - 使用快慢指针与反转链表方法

        3.0 判断环链表说明

        3.1 实现判断环链表与寻找环入口节点 - "龟兔赛跑"算法实现

        3.2 解释为什么第一次相遇后,兔、龟每一次都走一步最终会相遇且该节点是环入口节点的原因

        4.0 实现判断回文链表、判断环链表且寻找环入口节点的完整代码


 

        1.0 链表的创建

        链表是一种常见的数据结构,用于存储一系列元素。链表由节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。链表可以分为单向链表和双向链表,其中单向链表的节点只有一个指针指向下一个节点,而双向链表的节点有两个指针,分别指向前一个节点和后一个节点。        

        为后续实现算法方便,这里需要实现一个带哨兵节点的单链表

代码如下:

import java.util.Iterator;

public class List implements Iterable<Integer>{
    private final Node sentry;

    static class Node {
        public int value;
        public Node next;

        public Node() {
        }

        public Node(int value, Node next) {
            this.value = value;
            this.next = next;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value + "}";
        }
    }

    //外部类构造器,初始化哨兵节点
    public List() {
        sentry = new Node(-1,null);
    }

    //头插节点
    public void addFirst(int value) {
        this.sentry.next = new Node(value,this.sentry.next);
    }

    //尾插节点
    public void addLats(int value) {
        Node p = this.sentry;
        while (p.next != null) {
            p = p.next;
        }
        p.next = new Node(value,null);
    }

    //重写迭代器
    @Override
    public Iterator<Integer> iterator() {

        return new Iterator<Integer>() {
            Node p = sentry.next;

            @Override
            public boolean hasNext() {
                return p != null;
            }

            @Override
            public Integer next() {
                int k = p.value;
                p = p.next;
                return k;
            }
        };
    }

}

        简单对以上代码进行分析:将链表进行封装成一个外部类静态内部类则是节点类进行封装。外部类的成员变量为一个哨兵节点,内部类的成员变量为 int value 值Node next 指向下一个节点的引用变量。外部类实现了头插节点尾插节点重写了迭代器等。

需要了解可以点击该链接:Java 数据结构篇-实现单链表核心API-CSDN博客

 

        2.0 判断回文链表说明

        回文链表是指一个链表从头到尾和从尾到头读是一样的,也就是说,链表的节点值按照顺序排列和逆序排列是相同的。例如,链表 1 -> 2 -> 3 -> 2 -> 1 就是一个回文链表,因为从头到尾读和从尾到头读都是 1 -> 2 -> 3 -> 2 -> 1。

        2.1 快慢指针方法

        实现判断回文链表时需要用到快慢指针方法来寻找中间节点

        具体思路:实现快慢指针找中间节点,定义两个指针,对于 fast 指针来说,每一次循环都要走两步,直到 fast == null 或者 fast.next == null,遇到这两种情况都要结束循环了,注意不要缺少了 fast.next == null 的情况,不然有可能抛出 "空指针异常" ;对于 slow 指针来说,每一次循环都要走一步,直到退出循环后,若链表的节点的数量为奇数时,则指向的节点就是中间节点。

        若链表的节点的数量为偶数时,则指向的节点是中间两个节点的后一个节点。例如链表 1 -> 2 -> 3 -> 3 -> 2 -> 1 -> null,此时循环结束后,slow 指针指向的是靠后面值为 3 的节点。

代码如下:

    //查找链表中的中间的节点(快慢指针):假如为奇数,则需要找到中间的节点;
    // 假如是偶数,则需要找到中间的两个节点的后一个节点。
    public Node searchMidNode() {
        //判断是否为空链表
        if (this.sentry.next == null) {
            return null;
        }

        Node fast = this.sentry.next;
        Node slow = this.sentry.next;
        while (fast!= null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

        2.2 使用递归方式实现反转链表方法

        实现判断回文链表时需要实现反转链表。

        具体思路:先考虑递出的终止条件为:当 p.next == null 时,则返回 p 这个节点。再考虑在回归的过程中,需要将该 p 节点一直回归到回归过程结束为止。还需要将每一个节点都需要反转一下,p.next.next = p,注意这里需要将 p.next "暂时" 置为 nullp.next = null,否则会陷入死循环中。

代码如下:

    //用递归实现链表反转
    public Node reverseRecursion(Node p) {
        if (p.next == null) {
            return p;
        }

        Node last = reverseRecursion(p.next);
        p.next.next = p;
        p.next = null;
        return last;
    }

         用递归实现链表反转是其中一种的方法,还有四种方法可以实现链表反转,需要了解可以点击一下链接:Java 算法篇-深入了解单链表的反转(实现:用 5 种方式来具体实现)-CSDN博客 

        2.3 实现判断回文链表 - 使用快慢指针与反转链表方法

        具体思路为:先找到链表中的中间节点,例如链表 1 -> 2 -> 3 -> 2 -> 1 -> null ,需要先找节点值为:3 的节点,可以用快慢指针来实现找中间节点。然后将该节点后面的链表( 3 -> 2 -> 1 -> null )进行反转,可以用递归来实现反转的链表,得 1 -> 2 -> 3 -> null 。接着,用旧链表进行与反转后的链表遍历比较,若出现不相同值的节点,则判断该链表不是回文链表;若遍历完都没有返回 false ,则判断该链表为回文链表。

代码如下:

    //查找链表中的中间的节点(快慢指针):假如为奇数,则需要找到中间的节点;
    // 假如是偶数,则需要找到中间的两个节点的后一个节点。
    public Node searchMidNode() {
        //判断是否为空链表
        if (this.sentry.next == null) {
            return null;
        }

        Node fast = this.sentry.next;
        Node slow = this.sentry.next;
        while (fast!= null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }


    //用递归实现链表反转
    public Node reverseRecursion(Node p) {
        if (p.next == null) {
            return p;
        }

        Node last = reverseRecursion(p.next);
        p.next.next = p;
        p.next = null;
        return last;
    }



    //判断是否为回文链表
    public boolean isPalindromeList() {
        Node p = this.sentry.next;

        //需要先找到中间节点
        Node midNode = this.searchMidNode();
        //然后将中间节点往后的链表进行反转,反转可以用递归的方法。
        Node newMidNode = reverseRecursion(midNode);
        //接下来就要对旧节点的前半段链表进行循环遍历来比较了每一个节点的值是否相同了
        //当且仅当,当迭代到反转后的链表的最后一个为 null 时,结束循环
        while (newMidNode != null) {
            if (p.value != newMidNode.value) {
                return false;
            }
            p = p.next;
            newMidNode = newMidNode.next;
        }
        return true;

    }

        需要注意的是,对与 p 链表来说,一旦实现了链表反转, p 自身的链表会改变。反转之后的链表 newMidNode == null 时,就该结束循环了。而不能以 p == null 作为结束循环条件,原因是当链表的节点为偶数时,那么反转后的链表会比 p 链表少一个节点,假如用 p == null 作为结束循环的条件,那么当链表的节点数为偶数时,肯定会报 "空指针异常",所以需要以 newMidNode == null 作为循环结束条件

        3.0 判断环链表说明

        环链表是指链表中至少有一个节点的 next 指针指向了链表中的一个已经存在的节点,使得链表中存在环形结构。换句话说,链表中的一个节点的 next 指针指向了之前的某个节点,导致链表中存在环。

        3.1 实现判断环链表与寻找环入口节点 - "龟兔赛跑"算法实现

        具体思路:先来判断是否为环链表,可以比作为龟与兔的实际情景,当龟每一次走一步时,兔每一次走两步。即在相同时间下,兔所走的路程时龟的两倍

        情况一:当兔第一次没有追上龟时,则不是环链表,直接返回 null 。

        情况二:当兔第一次追上了龟时,可以判断为该链表为环形链表。接着寻找环入口,步骤为:可以借助兔子来记录第一次相遇的节点,对于龟来说,移到头节点开始一步步走,同时,兔子这次也是一步步走,当他们第二次相遇时,当前节点就为环入口节点。

代码如下:

    //判断是否闭环,如果是返回,则返回换入口;如果不是,则返回 null
    public Node isLoop() {
        Node fast = this.sentry.next;
        Node slow = this.sentry.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;


            if (slow == fast) {
                slow = this.sentry.next;

                //特例:当链表成为一个大环的时候(头尾相连),则直接返回

                //再相遇即为换入口节点
                while (true) {
                    if (slow == fast) {
                        return slow;
                    }
                    slow = slow.next;
                    fast = fast.next;

                }
            }
        }
        //从循环出来的不是闭环
        return null;
    }

        需要注意的是,当该链表是首尾相连时,第一次相遇时,不用再走第二次了,因为此时正好是环入口节点,直接返回当前节点。因此第一次相遇之后,将龟移到头节点处,接着就要判断此时龟与兔此时是否为同一个节点。否则,将龟移到头节点处后,没有先判断龟与兔是否为同一个节点,而将龟、兔同时走向下一步时,就会进入判断 if(slow == fast),返回已经相对与环节点的下一个的节点。

        3.2 解释为什么第一次相遇后,兔、龟每一次都走一步最终会相遇且该节点是环入口节点的原因

        假设,起点到环入口点的距离为 a 个节点,n 为在环中转的圈数,k 为在圈中走的节点数(可以理解为不够一圈的余数)。可以得出一条公式:h = a + n 无论 n 为多少,h 都会刚好来到环入口处

        那么在龟、兔第一次相遇时,对于龟来说,走了 g = a + n1 + k,对于兔来说,走了 t = a + n2 + k,对于 n1 ,n2 来说是多少都不在乎,但是两者的 k 、a 是一样的。上面说到,在第一次相遇的时候,兔所走的距离恰好是龟的距离的两倍,则龟走的距离 = 兔走的距离 - 龟走的距离,由此可得,相当与将龟走的距离换算为圈数: g = t - g = n2 - n1 g = n3,n3 具体是多少圈不在乎,反正知道是走了圈数,那么结合 a + n 永远走到的是环入口节点,那么 n3 再加上 a 是不是也会走到环入口处?

        所以此时,利用兔在与龟的第一次相遇的节点,与龟重新移回头节点处,接着龟与兔每一次走一步,知道他们相遇时所在的节点即为环入口节点。

 

        4.0 实现判断回文链表、判断环链表且寻找环入口节点的完整代码

import java.util.Iterator;

public class List implements Iterable<Integer>{
    private final Node sentry;

    static class Node {
        public int value;
        public Node next;

        public Node() {
        }

        public Node(int value, Node next) {
            this.value = value;
            this.next = next;
        }

        @Override
        public String toString() {
            return "Node{" +
                    "value=" + value + "}";
        }
    }

    //外部类构造器,初始化哨兵节点
    public List() {
        sentry = new Node(-1,null);
    }

    //头插节点
    public void addFirst(int value) {
        this.sentry.next = new Node(value,this.sentry.next);
    }

    //尾插节点
    public void addLats(int value) {
        Node p = this.sentry;
        while (p.next != null) {
            p = p.next;
        }
        p.next = new Node(value,null);
    }

    //重写迭代器
    @Override
    public Iterator<Integer> iterator() {

        return new Iterator<Integer>() {
            Node p = sentry.next;

            @Override
            public boolean hasNext() {
                return p != null;
            }

            @Override
            public Integer next() {
                int k = p.value;
                p = p.next;
                return k;
            }
        };
    }

    //查找链表中的中间的节点(快慢指针):假如为奇数,则需要找到中间的节点;
    // 假如是偶数,则需要找到中间的两个节点的后一个节点。
    public Node searchMidNode() {
        //判断是否为空链表
        if (this.sentry.next == null) {
            return null;
        }

        Node fast = this.sentry.next;
        Node slow = this.sentry.next;
        while (fast!= null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }


    //判断是否为回文链表
    public boolean isPalindromeList() {
        Node p = this.sentry.next;

        //需要先找到中间节点
        Node midNode = this.searchMidNode();
        //然后将中间节点往后的链表进行反转,反转可以用递归的方法。
        Node newMidNode = reverseRecursion(midNode);
        //接下来就要对旧节点的前半段链表进行循环遍历来比较了每一个节点的值是否相同了
        //当且仅当,当迭代到反转后的链表的最后一个为 null 时,结束循环
        while (newMidNode != null) {
            if (p.value != newMidNode.value) {
                return false;
            }
            p = p.next;
            newMidNode = newMidNode.next;
        }
        return true;

    }

    //用递归实现链表反转
    public Node reverseRecursion(Node p) {
        if (p.next == null) {
            return p;
        }

        Node last = reverseRecursion(p.next);
        p.next.next = p;
        p.next = null;
        return last;
    }


    //判断是否闭环,如果是返回,则返回换入口;如果不是,则返回 null
    public Node isLoop() {
        Node fast = this.sentry.next;
        Node slow = this.sentry.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;


            if (slow == fast) {
                slow = this.sentry.next;

                //特例:当链表成为一个大环的时候(头尾相连),则直接返回

                //再相遇即为换入口节点
                while (true) {
                    if (slow == fast) {
                        return slow;
                    }
                    slow = slow.next;
                    fast = fast.next;

                }
            }
        }
        //从循环出来的不是闭环
        return null;
    }

}

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

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

相关文章

【数据结构初阶】双链表

双链表 1.双链表的实现1.1结口实现1.2申请结点1.3初始化双链表1.4打印双链表1.5尾插1.6尾删1.7头插1.8头删1.9计算大小1.10查找1.11pos位置插入1.12删除pos位置1.12删除双链表 全部码源 1.双链表的实现 1.1结口实现 #include<stdio.h> #include<stdlib.h> #inclu…

快速排序知识总结

快速排序思维导图&#xff1a; 快速排序算法模版&#xff1a; #include <iostream>using namespace std;const int N 1e5 10;int n; int q[N];void quick_sort(int q[], int l, int r) {if (l > r) return;int x q[(l r) / 2], i l - 1, j r 1;while (i < …

10 Redis的持久化

Redis支持RDB和AOF两种持久化机制 1、RDB(Redis DataBase) 是对命令的全量快照随着key的数量增大&#xff0c;那么写入磁盘的开销也会越来越大 2、RDB文件的生成是否会阻塞主线程 save: 使用save的方式会阻塞主线程&#xff0c;影响redis的性能 bgsave: 一般情况下不会阻塞…

J. Chem. Inf. Model. | 使用GRID描述符进行深度学习预测血脑屏障透过性

今天为大家介绍的是来自Simon Cross团队的一篇论文。深度学习方法能够自动从输入数据中提取相关特征并捕捉输入和输出之间的非线性关系。在这项工作中&#xff0c;作者提出了基于GRID的AI&#xff08;GrAId&#xff09;描述符&#xff0c;这是对GRID MIFs的简单修改&#xff0c…

Javaweb之Ajax的详细解析

1.1 Ajax介绍 1.1.1 Ajax概述 我们前端页面中的数据&#xff0c;如下图所示的表格中的学生信息&#xff0c;应该来自于后台&#xff0c;那么我们的后台和前端是互不影响的2个程序&#xff0c;那么我们前端应该如何从后台获取数据呢&#xff1f;因为是2个程序&#xff0c;所以…

记一次攻防实战渗透

经典开局一个登录框 由于漏洞应该还未修复。对于数据和相关网址打个码见谅一下 常规思路&#xff08;爆破&#xff09; 常规操作进行一波 尝试弱口令然后开始爆破 对于此种有验证码的爆破&#xff0c;可以借用一个bp插件。 captcha-killer-modified-jdk14.jar 具体使用我就…

读懂:“消费报销”模式新零售打法,适用连锁门店加盟的营销方案

读懂&#xff1a;“消费报销”模式新零售打法&#xff0c;适用连锁门店加盟的营销方案 引言&#xff1a;2023年的双十一已经落下帷幕&#xff0c;作为每年的经典电商促销节&#xff0c;今年已是第15个年头&#xff0c;但是今年各大电商平台却都是非常默契的&#xff0c;没有公布…

算法学习 day26

第二十六天 最大子数组和 53. 最大子数组和 - 力扣&#xff08;LeetCode&#xff09; 动态规划问题 class Solution {public int maxSubArray(int[] nums) {int len nums.length;int[] dp new int[len];dp[0] nums[0];int res dp[0];for(int i 1; i < len; i){dp[i] …

【封装UI组件库系列】搭建项目及准备工作

封装UI组件库系列第一篇搭建项目 前言 &#x1f31f;搭建项目 创建工程 基本结构 1.创建8个组件展示页面 ​ 2.配置路由文件router/index.js 3.页面布局 &#x1f31f;总结 前言 在前端开发中&#xff0c;大家可能已经用过各种各样的UI组件库了&#xff0c;现在市面上热…

ANSYS网格无关性检查

网格精度对应力结果存在很大的影响&#xff0c;有时候可以发现&#xff0c;随着网格精度逐渐提高&#xff0c;所求得的最大应力值逐渐趋于收敛。 默认网格&#xff1a; 从默认网格下计算出的应力云图可以发现&#xff0c;出现了的三处应力奇异点&#xff0c;此时算出的应力值是…

聊一聊go的单元测试

文章目录 概要一、测试框架1.1、testing1.2、stretchr/testify1.3、smartystreets/goconvey1.4、cweill/gotests 二、打桩和mock2.1、打桩2.2、mock2.2.1、mockgen 三、基准测试和模糊测试3.1、基准测试3.2、模糊测试 四、总结4.1、小结4.2、其他4.3、参考资料 概要 软件测试是…

vue3 ts vite 主题色功能

开发工具&#xff1a;vue3 ts vite 如上图&#xff0c;选择个颜色整个变化&#xff0c;如下图 默认主题为绿色 切换成其它色。 这里面的颜色块&#xff0c;你也可以给个取器色组件&#xff0c;可切换成任意色。切换时主要执行下方的方法&#xff0c;有兴趣可自己研究下。 /…

【运维篇】5.6 Redis server 主从复制配置

文章目录 0. 前言1. 配置方式步骤1: 准备硬件和网络步骤2: 安装Redis步骤3: 配置主服务器的Redis步骤4: 配置从服务器的Redis步骤5: 测试复制功能步骤6: 监控复制状态 2. 参考文档 0. 前言 在Redis运维篇的第5.6章节中&#xff0c;将讨论Redis服务器的主从复制配置。在开始之前…

根据nginx日志统计页面访问次数

静态页面部署在nginx上&#xff0c;页面只有查看下载功能。 需求是统计每条访问次数和下载次数&#xff0c;根据日志分析写了一个shell脚本&#xff0c;触发脚本后生成一个html可以远程查看统计的数量。 #!/bin/bash # nginx日志文件路径 LOG_FILE"/usr/local/nginx/l…

vue安装three.js并创建第一个入门场景

vue安装three.js&#xff0c;并创建第一个入门场景 安装three.js npm install --save three引入three.js import * as THREE from threethree.js结构 three.js坐标 创建一个场景 scene场景&#xff0c;camera相机&#xff0c;renderer渲染器 创建一个场景 this.scene new T…

B站短视频如何去水印?一键解析下载B站视频!

在浏览B站视频时&#xff0c;我们有时会遇到带有水印的场景。这些水印可能会干扰我们对视频内容的观看体验&#xff0c;特别是在全屏观看时。此外&#xff0c;当我们想要保存或分享这些视频时&#xff0c;水印也会成为一种障碍。因此&#xff0c;去除水印的需求就变得非常迫切。…

机器学习算法项目开发流程

机器学习算法是当今人工智能领域最重要的技术之一&#xff0c;它可以让计算机通过学习数据中的模式和规律来实现预测和决策。在实际应用中&#xff0c;开发一个成功的机器学习算法项目需要遵循一定的开发流程。本文将介绍一个常见的机器学习算法项目开发流程&#xff0c;帮助读…

公司电脑文件透明加密、防泄密管理软件系统

天锐绿盾数据透明加密系统是一款采用驱动层透明加密技术实现电子文件安全加密的防护产品&#xff0c;可以对企业电子文件的存储、访问、传播和处理过程实施全方位保护。该系统遵循基于文件生命周期安全防护的思想&#xff0c;集成了密码学、访问控制和审计跟踪等技术手段&#…

22年+21年 计算机能力挑战赛初赛C语言程序题 题解

22年 第14题&#xff1a;答案&#xff1a;33 #include<stdio.h> int x1; int f(int a) { static int x2;int n0;if(a%2){ static int x3;nx; }else { static int x5;nx; }return nx;} void main() { int sumx,i;for(i0;i<4;i) sumf(i); printf(&qu…

百云齐鲁 | 云轴科技ZStack成功实践精选(山东)

山东省作为我国重要的工业基地和北方地区经济发展的战略支点&#xff0c;在“十四五”规划中将数字强省建设分为数字基础设施、数字科技、数字经济、数字政府、数字社会、数字生态六大部分&#xff0c;涵盖政治、经济、民生等多个方面&#xff0c;并将大数据、云计算、人工智能…