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. Web 安全 & cookies & HttpOnly

    Web 安全 & cookies & HttpOnly cookie HttpOnly 禁止 js 读取 cookie 的方法 HttpOnly 实现原理 document.cooki ...

  2. Python Learning Paths

    Python Learning Paths Python Expert Python in Action Syntax Python objects Scalar types Operators St ...

  3. yarn & macOS & upgrade

    yarn & macOS https://yarnpkg.com/zh-Hans/docs/install#mac-stable $ brew install yarn $ brew upgr ...

  4. Flutetr flutter_downloader 1.3.1

    flutter_downloader 此库更新极慢,所以问题及多,可以看文档 安装 dependencies: flutter_downloader: ^1.3.1 配置 Java: // MyApp ...

  5. Baccarat凭什么吸引做市商?2021年将如何发展?

    在过去的一年里,基于资金池的AMM自动化做市商几乎统治了所有DeFi活动,他们没有订单簿,而是根据算法曲线提供资产.尽管在流动性和交易方面取得了令人惊叹的成绩,但是其自身具有无常损失.多代币敞口以及低 ...

  6. Python学习笔记_购物车案例

    goods_dic = { "iphone":6000, "ipad":3000, "T-shirt":100, "coffee& ...

  7. 深入解析 HTTP 缓存控制

    缓存(Cache)是计算机领域里的一个重要概念,是优化系统性能的利器. 由于链路漫长,网络时延不可控,浏览器使用 HTTP 获取资源的成本较高.所以,非常有必要把"来之不易"的数据 ...

  8. EasyExcel学习

    导入依赖 <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</a ...

  9. Python爬虫系统化学习(3)

    一般来说当我们爬取网页的整个源代码后,是需要对网页进行解析的. 正常的解析方法有三种 ①:正则匹配解析 ②:BeatuifulSoup解析 ③:lxml解析 正则匹配解析: 在之前的学习中,我们学习过 ...

  10. 查看手机CPU每个APP利用率

    adb   shell   top   -m   5