链表算法题二,还原题目,用debug调试搞懂每一道题
文章简述
大家好,本篇是个人的第4篇文章。
承接第3篇文章《开启算法之路,还原题目,用debug调试搞懂每一道题》,本篇文章继续分享关于链表的算法题目。
本篇文章共有5道题目
一,反转链表(经典题目)
1.1.1 题目分析
反转链表是经典的题目,题中信息描述很清晰,给定一个单链表,将其反转。
先说说有什么思路呢?从题中给的案例输出结果看,是不是只需要将输入的链表的指针改成相反方向,就可以得到要输出的结果。
就好比如下图所示:
但是问题来了,我们是单链表,是没办法将下个节点直接指向该节点的上个节点。
因此就需要定义一个辅助指针,用来指向该节点的上个节点,这样就能完成,如下图所示。
那按照我们上面分析也就是将cur指针指向pre节点就可以了。
注意:此处有坑
当我们将当前节点【cur】指向上一个节点【pre】的时候,如何将指针向下移动呢?
此时的节点【cur】已经指向了上一个节点【pre】了,所以我们还需要一个临时变量去保存当前节点的下个节点,具体为什么这么做,我们在下面代码演示的时候debug看下过程。
接着我们上面的步骤,将指针向下移动,如图所示。
移动指针后,再将当前节点的next指针指向上一个节点。
最后当前节点没有下个节点的时候,就结束遍历,如图所示。
1.1.2 代码分析
按照套路,先初始化节点对象。
class ListNode { int val; ListNode next; ListNode() { } ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } @Override public String toString() { return "ListNode{" + "val=" + val + '}'; } }
创建单链表结构。
// 创建单链表 ListNode l1 = new ListNode(1); ListNode l2 = new ListNode(2); ListNode l3 = new ListNode(3); ListNode l4 = new ListNode(4); ListNode l5 = new ListNode(5); NodeFun nodeFun = new NodeFun(); nodeFun.add(l1); nodeFun.add(l2); nodeFun.add(l3); nodeFun.add(l4); // 返回创建的链表 ListNode node = nodeFun.add(l5);
反转链表的代码。
public ListNode reverseListIteration(ListNode head) { // 定义上节点辅助指针 ListNode pre = null; // 定义当前节点辅助指针 ListNode cur = head; // 循环当前节点不为空 while (null != cur) { // 临时变量保存当前节点的下个节点 ListNode temp = cur.next; // 当前节点的next指向上节点 cur.next = pre; // 上节点向下移动 pre = cur; // 当前节点指向下个节点 cur = cur.next; } return pre; }
1.1.3 debug调试
节点初始化完成了,按照分析我们定义了2个节点,如上图第一次遍历【pre】节点是null,【cur】从第一个节点开始。
下一步debug调试我们先不急,回顾之前说的一个问题,为什么要将当前节点的下一个节点用临时变量保存,那我们直接看debug调试。
第一次遍历的时候,修改完指针后当前节点已经指向上一个节点了,再看上述题目分析的图解。
这就是为啥要先把当前节点的下个节点缓存起来。
上图debug我们看出,【cur】当前节点的指针已经指向null,下一步就是移动指针指向下一个节点。
我们再接着进行debug调试,按照上述分析,第二步循环就是将节点【2】指向上一个节点【1】,如下图所示。
现在当前节点【cur】已经指向【2】,那它的下个节点就是【1】,如下图所示。
经过上面的两步循环,成功的将指针进行了反转,剩下的节点循环也就如出一辙了。
当循环到最后节点【5】时,下个节点为null,此时结束while循环,而节点【5】也是指向了上一个节点【4】。
最后我们再看下运行结果。
二,回文链表
1.2.1 题目分析
如果做过字符串的算法题,里面有个回文字符串的题目。没错,它俩的意思是一样的。
看题目描述得知一个链表是不是回文链表,就是看链表就是看链表正读和反读是不是一样的。
假如说,我们拿到了后半部分链表,再将其反转。去和链表的前半部分比较,值相等就是回文链表了。
注意:
这种方式会破坏原链表的结构,为保证题目的一致性,最后再将链表再重新拼接
另外一种解题方式为:将整个链表节点遍历保存到数组中,而数组是有下标,并可以直接获取数组的大小,那么只需从数组的首尾去判断即可
反转链表上一道题我们已经分享了,现在重点是如何获取后半部分的链表。
我们再说说快慢指针的思想,通常我们定义2个指针,一个移动快,一个移动慢。详细的案例可以参考本人上一篇文章《开启算法之路,还原题目,用debug调试搞懂每一道题》,有一道关于快慢指针的题目。
定义慢指针每次移动1个节点,快指针每次移动2个节点,当然我们是需要保证快节点的下下个
个节点不为空。
slow = slow.next; fast = fast.next.next;
其实快慢指针的思想就是,假设链表是一个回文链表,快指针比慢指针是多走一步,当快指针走完的时候,慢指针也就刚好走到该链表的一半。
上图中slow指针正好走到链表的一半,此时也就得到链表的后半部分了,即slow.next
。
1.2.2 代码分析
老套路,先创建一个回文链表。
ListNode l1 = new ListNode(1); ListNode l2 = new ListNode(2); ListNode l3 = new ListNode(2); ListNode l4 = new ListNode(1); NodeFun nodeFun = new NodeFun(); nodeFun.add(l1); nodeFun.add(l2); nodeFun.add(l3); ListNode head = nodeFun.add(l4);
获取后半部分链表代码。
private ListNode endOfFirstHalf(ListNode head) { ListNode fast = head; ListNode slow = head; while (fast.next != null && fast.next.next != null) { fast = fast.next.next; slow = slow.next; } return slow; }
反转链表的代码与上题目是一样的。
最后将两个链表进行判断是否是一样的。
// 判断是否回文 ListNode p1 = head; ListNode p2 = secondHalfStart; boolean flag = true; while (flag && p2 != null) { if (p1.val != p2.val) { flag = false; } p1 = p1.next; p2 = p2.next; }
1.2.3 debug调试
先获取链表的后半部分。
debug开始循环后,fast直接走到链表的第3个节点【2】
slow.next就是链表的后半部分,再将后半部分进行链表反转
最后我们也就得到如下2个链表。
最后将这2个链表进行比较是否相等,相等则是回文链表。
三,链表的中间节点
1.3.1 题目分析
获取链表的中间节点乍一看和回文链表中使用快慢指针获取后半链表有点类似呢?
没错,这波操作是类似的,但也并不是完全一样,其主要思想还是快慢指针。
换句话说,如果你已理解了上面的题,那这道题也就不是什么事了。话不多说,先来分析一波。
同样我们还是定义slow慢指针每次移动一个节点,fast快指针每次移动2个节点。
那么fast快指针移动到最后节点时,slow慢指针也就是要返回的链表。
我想,你是不是有个疑问。就是为什么慢指针是移动一个节点,快节点移动2个节点?如果是偶数个节点,这个规则还正确吗!那就验证下。
为了方便,就继续上面节点的遍历。
题目中描述,如果有2个中间节点,返回第二个节点
,所以返回节点【4,5,6】也就符合要求了
1.3.2 代码分析
创建链表结构。
ListNode l1 = new ListNode(1); ListNode l2 = new ListNode(2); ListNode l3 = new ListNode(3); ListNode l4 = new ListNode(4); ListNode l5 = new ListNode(5); NodeFun nodeFun = new NodeFun(); nodeFun.add(l1); nodeFun.add(l2); nodeFun.add(l3); nodeFun.add(l4); ListNode head = nodeFun.add(l5);
获取后半部分链表代码。
// 快慢指针 ListNode slow = head; ListNode fast = head; while(fast != null && fast.next != null){ //移动指针 fast = fast.next.next; slow = slow.next; } return slow;
1.3.3 debug调试
快指针移动到节点【3】,慢指针移动到节点【2】
接着再走一步,快指针移动到节点【5】,慢节点移动到节点【3】,到此也就满足题意的要求了。
四,链表中倒数第k个节点
1.4.1 题目分析
这道题要求就是返回倒数K个节点,最笨的办法就是参考上面链表反转,先将链表反转。获取前K个节点,将获取的节点再次进行反转即可得到题目要求。
但是显然这种方式只能满足答案输出,经过上面的3道题目,有没有得到什么启发呢?
是的,这道题依然可以使用双指针解决,是不是感觉双指针可以解决所有的链表问题了(QAQ)。
再仔细一想,是不是感觉和上一道《链表的中间节点》题目很类似?获取链表的中间节点是返回后半部分节点,而本道题是要求返回指定K个节点。
那就直接说结论吧,同样是定义快慢指针。只不过在上道题中快指针是每次移动2个节点,本道题中给定的K,就是快指针移动的节点个数。
同样初始化指针都在首节点,如果我们先将fast指针移动K个节点。
到此才算初始化节点完成,剩下的操作就是遍历剩下的链表,直到fast指针指向最后一个节点。
一直遍历到fast节点为null,此时返回slow指针所指引的节点。
1.4.2 代码分析
初始化链表,由于和前几道题的操作是一样的,此处就不在展示。
获取倒数第K个节点的代码。
public ListNode getKthFromEnd(ListNode head, int k) { ListNode slow = head; ListNode fast = head; // 先将快指针向前移动K while (k-- > 0) { fast = fast.next; } while (fast != null) { fast = fast.next; slow = slow.next; } return slow; }
1.4.3 debug调试
按照上面图解分析,fast快指针指向节点【3】的时候才算真正初始化快慢指针完成。
当快指针指向节点【5】时,slow慢节点指向节点【3】
注意:中间省略了一步,即慢指针指向节点【2】时,快指针指向节点【4】
节点【5】是最后一个节点,再次进入while循环。
最后一次循环时,慢指针指向了4,快指针下一个节点已经为null,此时结束循环。
五,移除重复节点
1.5.1 题目分析
这道题和上一篇中的题目【删除排序链表中的重复元素】是一样的,简单的做法即利用Set集合保存未重复的节点,再遍历链表判断是否已存在Set集合中。
因此本道题就不在多分析,直接贴上代码。
1.5.2 代码分析
Set<Integer> set = new HashSet<>(); ListNode temp = head; while(temp != null && temp.next != null){ set.add(temp.val); if(set.contains(temp.next.val)){ temp.next = temp.next.next; }else{ temp = temp.next; } } return head; }
六,总结
本次文章共分享总结5道题目,仔细分析有没有发现这些题套路都是一样的。都利用了双指针的思想,通过一定的规则移动快慢指针获取指定链表节点。
本次的5道题目和上次的3道题目,基本已经包含了链表简单题目的所有类型。当你把本篇文章的题目看完后,关于链表的简单题目你也已经做完了。
本人已经将链表的所有简单题目刷完,总结出来的结论即套路都是一样的。简单来说,大部分的题目都可以利用双指针,递归,数组来完成。
在下篇文章中会对链表的简单题目做一个小总结。
最后,求关注
原创不易,每一篇都是用心在写。如果对您有帮助,就请一键三连(关注,点赞,再转发)
我是杨小鑫,坚持写作,分享更多有意义的文章。
感谢您的阅读,期待与您相识!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
一文带你了解GaussDB(DWS) 的Roach逻辑备份实现原理
摘要:Roach工具是GaussDB(DWS)推出的一款主力的备份恢复工具,包含物理与逻辑备份两种主要能力,本文着重于讲解Roach逻辑备份的实现原理。 一、简介 在大数据时代,数据的完整和可靠性成为一个数仓最核心的能力之一。GaussDB(DWS)以其出众的分布式计算和存储能力广受用户青睐的同时,也特别着眼于数据备份容灾领域的创新和打磨。数据的可靠性可以说是数仓的“命门”。对于企业、政府等用户,如果因为硬件故障导致的文件损坏,或是业务操作的误删,导致了数据损坏或丢失,那么损失将是不可估量的。GaussDB(DWS)提供的Roach工具,将以其稳定、快速、可靠的备份能力,通过备份恢复数据库或业务表,为客户准备一个可靠的“后悔药”,从而有效地挽回客户损失。 二、Roach备份恢复基本框架 GaussDB(DWS)的Roach工具,同时提供物理备份和逻辑备份两种主要形态的备份。物理备份直接通过拷贝文件块,存储于备份介质之上,恢复时使用备份的文件块,重建集群中实例DN与CN的数据目录进行恢复。 本文中我们主要着眼于逻辑备份,在当前的GaussDB(DWS)中,相比于物理备份,逻辑备份拥有更好...
- 下一篇
测试人必会的 Mock Server 技能
这是无量测试之道的第193篇原创 Mock 是什么? Mock Server 是什么? Mock Server 的常用场景 Mock Server 搭建 1.借助第三方工具直接提供 Mock Server 2.自主编码实现 Mock Server(Flask) 总结 Mock 是什么? Mock 是模拟的意思。在测试中,通常表述为:对测试过程中不容易构造或者不容易获取的对象,用一个虚拟的对象来进行模拟的一个过程。 那么哪些对象不容易构造?哪些对象不容易获取呢? 拿微服务举例,在一个调用链条上,微服务 A 依赖 B 服务才能提供服务,而微服务 B 依赖 C 服务, 微服务 C 依赖 D 服务.....在这样的情况下,把每个依赖的服务都构造完毕再开始测试,就变得不太现实。这种情况我们称之为不容易构造。 又比如,假设我们的服务依赖银行的接口提供资金的查询。在测试中,银行不可能无条件或者随意提供接口给我们调用。那么,在我们开发完毕但是要依赖对方才能开始测试时,我们称这种情况为不容易获取。 无论是哪种情况,使用 Mock 的前提条件是:我们仅关注测试对象自身内部逻辑是否正确,而不关心其依赖对象逻...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7,CentOS8安装Elasticsearch6.8.6
- MySQL8.0.19开启GTID主从同步CentOS8
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS8安装Docker,最新的服务器搭配容器使用
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Hadoop3单机部署,实现最简伪集群
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7设置SWAP分区,小内存服务器的救世主