目录
算法通关村第二关 —— 指定区间反转问题解析
在上一篇文章,我们已经学习了链表的反转,共有三种方法:通过建立虚拟头结点辅助反转、直接操作链表实现反转、递归实现链表反转。今天我们来学习链表在指定区间的反转,给定单链表的头指针 head 和两个正数 left 和 right, 其中 left <= right, 我们要做的就是反转从 left 位置到 right 的链表结点并返回反转后的链表,如下图所示:
其实解决思路还是很简单的,原本是反转一整个链表,而现在只需反转指定区间的链表,然后再和原先不需反转的结点连接上就行了,下面我们来学习两种方法,分别是头插法和穿针引线法。
方法一 头插法
该方法比较直观简单,其实就是在反转的区间内,不断把下一个结点移到前一个结点来,最终实现链表区间的反转。但缺点就是,如果left和right区域很大,比如恰好是链表的头结点和尾结点,那么找到left 和 right 需要遍历一次,反转链表又需要遍历一次,虽然总的时间复杂度为O(N), 但是遍历了两次链表,可不可以只遍历一次呢?其实是可以的,我们以下面的图示来说明:
一开始我们先遍历链表,直到遍历到指定反转的区间,此时每遍历到一个结点,将新结点移到反转区间的其实位置即可,整个流程如下图所示:
整个流程非常简单,前面就是带虚拟结点的插入操作,当然每走一步都要考虑各种指针怎么指,既要将结点插入到对应的位置上,同时还要保证后续节点能够被找到。整体的代码及注释如下:
public ListNode reverseBetween(ListNode head, int left, int right) {
// 建立虚拟节点并指向头结点
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
// 遍历链表到反转区间的前一个接待你
for(int i = 0; i < left - 1; i++){
pre = pre.next;
}
// 反转区间最左端left
ListNode cur = pre.next;
ListNode next;
// 每一步的调整过程
for(int i = 0; i < right -left; i++){
// 即将进行调整的的结点
next = cur.next;
// cur指向其下下个结点,拆出即将调整的结点
cur.next = next.next;
// 插入的结点指向旧指定区间的第一个结点
next.next = pre.next;
// pre指向指定区间新的头结点
pre.next = next;
}
// 返回反转后链表的头结点
return dummy.next;
}
方法二 穿针引线法
这个方法顾名思义,针就是我们要反转的区间,线就是不需要反转的结点,我们只需要确定好反转的部分并对其进行反转,然后再和没有反转的结点连接起来,就类似穿针一般。此时由于链表区间是单独进行反转的,所以就可以复用我们上一篇文章链表反转的方法。这样子问题就变成了如何标记下图四个位置,以及如何反转left到right之间的链表了。
算法步骤流程为:
第一步: 确定待反转区间并对其单独进行反转,此时要注意尾结点指向null!否则会连同right后面不需要反转的结点一起反转,指向null后便可以只反转指定区间。
第二部:把pre的next指针指向反转后的链表头结点,把反转以后的链表尾结点的next指针指向succ。
整体的代码实现如下:
public static ListNode reverseBetween(ListNode head, int left, int right) {
// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode pre = dummyNode;
// 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
for (int i = 0; i < left - 1; i++) {
pre = pre.next;
}
// 第 2 步:从 pre 再走 right - left + 1 步,来到 right 节点
ListNode rightNode = pre;
for (int i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}
// 第 3 步:切断出一个子链表(截取链表)
ListNode leftNode = pre.next;
ListNode succ = rightNode.next;
// 该处便是将需要反转的链表切断出来,pre.next = null;其实可加可不加,
// 因为我们反转链表时会指定头结点
pre.next = null;
rightNode.next = null;
// 第 4 步:同第 206 题,反转链表的子区间
reverseList(leftNode);
// 第 5 步:接回到原来的链表中
pre.next = rightNode;
leftNode.next = succ;
return dummyNode.next;
}
/**
* 基本的反转方法
*
* @param head
* @return
*/
public static ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
总结
指定区间链表的反转本质上和链表反转没有什么区别,只不过让我们学会更加灵活地去操作使用链表,通过这个题型的学习也提升了我们以后应对不同情况处理链表的能力,本质上就是增删改查的基础,所以一定要脚踏实地,夯实基础。