完虐链表(二)之反转区间链表_Merciful
题目描述:
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
示例 1:
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]
示例 2:
输入:head = [5], left = 1, right = 1
输出:[5]
提示:
链表中节点数目为 n
1 <= n <= 500
-500 <= Node.val <= 500
1 <= left <= right <= n
(一)常规解法之穿针引线
穿针引线的本质是截断链表,把链表分成三部分,需要反转的部分,需要反转的前一部分,需要反转的后一部分。
我们假设left=3,right=5,
在需要反转的节点区间,我们分别定义左节点为leftNode,右节点为rightNode。左节点的前驱节点定义为pre,右节点的后继节点定义为succ。
接下来截断,令2,5节点暂时指向null。并将中间部分完成反转。
然后令pre节点指向反转区间新的头节点,令反转区间的尾节点指向succ节点即可
具体代码如下:
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
//定义一个傀儡节点,防止当left=1时带来的边界检测
ListNode newHead=new ListNode(-1);
newHead.next=head;
//前驱节点
ListNode pre=newHead;
//用for循环找到前驱节点
for(int i=0;i<left-1;i++){
pre=pre.next;
}
ListNode leftNode=pre.next;
ListNode rightNode=pre;
//前驱节点向后移动right-(left-1)步即右节点
for(int i=0;i< right-left+1;i++){
rightNode=rightNode.next;
}
//后继节点
ListNode succ=rightNode.next;
//开始截断链表
pre.next=null;
rightNode.next=null;
//反转后区间的新的头节点是右节点,开始反转
rightNode=reverseList(leftNode);
//将反转后的部分进行连接
pre.next=rightNode;
leftNode.next=succ;
//最后返回傀儡节点
return newHead.next;
}
//递归反转,在上一篇博客中已详细介绍,在这里直接套用,非常简单
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
(二)“头插法”
解法一有个比较明显的缺点,就是当left和right直接距离过大,恰好是链表的头节点和尾节点时,for循环遍历找到左节点和右节点时需要遍历整个链表,在将其反转时又需要遍历整个链表。遍历了两遍,虽然时间复杂度没有改变,但我们仍然可以进行优化。第二种方法可以看成头插法。
具体过程如下:
具体代码如下:
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
//定义头节点dummyHead。
ListNode dummyHead=new ListNode(-1);
dummyHead.next=head;
//初始化两个指针
ListNode p=dummyHead;
ListNode g=head;
//用for循环让p,g指针走向对应的位置
for(int i=0;i<left-1;i++){
p=p.next;g=g.next;
}
//用头插法 插入节点
for(int j=0;j<right-left;j++){
ListNode remove=g.next;
g.next=g.next.next;
remove.next=p.next;
p.next=remove;
}
return dummyHead.next;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
(三)“容器法”
解法三是将需要反转的链表区间拿出来,放到一个“容器”里。不同的“容器”有不同的特点,如果放到栈里,那么特点就是先入后出,先顺序放入,再将栈顶元素顺序弹出,如果放到数组里,那么特点就是顺序连接,通过访问下标可以快速修改链表节点的指向。
具体过程及代码如下:
用栈反转链表依然如下图,这次我们需要用while循环一个count变量,每循环一次count+1,当count>left,进入反转区间时,就开始放入栈,当count>=right时,就停止放入栈。然后将放入的节点像下图一样反转,最后拼接到原链表。
Ⅰ、用栈
class Solution {
public ListNode reverseBetween(ListNode head, int m,int n) {
//初始化创建一个栈
Stack<ListNode> stack=new Stack<>();
//定义傀儡节点,同上,防止m=1时可能带来的讨论
ListNode newHead=new ListNode(0);
newHead.next=head;
//定义count变量判断当前循环是否在反转链表区间
int count=1;
//定义cur指针,表示当前所在的节点
ListNode cur=head;
//cur节点的前驱节点
ListNode preCur=newHead;
ListNode rightNode=null;
//开始循环
while(cur!=null){
if(count<m){
//两个指针继续走一步
preCur=cur;
cur=cur.next;
}else{
if(count<=n){
stack.push(cur);
preCur.next=cur.next;
cur=cur.next;
}else{
rightNode=cur;
break;
}
}
count++;
}
//下面将栈中的节点依次弹出,反转链表
ListNode curs=stack.pop();
ListNode start=curs;//反转后新的头
while(!stack.isEmpty()){
ListNode tmp=stack.pop();
curs.next=tmp;
curs=curs.next;
}
//将反转后的链表与之前的链表连接
curs.next=rightNode;
preCur.next=start;
return newHead.next;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
Ⅱ、用数组
用数组比较麻烦的一个地方就是我们需要讨论left和right的边界检测,而优点就是一个循环就能非常简单的反转链表,并且用下标访问链表节点非常方便。
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right)
{
//定义temp指针,初始化指向head位置
ListNode temp=head;
List<ListNode> list=new ArrayList<>();
//将链表节点放入数组
while(temp!=null)
{
list.add(temp);
temp=temp.next;
}
//将left,right更新成数组下标
left=left-1;
right=right-1;
//将left至right之间的节点反转
for(int i=right;i>left;i--)
{
list.get(i).next=list.get(i-1);
}
//判断right是否位于链表的最后一个节点。
//如果是,则指向null,如果不是,则指向下一个节点
list.get(left).next=(right==list.size()-1?null:list.get(right+1));
//判断left是否位于第一个节点
//如果是,则直接将链表的最后一个节点当作新的头返回
//如果不是,则将left的前驱节点指向right节点
if(left==0)
{
return list.get(right);
}
else{
list.get(left-1).next=list.get(right);
return head;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
(四)递归(稍复杂)
本题的递归分两种情况,一种情况是将位于left之前的节点进行递归,另一种情况是将left及left之后的节点,即进入反转部分后进行的递归。
具体过程及代码如下:
对于节点1来说,它不需要关心之后的部分是怎么反转的,它只需要调用递归,并得到其返回的结果:将之后所有的链表完成反转,并返回头节点。
我们只需要将返回的头节点挂在节点1之后。
同理,每一次递归我们只需要将得到的头节点挂在调用递归的节点后就可以,对应代码如下:
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode between=reverseBetween(head.next,m-1,n-1);
head.next=between;
return head;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
第一次递归处理:反转链表1->2->3->4->5->6->null,m=3,n=5之间的部分
第二次递归处理:反转链表2->3->4->5->6->null,m=2,n=4之间的部分
第三次递归处理:反转链表3->4->5->6->null,m=1,n=3之间的部分
而此时第三次递归到达left的位置,节点3本身也要反转,所以递归要发生变化,所以第一种类型的递归在m==1是发生终止,对应代码如下:
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
if(m==1){
return 未知;
}
ListNode between=reverseBetween(head.next,m-1,n-1);
head.next=between;
return head;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
当递归到left的位置时,我们只需反转前n个节点,所以第二个递归有两个参数:头节点和n。其他过程如下:
全部代码如下:
class Solution {
public ListNode reverseBetween(ListNode head, int m, int n) {
//递归终止条件
if(m==1){
return reverseTop(head,n);
}
ListNode between=reverseBetween(head.next,m-1,n-1);
head.next=between;
return head;
}
ListNode succ=null;
public ListNode reverseTop(ListNode head,int n){
//递归终止条件
if(n==1){
succ=head.next;
return head;
}
ListNode newHead=reverseTop(head.next,n-1);
head.next.next=head;
head.next=succ;
return newHead;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26