Java单链表反转图文详解
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单链表反转图文详解的更多相关文章
- Java单链表反转 详细过程
版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/guyuealian/article/details/51119499 Java单链表反转 Java实 ...
- java 单链表反转
最近与人瞎聊,聊到各大厂的面试题,其中有一个就是用java实现单链表反转.闲来无事,决定就这个问题进行一番尝试. 1.准备链表 准备一个由DataNode组成的单向链表,DataNode如下: pub ...
- java单链表反转
今天做leetcode,遇到了单链表反转.研究了半天还搞的不是太懂,先做个笔记吧 参考:http://blog.csdn.net/guyuealian/article/details/51119499 ...
- Java Stream函数式编程图文详解(二):管道数据处理
一.Java Stream管道数据处理操作 在本号之前发布的文章<Java Stream函数式编程?用过都说好,案例图文详解送给你>中,笔者对Java Stream的介绍以及简单的使用方法 ...
- java单链表反转(花了半个多小时的作品)
欢迎光临............... 首先我们要搞清楚链表是啥玩意儿?先看看定义: 讲链表之前我们先说说Java内存的分配情况:我们new对象的时候,会在java堆中为对象分配内存,当我们调用方法的 ...
- Centos 7 进入单用户模式图文详解
由于昨晚做了一个很傻X的事情,所以有幸进入了CentOS 7 的单用户模式. CentOS 7 在进入单用户的时候和6.x做了很多的改变, 下面让我们来看看如何进入单用户模式. 如何进入CentOS ...
- 【图文详解】scrapy安装与真的快速上手——爬取豆瓣9分榜单
写在开头 现在scrapy的安装教程都明显过时了,随便一搜都是要你安装一大堆的依赖,什么装python(如果别人连python都没装,为什么要学scrapy….)wisted, zope interf ...
- 单链表反转(Singly Linked Lists in Java)
单链表反转(Singly Linked Lists in Java) 博客分类: 数据结构及算法 package dsa.linkedlist; public class Node<E> ...
- Java WebService接口生成和调用 图文详解>【转】【待调整】
webservice简介: Web Service技术, 能使得运行在不同机器上的不同应用无须借助附加的.专门的第三方软件或硬件, 就可相互交换数据或集成.依据Web Service规范实施的应用之间 ...
随机推荐
- taro swiper & scroll tabs
taro swiper & scroll tabs https://taro-docs.jd.com/taro/docs/components/viewContainer/swiper.htm ...
- local JSON file loader in js
local JSON file loader in js "use strict"; /** * * @author xgqfrms * @license MIT * @copyr ...
- 「NGK每日快讯」2021.1.22日NGK公链第80期官方快讯!
- JavaScript中判断对象是否属于Array类型的4种方法及其背后的原理与局限性
前言 毫无疑问,Array.isArray是现如今JavaScript中判断对象是否属于Array类型的首选,但是我认为了解本文其余的方法及其背后的原理与局限性也是很有必要的,因为在JavaScrip ...
- 微服务学习.net5+consul
趁着刚过完年,还没有开始做业务的时候,学习下consul 概念自己去官网看,这里只讲下具体实现 官网下载https://www.consul.io/downloads 我下载的是Windows版本 启 ...
- nginx反向代理理解
实际开发中,会有不同的环境: - 开发环境:自己的电脑- 测试环境:提供给测试人员使用的环境- 预发布环境:数据是和生成环境的数据一致,运行最新的项目代码进去测试- 生产环境:项目最终发布上线的环境 ...
- 带你认识webpack
一.webpack是什么 webpack是一种前端资源构建工具,一个静态模块打包器(module bundler).在webpack看来,前端的所有资源文件(js/json/css/img/less/ ...
- git配置了公钥,在下载项目时为什么还要输入密码
配置git地址:https://www.cnblogs.com/lz0925/p/10794616.html 原文链接:https://blog.csdn.net/xiaomengzi_16/arti ...
- Vue使用 空白占位符
当有时候需要在页面显示时显示空格时,可以使用 ,但是使用这个占位符时,无论写多少个,就只能显示一个空格.要想显示多个空格进行占位,这种方式显然是可行的,解决方法是使用转义字符. 先看代码: <t ...
- git相关问题
1.git查看远程分支更新到本地 git clone 项目地址,示例如下: git clone https://github.com/zhongyushi-git/vue-test.git 在拉取时, ...