原题链接:leetcode-92

迭代-移动结点式反转

1、如果只需反转单链表中的一部分呢?如反转单链表的m-n区间
使用迭代的方式在顺向迭代反转的时候,链表反转的结点处理和上面的顺向遍历时修改指向略微不一样

顺向遍历时修改指向 解法图示:
image.png

由于这种改变反转的方式,在链尾时才能完全反转,在实现反转的中途链表是混乱的,甚至是断开的。如果要在这个算法思路上改造适合指定区间的反转,需要在反转指定区间后,额外处理头结点的指向、已反转链表与原链表的焊接的问题,改造代价略高。

我们换一种反转的操作思路,从而实现指定区间的反转:

  1. 输入 1->2->3->4->5->null m=2n=3
  2. 期望 1->4->3->2->5->null

下面是该算法的处理过程:
image.png

该算法从“移动”结点的角度出发,通过断链、改变指向的方式实现了结点的移动,核心是head结点指向永远不变,head的next结点不断移动(交换)到指定反转区域的首部,head结点不断往后移,head.next不断往前移,中间保持其他结点的相对位置,从而实现反转。

以1->2->3->4->5->6->null, n=2,m=5为例,链表变化过程如下
image.png

完整代码如下:

  1. function reverseBetween(head, m, n) {
  2. let dummy = new ListNode();
  3. dummy.next = head;
  4. let prev = dummy;
  5. // head指向m所在结点,prev指向head前的结点
  6. for (let i = 1; i < m; i++) {
  7. prev = prev.next;
  8. }
  9. head = prev.next;
  10. // 开始反转
  11. // 我们需要反转n-m次,head结点指向永远不变,head的next结点不断移动(交换)到指定反转区域的首部
  12. for(let i=m; i<n; i++) {
  13. // 注意顺序
  14. let next_temp = head.next;
  15. head.next = next_temp.next;
  16. next_temp.next = prev.next;
  17. prev.next = next_temp;
  18. }
  19. return dummy.next;
  20. }

递归-从函数定义上理解

回顾反转链表里的递归解法(点我),我们当时是以回溯的思想来思考的,我们将当递归到达base出口弹栈的过程理解为反向遍历,在弹递归栈的时候实现了反转。
反转链表-指定区间 - 图4

同样的代码,我们试试从递归函数定义的角度上理解

  1. funciton reverse(ListNode head) {
  2. if (head.next == null) return head;
  3. let last = reverse(head.next);// 链表新头结点(原链尾)
  4. head.next.next = head;
  5. head.next = null;
  6. return last;
  7. }

image.png

reverse函数定义理解:输入一个节点head,将「以head为起点」的链表反转,并返回反转之后的头结点。
所以执行完reverse(head.next)后,链表变为这个样子:
image.png
下面开始改变结点之间的链接
image.png

image.png

同样head.next=null不仅是为了断链,且是因为指向null代表结点为链尾
(因为反转的是整个链表,所以原表头肯定是新链表的链尾了)


然后我们再根据此法进阶,解出指定区间反转链表
对于函数reverseBetween(head, m, n)我们找到递归出口,m=1时即,反转链表的前n个
现在我们来解反转链表前n个:
image.png

解决思路和反转整个链表差不多,不过1作为head最后指向的不是null,而是链表的n结点的下一个元素

  1. let tail_next = null;// 反转区域的链表尾的下一个对接结点
  2. function reverseN(head, n) {
  3. if (n === 1) {
  4. tail_next = head.next;
  5. return head;
  6. }
  7. // 链表新头结点
  8. let last = reverseN(head.next, n - 1);
  9. head.next.next = head;
  10. head.next = tail_next; // 对比反转整个链表时 此处为null,反转区间时应该继续接入到链表里去
  11. return last;
  12. }

而实现m-n之间的完整代码

  1. function reverseBetween(head, m, n) {
  2. let tail_next = null;// 反转区域的链表尾的下一个对接结点
  3. if (m == 1) {
  4. return reverseN(head, n);
  5. }
  6. // 前进到反转的起点触发 base case
  7. head.next = reverseBetween(head.next, m - 1, n - 1);// n在前n个里使用的计数
  8. return head;
  9. function reverseN(head, n) {
  10. if (n === 1) {
  11. tail_next = head.next;
  12. return head;
  13. }
  14. // 链表新头结点
  15. let last = reverseN(head.next, n - 1);
  16. head.next.next = head;
  17. head.next = tail_next; // 对比反转整个链表时 此处为null,反转区间时应该继续接入到链表里去
  18. return last;
  19. }
  20. }

最后

参考

递归反转链表:如何拆解复杂问题

引申

递归实现链表