文章简述

大家好,本篇是个人的第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道题目,基本已经包含了链表简单题目的所有类型。当你把本篇文章的题目看完后,关于链表的简单题目你也已经做完了。

本人已经将链表的所有简单题目刷完,总结出来的结论即套路都是一样的。简单来说,大部分的题目都可以利用双指针,递归,数组来完成

在下篇文章中会对链表的简单题目做一个小总结。

最后,求关注

原创不易,每一篇都是用心在写。如果对您有帮助,就请一键三连(关注,点赞,再转发)

我是杨小鑫,坚持写作,分享更多有意义的文章。

感谢您的阅读,期待与您相识!

链表算法题二,还原题目,用debug调试搞懂每一道题的更多相关文章

  1. 链表算法题之中等级别,debug调试更简单

    文章简述 大家好,本篇是个人的第 5 篇文章 从本篇文章开始,分享关于链表的题目为中等难度,本次共有 3 道题目. 一,两数相加 1.1 题目分析 题中写到数字是按照逆序的方式存储,从进位的角度看,两 ...

  2. 开启算法之路,还原题目,用debug调试搞懂每一道题

    文章简述 大家好,本篇是个人的第 3 篇文章. 承接第一篇文章<手写单链表基础之增,删,查!附赠一道链表题>,在第一篇文章中提过,在刷算法题之前先将基础知识过一遍,这样对后面的做算法题是很 ...

  3. LeetCode 上最难的链表算法题,没有之一!

    题目来源于 LeetCode 第 23 号问题:合并 K 个排序链表. 该题在 LeetCode 官网上有关于链表的问题中标注为最难的一道题目:难度为 Hard ,通过率在链表 Hard 级别目前最低 ...

  4. 《剑指Offer》算法题——二维数组查找

    题目:在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. class Solutio ...

  5. JAVA常见算法题(二十七)

    题目:给定一个存放整数的数组,请写一个算法,把偶数移动到该数组的右边,奇数放在该数组的左边,请考虑时间和空间的最优算法. package com.forezp.util; /** * 题目:给定一个存 ...

  6. JAVA常见算法题(二十八)

    package com.forezp.util; import java.util.Arrays; /** * 两个int数组,都是从小到大的的排列,请合并为一个新的数组,也是从小到到大的排列, * ...

  7. JAVA常见算法题(二十六)

    package com.xiaowu.demo; import java.util.Scanner; /** * Java实现将阿拉伯数字转为汉字 * * @author WQ * */ public ...

  8. JAVA常见算法题(二十五)

    /** * Java实现中文数字转换为阿拉伯数字 * * * @author WQ * */ public class Demo26 { public static void main(String[ ...

  9. JAVA常见算法题(二十四)

    package com.xiaowu.demo; //一个5位数,判断它是不是回文数.即12321是回文数,个位与万位相同,十位与千位相同. public class Demo24 { public ...

随机推荐

  1. Flink-v1.12官方网站翻译-P010-Fault Tolerance via State Snapshots

    通过状态快照进行容错 状态后台 Flink管理的键控状态是一种碎片化的.键/值存储,每项键控状态的工作副本都被保存在负责该键的任务管理员的本地某处.操作员的状态也被保存在需要它的机器的本地.Flink ...

  2. Hadoop----hdfs dfs常用命令的使用

    用法    -mkdir    创建目录    Usage:hdfs dfs -mkdir [-p] < paths>    选项:-p    很像Unix mkdir -p,沿路径创建父 ...

  3. 三维CAD——基于B_rep的建模操作

    内容来自高老师的<三维CAD建模>课,本文就主要介绍半边结构和欧拉操作以及代码实现. 1. 边界表示法及其数据结构 · 拓扑结构 a.拓扑元素:面.边.点.体 b.拓扑关系:9种.V{V} ...

  4. Codeforces Round #533 (Div. 2) C. Ayoub and Lost Array(递推)

    题意: 长为 n,由 l ~ r 中的数组成,其和模 3 为 0 的数组数目. 思路: dp[ i ][ j ] 为长为 i,模 3 为 j 的数组数目. #include <bits/stdc ...

  5. cf-1230C Anadi and Domino

    题目链接:http://codeforces.com/contest/1230/problem/C 题意: 有21 个多米诺骨牌,给定一个无向图(无自环,无重边),一条边上可以放一个多米诺骨牌.如果两 ...

  6. HDU - 4221 贪心

    题意: 你有n个任务,每一个任务有一个完成所需时间AI,和一个截止时间BI.时间从0开始,如果完成任务的时间(设这个时间为ans)大于BI那么就会收到ans-BI的惩罚,问你完成所有这些任务你会收到的 ...

  7. 三、Python基本数据类型

    一.基本算术运算(获取的结果是值) 1 a1=10 2 a2=20#初始赋值 3 a3=a1+a2 #结果30 4 a4=a2-a1 #结果10 5 a5=a1*a2 #结果200 6 a6=a2/a ...

  8. 手动编译Selenium源码

    起因 Selenium ChromeDriver和Chrome升级到最新v81版本,同时需要将Selenium3.3.1升级到3.141.59. Selenium本身不支持https,所以修改源码用于 ...

  9. 渗透技巧——如何逃逸Linux的受限制shell执行任意命令

    导语:本文介绍了如何在受限制的shell中执行任意命令,实现交互.其相应的利用场景是说当我们通过一些手段拿到当前Linux机器的shell时,由于当前shell的限制,很多命令不能执行,导致后续的渗透 ...

  10. Dapr 正式发布1.0

    年前我写了一篇博客<Dapr 已在塔架就位 将发射新一代微服务>, 今天Dapr 正式发布了1.0 : Dapr Runtime v1.0.0 Dapr dotnet SDK v1.0.0 ...