Java单链表反转图文详解

最近在回顾链表反转问题中,突然有一些新的发现和收获,特此整理一下,与大家分享

背景回顾

单链表的存储结构如图:

数据域存放数据元素,指针域存放后继结点地址

我们以一条 N1 -> N2 -> N3 -> N4 指向的单链表为例:

反转后的链表指向如图:

我们在代码中定义如下结点类以方便运行测试:

    /**
* 结点类
* (因为后续在main方法中运行,为了方便定义为static内部类)
*/
static class Node {
int val; // 数据域
Node next; // 指针域,指向下一个结点 Node(int x, Node nextNode) {
val = x;
next = nextNode;
}
}

通过循环遍历方式实现链表反转

实现思路:从链表头结点出发,依次循环遍历每一个结点,并更改结点对应的指针域,使其指向前一个结点

代码如下:

    /**
* 循环遍历方式实现链表反转
*
* @param head 链表的头结点
* @return
*/
public static Node cycleNode(Node head) { Node prev = null; // 保存前一个结点的信息 // 循环遍历链表中的结点
while (head.next != null) {
// 1. 先保存当前结点的下一个结点的信息到tempNext
Node tempNext = head.next;
// 2. 修改当前结点指针域,使其指向上一个结点(如果是第一次进入循环的头结点,则其上一个结点为null)
head.next = prev;
// 3. 将当前结点信息保存到prev中(以作为下一次循环中第二步使用到的"上一个结点")
prev = head;
// 4. 当前结点在之前的123步中指针域已经修改完毕,此时让head重新指向待处理的下一个结点
head = tempNext;
} // 上面的循环完成后,实际只修改了原先链表中的头结点到倒数第二个结点间的结点指向,倒数第一个结点(尾结点)并未处理
// 此时prev指向原先链表中的倒数第二个结点,head指向尾结点
// 处理尾结点的指针域,使其指向前一个结点
head.next = prev; // 返回尾结点,此时的尾结点既是原先链表中的尾结点,又是反转后的新链表中的头结点
return head;
}

测试效果:

    public static void main(String[] args) {
// 构造测试用例,链表指向为 N1 -> N2 -> N3 -> N4
Node n4 = new Node(4, null);
Node n3 = new Node(3, n4);
Node n2 = new Node(2, n3);
Node n1 = new Node(1, n2);
Node head = n1;
// 输出测试用例
System.out.println("原始链表指向为:");
printNode(head); // 普通方式反转链表
System.out.println("循环方式反转链表指向为:");
head = cycleNode(head);
printNode(head);
} /**
* 循环打印链表数据域
* @param head
*/
public static void printNode(Node head) {
while (head != null) {
System.out.println(head.val);
head = head.next;
}
}

运行结果如图:

可以看到,原先指向为 N1 -> N2 -> N3 -> N4 的链表,运行反转方法后,其指向已变为 N4 -> N3 -> N2 -> N1

通过递归方式实现链表反转

实现思路:从链表头结点出发,依次递归遍历每一个结点,并更改结点对应的指针域,使其指向前一个结点(没错,实际每一次递归里的处理过程跟上面的循环里是一样的)

代码实现:

    /**
* 递归实现链表反转
* 递归方法执行完成后,head指向就从原链表顺序:头结点->尾结点 中的第一个结点(头结点) 变成了反转后的链表顺序:尾结点->头结点 中的第一个结点(尾结点)
*
* @param head 头结点
* @param prev 存储上一个结点
*/
public static void recursionNode(Node head, Node prev) { if (null == head.next) {
// 设定递归终止条件
// 当head.next为空时,表明已经递归到了原链表中的尾结点,此时单独处理尾结点指针域,然后结束递归
head.next = prev;
return;
} // 1. 先保存当前结点的下一个结点的信息到tempNext
Node tempNext = head.next;
// 2. 修改当前结点指针域,使其指向上一个结点(如果是第一次进入递归的头结点,则其上一个结点为null)
head.next = prev;
// 3. 将当前结点信息保存到prev中(以作为下一次递归中第二步使用到的"上一个结点")
prev = head;
// 4. 当前结点在之前的123步中指针域修改已经修改完毕,此时让head重新指向待处理的下一个结点
head = tempNext; // 递归处理下一个结点
recursionNode(head, prev);
}

测试效果:

    public static void main(String[] args) {
// 构造测试用例,链表指向为 N1 -> N2 -> N3 -> N4
Node n4 = new Node(4, null);
Node n3 = new Node(3, n4);
Node n2 = new Node(2, n3);
Node n1 = new Node(1, n2);
Node head = n1;
// 输出测试用例
System.out.println("原始链表指向为:");
printNode(head); // 递归方式反转链表
System.out.println("递归方式反转链表指向为:");
recursionNode(head, null);
printNode(head);
} /**
* 循环打印链表数据域
* @param head
*/
public static void printNode(Node head) {
while (head != null) {
System.out.println(head.val);
head = head.next;
}
}

注意:在上面的测试代码中,在调用递归函数时传递了Node类的实例head作为参数

根据Java中 方法调用传参中,基本类型是值传递,对象类型是引用传递 可得 =>

因为在调用递归函数时传递了head对象的引用,且在递归函数运行过程中,我们已经数次改变了head引用指向的对象

那么当递归函数执行完毕时,head引用指向的对象此时理论上已经是原链表中的尾结点N4了,且链表顺序也已经变成了 N4 -> N3 -> N2 -> N1

运行效果截图:

最终的程序运行结果与我的设想大相径庭!

那么,问题出在哪里呢?

递归方式反转链表问题排查与延伸

问题定位

既然程序运行效果与预期效果不符,那我们就在head对象引用可能发生变化的地方加入注释打印一下对象地址,看看能不能发现问题在哪:

加入注释后的代码如下:

    public static void main(String[] args) {
// 构造测试用例,链表指向为 N1 -> N2 -> N3 -> N4
Node n4 = new Node(4, null);
Node n3 = new Node(3, n4);
Node n2 = new Node(2, n3);
Node n1 = new Node(1, n2);
Node head = n1;
// 输出测试用例
System.out.println("原始链表指向为:");
printNode(head); // 递归方式反转链表
System.out.println("递归方式反转链表指向为:");
System.out.println("递归调用前 head 引用指向对象: " + head.toString());
recursionNode(head, null);
System.out.println("递归调用后 head 引用指向对象: " + head.toString());
printNode(head);
} /**
* 循环打印链表数据域
* @param head
*/
public static void printNode(Node head) {
while (head != null) {
System.out.println(head.val);
head = head.next;
}
} /**
* 递归实现链表反转
* 递归方法执行完成后,head指向就从原链表顺序:头结点->尾结点 中的第一个结点(头结点) 变成了反转后的链表顺序:尾结点->头结点 中的第一个结点(尾结点)
*
* @param head 头结点
* @param prev 存储上一个结点
*/
public static void recursionNode(Node head, Node prev) {
System.out.println("递归调用中 head引用指向对象: " + head.toString());
if (null == head.next) {
// 设定递归终止条件
// 当head.next为空时,表名已经递归到了原链表中的尾结点,此时单独处理尾结点指针域,然后结束递归
head.next = prev;
System.out.println("递归调用返回前 head引用指向对象: " + head.toString());
return;
} // 1. 先保存当前结点的下一个结点的信息到tempNext
Node tempNext = head.next;
// 2. 修改当前结点指针域,使其指向上一个结点(如果是第一次进入循环的头结点,则其上一个结点为null)
head.next = prev;
// 3. 将当前结点信息保存到prev中(以作为下一次递归中第二步使用到的"上一个结点")
prev = head;
// 4. 当前结点在之前的123步中指针域修改已经修改完毕,此时让head重新指向待处理的下一个结点
head = tempNext; // 递归处理下一个结点
recursionNode(head, prev);
}

运行结果:

从上面的运行结果看,在递归函数执行期间,head引用指向的对象确实发生了变化

注意 调用前 / 调用返回前 / 调用后 这三个地方head引用指向对象的变化:

可以发现,虽然递归函数执行期间确实改变了head引用指向的对象,但实际上是变了个寂寞!

Java单链表反转图文详解的更多相关文章

  1. Java单链表反转 详细过程

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/guyuealian/article/details/51119499 Java单链表反转 Java实 ...

  2. java 单链表反转

    最近与人瞎聊,聊到各大厂的面试题,其中有一个就是用java实现单链表反转.闲来无事,决定就这个问题进行一番尝试. 1.准备链表 准备一个由DataNode组成的单向链表,DataNode如下: pub ...

  3. java单链表反转

    今天做leetcode,遇到了单链表反转.研究了半天还搞的不是太懂,先做个笔记吧 参考:http://blog.csdn.net/guyuealian/article/details/51119499 ...

  4. Java Stream函数式编程图文详解(二):管道数据处理

    一.Java Stream管道数据处理操作 在本号之前发布的文章<Java Stream函数式编程?用过都说好,案例图文详解送给你>中,笔者对Java Stream的介绍以及简单的使用方法 ...

  5. java单链表反转(花了半个多小时的作品)

    欢迎光临............... 首先我们要搞清楚链表是啥玩意儿?先看看定义: 讲链表之前我们先说说Java内存的分配情况:我们new对象的时候,会在java堆中为对象分配内存,当我们调用方法的 ...

  6. Centos 7 进入单用户模式图文详解

    由于昨晚做了一个很傻X的事情,所以有幸进入了CentOS 7 的单用户模式. CentOS 7 在进入单用户的时候和6.x做了很多的改变, 下面让我们来看看如何进入单用户模式. 如何进入CentOS ...

  7. 【图文详解】scrapy安装与真的快速上手——爬取豆瓣9分榜单

    写在开头 现在scrapy的安装教程都明显过时了,随便一搜都是要你安装一大堆的依赖,什么装python(如果别人连python都没装,为什么要学scrapy….)wisted, zope interf ...

  8. 单链表反转(Singly Linked Lists in Java)

    单链表反转(Singly Linked Lists in Java) 博客分类: 数据结构及算法   package dsa.linkedlist; public class Node<E> ...

  9. Java WebService接口生成和调用 图文详解>【转】【待调整】

    webservice简介: Web Service技术, 能使得运行在不同机器上的不同应用无须借助附加的.专门的第三方软件或硬件, 就可相互交换数据或集成.依据Web Service规范实施的应用之间 ...

随机推荐

  1. JavaScript 如何使用 setTimeout 实现 setInterval

    JavaScript 如何使用 setTimeout 实现 setInterval website multi content page setIntervalSimulator "use ...

  2. shit mint-ui & navbar click event bug

    shit mint-ui & navbar click event bug # Vue 2.0 npm install mint-ui -S // 引入全部组件 import Vue from ...

  3. flutter web in action

    flutter web in action flutter for web https://flutter.dev/web https://flutter.dev/docs/get-started/w ...

  4. js 上传图片,用户自定义截取图片大小

    js 上传图片,用户自定义截取图片大小 js 组件

  5. [Python学习笔记]文件的读取写入

    文件与文件路径 路径合成 os.path.join() 在Windows上,路径中以倒斜杠作为文件夹之间的分隔符,Linux或OS X中则是正斜杠.如果想要程序正确运行于所有操作系统上,就必须要处理这 ...

  6. pwn篇:攻防世界进阶welpwn,LibcSearcher使用

    攻防世界welpwn (搬运一篇自己在CSDN写的帖子) 链接:https://blog.csdn.net/weixin_44644249/article/details/113781356 这题主要 ...

  7. DRF的orm多表关系补充及serializer子序列化

    目录 一.控制多表关系的字段属性 1.如何建立基表 2.断开连表关系 3.四种级联关系 二.子序列化 一.控制多表关系的字段属性 1.如何建立基表 要在基表中配置Meta,设置abstract=Tru ...

  8. Redis-第十章节-链表

    目录 数组和链表 链表 对比 总结 1.数组和链表 数组: 数组会在内存中开辟一块连续的空间存储数据,这种存储方式有利也有弊端.当获取数据的时候,直接通过下标值就可以获取到对应的元素,时间复杂度为O( ...

  9. MySQL确认注入点

    目录 WHERE子句后面的注入点 逻辑符号AND.OR other order by union limit table WEB渗透测试流程中,初期工作是进行信息收集,完成信息收集之后,就会进行漏洞测 ...

  10. 单细胞分析实录(9): 展示marker基因的4种图形(二)

    在上一篇中,我已经讲解了展示marker基因的前两种图形,分别是tsne/umap图.热图,感兴趣的读者可以回顾一下.这一节我们继续学习堆叠小提琴图和气泡图. 3. 堆叠小提琴图展示marker基因 ...