在上章我介绍了第一种即完全反转的算法,在本章则会重点讲解第二种即指定区间反转的算法(注:我不仅仅只是给出时间复杂度和空间复杂度最低的算法,有一些容易理解的算法我也会给出,可以帮助大家从不同的视角理解问题,但是最优解我一般会给出标注、具体示例以及对应的Java代码)
2.1 直接迭代反转(本方法很容易理解,但在具体处理上可能有很多小细节)
- 本章是对于指定区间进行反转,如果要联系上一章的知识,那么我们可以将该局部部分先断开当作完整链表来进行反转
- 再将已经反转好的局部链表与断开的节点建立连接,再重构链表
本方法较容易理解,但不太推荐,因为要断开再链接的话可能需要多个结点的辅助。
- 时间复杂度: O(n),其中 n 是链表节点的数量。
- 空间复杂度: O(1),因为只使用了几个额外的变量。
2.2 头插迭代法(与法一类似,但是并没有断开、连接,而是使用头插法的思想直接就地逆置,本人认为最优解)
- 假设链表为
1 -> 2 -> 3 -> 4 -> 5
,m = 2, n = 4
- dummy 节点指向 head,用于处理
m = 1
的特殊情况。
-
初始化:
dummy -> 1 -> 2 -> 3 -> 4 -> 5
prev
-
prev
定位到第m-1
个节点(节点1)
dummy -> 1 -> 2 -> 3 -> 4 -> 5
prev
-
开始反转:
- 第一次反转后:
dummy -> 1 -> 3 -> 2 -> 4 -> 5
prev
- 第二次反转后:
dummy -> 1 -> 4 -> 3 -> 2 -> 5
prev
- 第一次反转后:
反转完成。
对于该方法,有一个很好的记忆方式,比如对于上述例子,我们要把2、3、4进行反转,那么每次迭代的时候都把2后面的结点进行头插操作,这样的话就变成了1、3、2、4、5,下一次迭代,则是再把2后面的结点进行头插操作,变成1、4、3、2、5,依次进行迭代,直到全部完成。
Java代码示例:
public ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode prev = dummy;
// 定位到第m-1个节点
for (int i = 0; i < m - 1; i++) {
prev = prev.next;
}
ListNode current = prev.next;
ListNode next;
// 反转第m到第n个节点
for (int i = 0; i < n - m; i++) {
next = current.next;
current.next = next.next;
next.next = prev.next;
prev.next = next;
}
return dummy.next;
}
- 时间复杂度: O(n),其中 n 是链表节点的数量。
- 空间复杂度: O(1),因为只使用了几个额外的变量。
2.3 递归求解
本次递归求解配合Java具体代码来进行分析;
Java代码如下:
public class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
public class Main {
public static ListNode reverseBetween(ListNode head, int m, int n) {
if (m == 1) {
return reverseN(head, n);
}
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
static ListNode successor = null; // 后驱节点
public static ListNode reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next;
return head;
}
ListNode last = reverseN(head.next, n - 1);
head.next.next = head;
head.next = successor;
return last;
}
public static void main(String[] args) {
// 测试代码
}
}
假设链表是 1 -> 2 -> 3 -> 4 -> 5
,m = 2
, n = 4
。
Step 1: 我们首先减小问题规模,即我们先处理 reverseBetween(head.next, m - 1, n - 1)
。对于当前的 head
,它成为了较小问题里的 m = 1, n = 3
。
1 -> 2 -> 3 -> 4 -> 5
head
Step 2: 在较小的问题中,因为 m = 1
,我们会调用 reverseN
函数。开始局部反转 2 -> 3 -> 4
。
1 -> 2 -> 3 -> 4 -> 5
head
reverseN(2, 3)
- 当
n = 1
,我们设置successor = 4->next(即5)
。
Step 3: 当递归回溯,2 -> 3 -> 4
反转为 4 -> 3 -> 2
。
1 -> 4 -> 3 -> 2 -> 5
Step 4: 在回溯过程中,逐个设置正确的 next
指针,即 2->next = 5
,这是由 successor
保存的。
1 -> 4 -> 3 -> 2 -> 5
Step 5: 最后,返回新的局部反转后的链表头,即节点 4
。
反转完成。
- 时间复杂度: 因为我们只对链表进行了一次遍历,所以时间复杂度为 O(n)。
- 空间复杂度: 递归调用会使用堆栈空间,最坏情况下堆栈深度为 O(n)。