力扣题目链接
更多内容可点击此处跳转到代码随想录,看原版文件
题意:反转一个单链表。
示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL
如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。
其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。
那么接下来看一看是如何反转的呢?
我们拿有示例中的链表来举例,如下面所示:(纠正:动画应该是先移动pre,在移动cur)
首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。
cur = head
pre = null
然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。
temp = cur - > next
为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。
现在,cur->next 指向pre ,此时我们反转了第一个节点。
cur - > next = pre
接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。先移动pre到cur的位置,再把cur往temp的位置去移动。(如果先把cur往temp的位置去移动,那此时cur = temp,cur的值已变化,pre就无法指向正确位置了)
pre = cur
cur = temp
一次循环就改变一个节点,循环下去改变下一个节点,知道cur指向null了,那么这个循环就结束了,链表也反转完毕了。而此时pre就等于我们新链表中的新头结点,最后直接返回就可以了。
while(cur)
{
temp = cur - > next
cur - > next = pre
pre = cur
cur = temp
}
return pre;
思路已在上方呈现,下面是示例代码:
public class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null; // 初始化前一个节点为null
ListNode cur = head; // 初始化当前节点为头节点
ListNode temp = null; // 用于保存下一个节点的临时变量
while (cur != null) {
temp = cur.next; // 保存下一个节点
cur.next = pre; // 将当前节点指向前一个节点,实现链表反转
pre = cur; // 更新前一个节点为当前节点
cur = temp; // 更新当前节点为下一个节点
}
return pre; // 返回反转后的头节点
}
}
递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。
关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。
具体可以看代码(已经详细注释),双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head); // 调用私有递归方法,反转链表
}
/**
* 递归反转链表
*
* @param pre 反转后的链表头节点
* @param cur 当前节点
* @return 反转后的链表头节点
*/
private ListNode reverse(ListNode pre, ListNode cur) {
if (cur == null) {
return pre; // 如果当前节点为空,说明已遍历到链表末尾,返回反转后的链表头节点
}
ListNode temp = null; // 用于保存下一个节点的临时变量
temp = cur.next; // 保存下一个节点
cur.next = pre; // 反转当前节点指向前一个节点
// 更新pre、cur位置
// pre = cur; // 将当前节点指向的前一个节点更新为当前节点
// cur = temp; // 将下一个节点更新为当前节点
return reverse(cur, temp); // 递归反转后续节点,往下继续走的意思
}
}
reverseList
方法是公共的入口方法,用于调用递归反转链表方法。reverse
方法是私有的递归方法,用于实现链表的反转。我们可以发现,上面的递归写法和双指针法实质上都是从前往后翻转指针指向,其实还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。
具体代码如下(带详细注释):
class Solution {
ListNode reverseList(ListNode head) {
// 边缘条件判断
if(head == null) return null;
if (head.next == null) return head;
// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
}
双指针法和递归法是两种常见的用于反转链表的方法,它们的主要区别在于实现方式和思路。
对双指针中的代码进行编写测试类,用于试运行:
第一版:
public class Solution {
public static ListNode reverseList(ListNode head) {
ListNode pre = null; // 初始化前一个节点为null
ListNode cur = head; // 初始化当前节点为头节点
ListNode temp = null; // 用于保存下一个节点的临时变量
while (cur != null) {
temp = cur.next; // 保存下一个节点
cur.next = pre; // 将当前节点指向前一个节点,实现链表反转
pre = cur; // 更新前一个节点为当前节点
cur = temp; // 更新当前节点为下一个节点
}
return pre; // 返回反转后的头节点
}
public static void main(String[] args) {
// 创建测试链表
ListNode head = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
ListNode node4 = new ListNode(4);
ListNode node5 = new ListNode(5);
head.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
// 打印原链表
System.out.print("原链表:");
printLinkedList(head);
// 调用反转链表方法
ListNode reversed =reverseList(head);
// 打印反转后的链表
System.out.print("反转后的链表:");
printLinkedList(reversed);
}
// 辅助方法,用于遍历打印链表
public static void printLinkedList(ListNode head) {
ListNode temp = head;
StringBuilder sb = new StringBuilder(); // 使用StringBuilder来逐次构建字符串
while (temp != null) {
sb.append(temp.val).append(" -> "); // 逐次追加链表节点的值
temp = temp.next;
}
sb.append("NULL"); // 最后追加 "NULL" 字符串
System.out.println(sb.toString()); // 输出构建好的字符串
}
}
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
第二版:
public class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null; // 初始化前一个节点为null
ListNode cur = head; // 初始化当前节点为头节点
ListNode temp = null; // 用于保存下一个节点的临时变量
while (cur != null) {
temp = cur.next; // 保存下一个节点
cur.next = pre; // 将当前节点指向前一个节点,实现链表反转
pre = cur; // 更新前一个节点为当前节点
cur = temp; // 更新当前节点为下一个节点
}
return pre; // 返回反转后的头节点
}
public static void main(String[] args) {
// 创建测试链表
ListNode head = new ListNode(1);
ListNode node2 = new ListNode(2);
ListNode node3 = new ListNode(3);
ListNode node4 = new ListNode(4);
ListNode node5 = new ListNode(5);
head.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
// 创建Solution对象
Solution solution = new Solution();
// 调用反转链表方法
ListNode reversed = solution.reverseList(head);
// 打印原链表
System.out.print("原链表:");
printLinkedList(head);
// 打印反转后的链表
System.out.print("反转后的链表:");
printLinkedList(reversed);
}
// 辅助方法,用于遍历打印链表
public static void printLinkedList(ListNode head) {
ListNode temp = head;
while (temp != null) {
System.out.print(temp.val + " -> ");
temp = temp.next;
}
System.out.println("NULL");
}
}
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
这两段代码的区别在于打印原链表的时机不同。
第一段代码中,先打印原链表,然后再调用反转链表方法进行链表反转,最后打印反转后的链表。
// 打印原链表
System.out.print("原链表:");
printLinkedList(head);
// 调用反转链表方法
ListNode reversed = reverseList(head);
// 打印反转后的链表
System.out.print("反转后的链表:");
printLinkedList(reversed);
而第二段代码中,先调用反转链表方法进行链表反转,然后再打印原链表和反转后的链表。
// 调用反转链表方法
ListNode reversed = reverseList(head);
// 打印原链表
System.out.print("原链表:");
printLinkedList(head);
// 打印反转后的链表
System.out.print("反转后的链表:");
printLinkedList(reversed);
因此,第一段代码的输出结果为:
原链表:1 -> 2 -> 3 -> 4 -> 5 -> NULL
反转后的链表:5 -> 4 -> 3 -> 2 -> 1 -> NULL
而第二段代码的输出结果为:
原链表:1 -> NULL
反转后的链表:5 -> 4 -> 3 -> 2 -> 1 -> NULL
所以,区别在于第一段代码输出了完整的原链表,而第二段代码输出了只有一个节点的原链表。推荐使用第一段代码的方式来打印完整的原链表和反转后的链表。