链表问题1——递归反转链表
一.前言
之前系统的把二叉树的大部分问题都过了一遍,但是目前还没有系统的总结,过两天要再梳理一下,整一篇汇总博客,自己理理思路。现在开始链表问题的复习。
反转单链表的迭代实现不是一个困难的事情,但是递归实现就有点难度了。再难一点,如果只反转单链表中的一部分,是否能够递归实现。
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
迭代的思路:先用一个for循环找到第m个位置,再用一个for循环将m和n之间的元素反转。
迭代思路简单,但是细节问题很多;递归就要简洁很多。
二.递归反转整个链表
下面这个算法很经典,常用来显示递归的巧妙和优美。
ListNode reverse(ListNode head){
if(head.next==null) return head;
ListNode last = reverse(head.next);
head.next.next=head;
head.next = null;
return last;
}
再强调一次:递归算法最重要的就是明确递归函数的定义,具体到reverse函数:输入一个节点head,将以head为起点的链表反转,并返回反转之后的头结点。
我们不去深究具体的递归细节,下面这个代码,反正实现的就是把以head.next为起点的链表反转,并返回反转之后的头结点last。
ListNode last = reverse(head.next);
head.next指向的是2,将2.next再指向head,将原本的head变为尾部了。
head.next.next=head;
最后:
head.next =null;
return last;
至此,整个链表就反转过来了,在这里还有两个地方要注意:
- 递归算法,都要有一个base case,在这里就是:
if(head.next==null) return head;
- 还要重新将尾指针指向null。
三.反转链表前N个节点
// 将链表的前 n 个节点反转(n <= 链表长度)
ListNode reverseN(ListNode head, int n)
如:执行 reverseN(head,3)
ListNode successor = null; //后驱节点
//反转以head为起点的n个节点,返回新的头节点
ListNode reverseN(ListNode head,int n){
if(n==1){
//记录第n+1个节点,base case
successor = head.next;
return head;
}
//以head.next为起点,需要反转前n-1个节点
ListNode last = reverseN(head.next,n-1);
head.next.next=head;
head.next = successor;
return last;
}
和反转整个链表的区别:
- base case变为n=1,反转一个元素,就是自己本身,同时要记录后驱节点
- 反转整个链表直接将head.next=null;但是这里我们不知道head。next后面还有没有链表节点,所以要提前记录原本的下一节点successor,然后最后将其赋值给head.next。
四.反转链表的一部分
给一个索引区间[m,n](索引从 1 开始),仅仅反转区间中的链表元素。
ListNode reverseBetween(ListNode head, int m, int n)
如果m=1,就跟刚才的reverseN的情况一样,那如果m!=1,假设head.next的索引就是1,那么对应的head.next应该表示为从第m-1个元素开始的。。。依次递归:
ListNode reverseBetween(ListNode head, int m, int n){
if(m==1)
return reversN(head,n);
//前进到反转的起点触发base case
head.next = reverseBetween(head.next,m-1,n-1);
return head;
}
值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习,但是考虑效率的话还是使用迭代算法更好。
迭代解法:
在这里插入代码片