链表反转是一个出现频率特别高的算法题,笔者过去这些年面试,至少遇到过七八次。其中更夸张的是曾经两天写了三次,上午YY,下午金山云,第二天快手。链表反转在各大高频题排名网站也长期占领前三。比如牛客网上这个No 1 好像已经很久了。
另外很多题目也都要用它来做基础, 例如指定区间反转、链表K个一组翻转。还有一些在内部的某个过程用到了反转,例如两个链表生成相加链表。还有一种是链表排序的,也是需要移动元素之间的指针,难度与此差不多。从今天开始,我们用3篇来专门研究一下这几个问题。第一篇,介绍反转的3种实现方法。第二篇介绍反转的几种变形,第三篇介绍两个链表生成相加的链表。
首先我们看一下题目要求:
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
如下图:
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
1.实现
这个题目的关键点是如果再定义一个新链表,会对内存空间造成浪费,比较好的方式是将每个结点的指向都反过来就行,也就是下面这个样子:
那这里的问题就是如何准确的记录并调整指针,代码也不算很复杂:
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
ListNode temp = null;
while (cur != null) {
temp = cur.next;// 保存下一个节点
cur.next = prev;
prev = cur;
cur = temp;
}
return prev;
}
}
建议你自己在纸上画画图、想一想,如果能想明白就不必看我下面的解释了。
二、迭代法解析
上面迭代法不算复杂,一般是能想清楚的,但是面试的时候可能会突然卡壳,怎么也搞不清楚指针到底怎么处理。我们可以通过图示来看一下:
在上图中,我们用cur来表示旧链表被访问的位置,pre表示新链表的表头。注意图中箭头方向,cur和pre都是两个表的表头,每移动完一个结点之后,我们必须准确知道两个链表的表头。
cur是需要接到pre的,那该怎么知道其下一个结点5呢?
很显然仅仅靠pre和cur是不够的,我们需要一个temp结点来临时保存cur的下一个指针(图中的5),然后将cur的next指针指向pre,然后将cur赋值给pre,最后将temp再赋值给cur。
上面这个过程不仅严密,而且顺序都不能错。也就是这几行代码:
temp = cur.next;// 保存下一个节点
cur.next = prev;
prev = cur;
cur = temp;
三.递归方法实现
这个题目还可以使用递归实现的,但是笔者强烈建议你将前面的方法想清楚,写清楚,最好闭着眼睛就能写,递归的方式就不考虑了。
因为每次cur移动之后仍然是旧的链表表头,prev移动之后仍然是新链表的表头。我们可以使用相同的操作继续处理,所以这里我们可以通过递归来做:
// 递归
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
temp = cur.next;// 先保存下一个节点
cur.next = prev;// 反转
// 更新prev、cur位置
prev = cur;
cur = temp;
return reverse(prev, cur);
}
}