手写链表反转
1、问题背景
反转链表是一个出现频率特别高的算法题,在各大高频题排名网站也长期占领前三,比如长期占据牛客网的top1。所以链表反转是学习链表的过程中最最重要的问题,没有之一。
2、问题描述
给你一个单链表的头节点head,请你反转链表,并且返回反转之后的链表。
示例:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
3、解决方法
3.1 方法一:建立虚拟头结点辅助反转
在链表插入元素的时候,如何去处理头结点是一个比较麻烦的问题,为此可以建立一个虚拟的节点ans,让ans指向链表的头结点,即ans.next = head,接下来每次从旧的链表上拆下来一个节点接到ans后面,再调整其他的线就好了。
从图上可以看到,每次从原链表中取下一个节点,接到虚拟头节点ans后面,即cur.next = ans.next,ans.next = cur,cur = next
为什么需要新用一个next来存储cur的下一个节点?
因为当cur被接入反转后的链表后,cur的next指针是指向ans的下一个节点的,如果不事先将next找到并存储起来,在接完cur后,next会丢失。
具体的代码实现如下:
// 方法一:虚拟节点法
public static ListNode reverseList(ListNode head){
ListNode ans = new ListNode(-1);
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = ans.next;
ans.next = cur;
cur = next;
}
return ans.next;
}
3.2 方法二:直接操作链表实现反转
上面一种方法虽然很好理解,应用也很广,但是比较常规,下面一种不借助虚拟节点的方法更能体现能力。
先来看一下链表在反转前后的结构和指针的位置:
再来看一下执行期间的流程示意图,图中的cur本来指向旧链表的头节点,pre表示调整好的新链表的头节点,next是下一个需要调整的节点。
注意图中箭头的方向,cur和prev原来是两个新旧链表的表头,在中间调整了cur的指针之后,需要平移一次prev和cur的位置,让其重新变成两个表的表头。
以下是代码实现:
//方法二:直接操作链表
public static ListNode reverseList(ListNode head){
ListNode prev = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
以上的代码可以在理解的基础上背下来,因为非常非常重要!!!!
3.3 方法三:通过递归实现链表反转
先看代码,再解释一下递归的原理
//方法三:递归实现
public static ListNode reverseList(ListNode head){
if(head == null || head.next == null){
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
这个递归函数的工作原理是:首先检查链表是否为空或者只有一个节点。如果是,那么我们不需要做任何事情,所以我们直接返回头节点。否则我们递归地对剩余的链表调用这个函数,然后将当前的头节点设置为新的尾节点。最后,我们断开原来head的next,防止循环引用。
具体图解如下:
需要注意的是,递归解法虽然代码简洁,但是对于很长的链表,可能会导致调用栈深度超过系统限制,从而导致程序崩溃。这种情况下,一般使用其他的两种方法。
4、总结
本文主要讨论了对于链表反转这一问题的解法,最常用的就是借助虚拟节点来实现,这在很多底层源码中都有使用,而面试中最常用的是第二种不带虚拟头结点的方法,这种解法需要重点理解和记忆,最后一种用递归来实现,代码会很简洁,思路也比较简单,但是由于递归本身所存在的缺点,这种解法也只做思路开拓。