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引用指向的对象,但实际上是变了个寂寞!😶

函数调用返回后,head引用指向的对象还是调用前的那个!

在debug模式下,我们再继续深入看看递归函数调用前跟调用后的head对象是不是完全一样的:

从上面两张图可以发现,虽然递归调用前跟调用后head引用指向的对象都是同一个,但这个对象本身的属性(指针域)已经发生了变化!

由此说明递归函数的执行并不是在做无用功,而是切切实实改变了原链表的各结点指向顺序!

只是因为递归函数执行完成后,并没有成功让传入的head对象引用指向反转后的新链表的头结点N4,

此时head对象引用仍然跟调用前一样指向了N1,而N1在反转后的新链表中变成了尾结点,至此,我们已经完美的丢失了反转后的新链表 😭,光靠指向尾结点的head根本无法遍历到新链表的其他结点。。。

问题延伸:探究Java方法调用中的参数传递实质

由上面的问题定位可知,问题出在我对Java方法调用中的参数传递理解有偏差,那么接下来就来详细探究一下Java方法调用中的参数传递过程吧!

形参与实参

测试示例代码:

public static void recursionNode(Node headNode, Node prevNode) {
		// do something...
}

public static void main(String[] args) {
		// init head...
		recursionNode(head, null); // 调用方法
}

在上面的示例代码中,我们定义了recursionNode()方法,并在main()方法中调用它

方法定义中的 headNode prevNode 是 形式参数,调用时传入的 head null 是 实际参数

值传递

方法定义中的形式参数类型如果是基本数据类型(byte、short、int、long、float、double、boolean、char),则调用方法时,实参到形参的传递实际是值传递,传递的是实际参数值的副本(拷贝)

因此,在方法体中任意修改形参的值,并不会影响到方法体外的实参的值

引用传递

方法定义中的形式参数类型如果是对象类型(含基本数据类型的数组),则调用方法时,实参到形参的传递实际也是值传递,传递的是实参对象的引用地址

如何理解这个 实参对象的引用地址 的概念呢?让我们来看看示例代码运行时的内存模型图(简单抽象了stack和heap的部分,如有不对欢迎指正 😆)

如图,main方法和recursionNode方法执行时实际是作为不同的栈帧入栈到当前线程的虚拟机栈中

main方法中的 head 引用实际保存的是一个地址,通过这个地址可以找到堆(heap)中的一个Node对象

recursionNode方法中的 headNode 引用实际保存的也是一个地址,通过这个地址可以找到堆中的一个Node对象

那么在main方法中调用recursionNode方法,实参 head 到形参 headNode 的传递过程中,到底传递的是什么呢?

很明显,传递的就是那个能寻址到堆中某个Node对象的 地址(划重点,要考!)

由此,实参 head 对象引用和形参 headNode 对象引用具有了相同的地址值,指向堆中的同一个Node对象

通过这两个引用中的任何一个,都可以改变堆中对应的那个对象的属性和状态

递归方法调用后发生了什么

理解了对象引用传递的实质后,再回过头来看上面递归方法调用后实际结果与预期结果不一致的问题,一切就迎刃而解了

如图,递归调用结束后,虽然递归方法recursionNode()方法体内的 headNode 引用确实已经变成了指向新的Node对象N4,但是main方法中,head 引用指向的仍然是递归方法调用前的Node对象N1(随着递归方法的执行,N1对象内部的指针域已经产生了变化)

正确的递归方式实现链表反转

 /**
  * 递归实现链表反转,递归方法执行完成后,head就从 头结点->尾结点 中的起始点(头结点)变成了 尾结点->头结点 中的起始点(尾结点)
  *
  * @param head 头结点
  * @param prev 存储上一个结点
  */
 public static Node recursionNode2(Node head, Node prev) {
  if (null == head.next) {
   // 设定递归终止条件
   head.next = prev;
   return head;
  }
  Node tempNext = head.next;
  head.next = prev;
  prev = head;
  head = tempNext;
  Node result = recursionNode2(head, prev);
  return result;
 }

测试结果:

 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 = recursionNode2(head, null);
  printNode(head);
 }

 /**
  * 循环打印链表数据域
  * @param head
  */
 public static void printNode(Node head) {
  while (head != null) {
   System.out.println(head.val);
   head = head.next;
  }
 }

运行结果截图:

可以看到,经过改善的新递归方法实现了预期的效果!😁

总结

到此这篇关于Java单链表反转的文章就介绍到这了,更多相关Java单链表反转内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java实现单链表反转的多种方法总结

    对于单链表不熟悉的可以看一下基于Java实现单链表的增删改查 一.原地反转 1.新建一个哨兵节点下一结点指向头结点 2.把待反转链表的下一节点插入到哨兵节点的下一节点 反转之前的链表:1–>2–>3–>4>–>5 加入哨兵节点:dummp–>1–>2–>3–>4>–>5 原地反转: 定义:prev=dummp.next; pcur=prev.next; prev.next=pcur.next; pcur.next=dummp.next; d

  • Java单链表反转图文教程

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

  • Java 单链表数据结构的增删改查教程

    我就废话不多说了,大家还是直接看代码吧~ package 链表; /** * *1)单链表的插入.删除.查找操作: * 2)链表中存储的是int类型的数据: **/ public class SinglyLinkedList { private Node head = null; //查找操作 public Node findByValue(int value){ Node p = head; //从链表头部开始查找 while(p.next != null && p.data != va

  • Java单链表的简单操作实现教程

    前言 用Java实现单链表的简单操作,阅读本文和上一篇文章体会Java中类与C++中结构体指针的区别 提示:以下是本篇文章正文内容,下面案例可供参考 一.基本实现思路 构造结点类 构造链表类 具体测试实现 二.代码实现 1.定义结点类 package list.test01; /* *定义结点类 */ public class Node { private int data; public Node next; public Node(int data) { this.data = data;

  • C语言实现单链表反转

    一.理解指针 看懂链表的结构并不是很难,但是一旦把它和指针混在一起,就很容易让人摸不着头脑.所以,要想写对链表代码,首先就要理解好指针. 有些语言有"指针"的概念,比如 C 语言:有些语言没有指针,取而代之的是"引用",比如 Java.Python.不管是"指针"还是"引用",实际上,它们的意思都是一样的,都是存储所指对象的内存地址. 将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变

  • Java单链表的增删改查与面试题详解

    目录 一.单链表的增删改查 1.创建结点 2.单链表的添加操作 3.单链表的删除操作 4.单链表的有效结点的个数 二.大厂面试题 一.单链表的增删改查 1.创建结点 单链表是由结点连接而成,所以我们首先要创建结点类,用于对结点进行操作.定义data属性 表示序号,定义name属性表示结点存放的数据信息,定义next属性表示指向下一个结点.构造器只需要放入data属性和name属性,重写toString方法方便打印结点信息. public class Node { public int data;

  • Java单链表的实现代码

    下面是小编给大家分享的一个使用java写单链表,有问题欢迎给我留言哦. 首先定义一个Node类 public class Node { protected Node next; //指针域 public int data;//数据域 public Node( int data) { this. data = data; } //显示此节点 public void display() { System. out.print( data + " "); } } 接下来定义一个单链表,并实现

  • Java单链表基本操作的实现

    最近被问到链表,是一个朋友和我讨论Java的时候说的.说实话,我学习编程的近一年时间里,学到的东西还是挺少的.语言是学了Java和C#,关于Web的学了一点Html+css+javascript.因为比较偏好,学习WinForm时比较认真,数据库操作也自己有所研究.但链表这个东西我还真没有学习和研究过,加上最近自己在看WPF,而课程也到了JSP了,比较紧. 但是我还是抽了一个晚上加半天的时间看了一下单向链表.并且使用Java试着写了一个实例出来.没有接触过链表的朋友可以作为参考,希望大家多提宝贵

  • java单链表逆序用法代码示例

    本篇博客,比较简单.对单链表逆序不理解的看看就可以了. 逆序思想 现假设有一链表,有待逆序操作.我们首先想到的就是将那个指针关系逆序了就行了呗. 事实上,就是这样.博主就是以这个为目标来完成的单链表逆序操作. Node pre = null; Node post = null; while(head!=null){ post = head.next; head.next = pre; pre = head; head = post; } 这便是逆序的核心了.下面我们就来一步步的讲解. 首次逆序:

  • 单链表反转python实现代码示例

    单链表的反转可以使用循环,也可以使用递归的方式 1.循环反转单链表 循环的方法中,使用pre指向前一个结点,cur指向当前结点,每次把cur->next指向pre即可. 代码: class ListNode: def __init__(self,x): self.val=x; self.next=None; def nonrecurse(head): #循环的方法反转链表 if head is None or head.next is None: return head; pre=None; c

随机推荐