链表反转
反转链表涉及结点的增加、删除等多种操作,能非常有效考察思维能力和代码驾驭能力。另外很多题目也都要用它来做基础, 例如指定区间反转、链表K个一组翻转。还有一些在内部的某个过程用到了反转,例如两个链表生成相加链表。还有一种是链表排序的,也是需要移动元素之间的指针,难度与此差不多。因为太重要,所以我们用一章专门研究这个题目。
206. 反转链表:给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
1. 建立虚拟头结点辅助反转
链表插入元素处理头结点是个比较麻烦的问题——>可以先建立一个虚拟的结点ans,并且令ans.next=head
,这样可以很好的简化我们的操作。
如下图所示,如果我们将链表{1->2->3->4->5}进行反转,我们首先建立虚拟结点ans,并令ans.next=node(1)
,接下来我们每次从旧的链表拆下来一个结点接到ans后面,然后将其他线调整好就可以了。
// 方法1:虚拟结点法
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;
}
2. 直接操作链表实现反转
观察一下反转前后的结构和指针位置:
再看一下执行期间的过程示意图,在图中:
- cur本来指向旧链表的首结点
- pre表示已经调整好的新链表的表头
- next是下一个要调整的。
注意 图中箭头方向,cur和pre是两个表的表头,移动过程中cur经过一次中间状态之后,又重新变成了两个链表的表头。
class Solution {
public ListNode reverseList(ListNode head) {
//双指针
ListNode pre=null;
ListNode cur=head;
//注意不是cur.next!=null
while (cur != null) {
//反转指针
ListNode next=cur.next;//临时节点保存cur的下一个节点
cur.next=pre;
//移动指针
pre=cur;
cur=next;
}
return pre;
}
}
3. 使用递归
递归的本质是将一个大问题分解为一个个子问题的解决过程
- 首先,定义递归的边界条件。在本题中,当链表为空或只有一个节点时,无需反转,直接返回原链表头节点。
- 对于多于一个节点的情况,我们可以假设我们已经获得了反转后的子链表。那么我们只需要将当前节点的下一个节点指向当前节点,然后将当前节点的指针指向null,这样就实现了反转。
- 接下来,递归调用函数,传入当前节点的下一个节点,即
reverseList(head.next)
。这样,递归的过程会将子链表反转,并获得反转后的子链表的头节点。 - 最后,我们将反转后的子链表的头节点返回,作为新的链表头节点。
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null,head);
}
private ListNode reverse(ListNode pre,ListNode cur){
if (cur==null) {
return pre;
}
ListNode temp=null;
temp=cur.next;//先保存下一个节点
cur.next=pre;//反转一个节点
//依次递归
return reverse(cur,temp);
}
--------------------------------------------------------------------------------
//递归第2种
class Solution {
public 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;
}
}
- 时间复杂度: O(n), 要递归处理链表的每个节点
- 空间复杂度: O(n), 递归调用了 n 层栈空间
🎯如果将代码应用于链表1,2,3,4,5的反转
- 在递归过程中,我们会从最后一个节点5开始逆序处理,即先将节点5的前一个节点的指针指向它,然后将节点5的指针指向null;
- 接着,处理节点4的过程中,节点4的下一个节点是节点5,所以我们需要将节点5的指针指向节点4;
- 接着,处理节点3的过程中,节点3的下一个节点是节点4,所以我们需要将节点4的指针指向节点3……以此类推
- 最后处理完节点1的过程后,链表成功反转,新的链表头节点是节点5。
为什么cur一直指向5?
我们可以在代码中看到,每次递归调用时,都是传入当前节点的下一个节点作为参数,即 reverseList(head.next)
,因此代码中的 cur
变量指向的是最后一个节点,也就是反转后的链表头节点。
具体来说,当递归逐步返回时,我们会将递归返回值设置为新的链表头节点,这个新的链表头节点会一直传递到递归开始的地方。当递归结束时,整个链表的头节点就是最后一次递归调用的返回值,也就是 cur = 5
。
因此,在这个具体的例子中,代码中的 cur
指向的是最后一个节点5,这是因为它是反转后的链表头节点。
用的返回值,也就是 cur = 5
。
因此,在这个具体的例子中,代码中的 cur
指向的是最后一个节点5,这是因为它是反转后的链表头节点。