广告
返回顶部
首页 > 资讯 > 后端开发 > Python >Java单链表反转图文教程
  • 474
分享到

Java单链表反转图文教程

2024-04-02 19:04:59 474人浏览 八月长安

Python 官方文档:入门教程 => 点击学习

摘要

目录前言背景回顾通过循环遍历方式实现链表反转通过递归方式实现链表反转递归方式反转链表问题排查与延伸问题定位问题延伸:探究Java方法调用中的参数传递实质正确的递归方式实现链表反转总结

前言

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

背景回顾

单链表的存储结构如图:

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

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

反转后的链表指向如图:

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


 
 static class node {
 int val; // 数据域
 Node next; // 指针域,指向下一个结点

 Node(int x, Node nextNode) {
  val = x;
  next = nextNode;
 }
 }

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

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

代码如下:


 
 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);
 }

 
 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

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

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

代码实现:


 
 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);
 }

 
 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);
 }

 
 public static void printNode(Node head) {
  while (head != null) {
   System.out.println(head.val);
   head = head.next;
  }
 }

 
 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对象内部的指针域已经产生了变化)

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


 
 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);
 }

 
 public static void printNode(Node head) {
  while (head != null) {
   System.out.println(head.val);
   head = head.next;
  }
 }

运行结果截图:

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

总结

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

--结束END--

本文标题: Java单链表反转图文教程

本文链接: https://www.lsjlt.com/news/122816.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Java单链表反转图文教程
    目录前言背景回顾通过循环遍历方式实现链表反转通过递归方式实现链表反转递归方式反转链表问题排查与延伸问题定位问题延伸:探究Java方法调用中的参数传递实质正确的递归方式实现链表反转总结...
    99+
    2022-11-12
  • java怎么实现单链表反转
    要实现单链表的反转,可以使用迭代或递归两种方法。 迭代法: public ListNode reverseList(ListNo...
    99+
    2023-10-26
    java
  • 怎么理解Java递归单链表反转
    这篇文章主要讲解了“怎么理解Java递归单链表反转”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么理解Java递归单链表反转”吧!首先,咱们要先明确,什么...
    99+
    2022-10-19
  • Java数据结构之链表实现(单向、双向链表及链表反转)
    前言 之前学习的顺序表查询非常快,时间复杂度为O(1),但是增删改效率非常低,因为每一次增删改都会元素的移动。可以使用另一种存储方式-链式存储结构。 链表是一种物理存储单元上非连续、...
    99+
    2022-11-12
  • Java反转链表测试过程介绍
    目录导读链表特点单链表和双链表的定义单向链表Node结点反转单向链表测试函数双向链表Node结点反转双向链表测试代码导读 本文主体为单项链表和双向链表的反转以及简单的测试,以便于理解...
    99+
    2023-05-15
    Java反转链表 Java反转链表测试
  • Java实现单链表反转的多种方法总结
    对于单链表不熟悉的可以看一下基于Java实现单链表的增删改查 一、原地反转 1、新建一个哨兵节点下一结点指向头结点 2、把待反转链表的下一节点插入到哨兵节点的下一节点 反转之前的链...
    99+
    2022-11-12
  • 一篇文章带你玩转JAVA单链表
    目录一、链表 1. 概念2. 结构二、单向不带头非循环链表 1. 概念及结构2. 链表的实现三、链表面试题四、总结一、链表  1. 概念 链表是一种物理...
    99+
    2022-11-12
  • Java实现单链表SingleLinkedList增删改查及反转 逆序等
    节点类 可以根据需要,对节点属性进行修改。注意重写toString()方法,以便后续的输出操作。 //节点类 class Node { public int id; ...
    99+
    2022-11-12
  • Java 反转带头结点的单链表并显示输出的实现过程
      注意:要保证已经有Node类和单链表的初始化,这样才能调用反转方法并显示结果。 方法如下: //Node<T>指泛型结点类 public void re...
    99+
    2022-11-12
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作