递归反转链表的一部分.md 15.5 KB
Newer Older
L
labuladong 已提交
1 2
# 递归反转链表的一部分 

3 4
<p align='center'>
<a href="https://github.com/labuladong/fucking-algorithm" target="view_window"><img alt="GitHub" src="https://img.shields.io/github/stars/labuladong/fucking-algorithm?label=Stars&style=flat-square&logo=GitHub"></a>
L
labuladong 已提交
5
<a href="https://appktavsiei5995.pc.xiaoe-tech.com/index" target="_blank"><img class="my_header_icon" src="https://img.shields.io/static/v1?label=精品课程&message=查看&color=pink&style=flat"></a>
6 7 8 9
<a href="https://www.zhihu.com/people/labuladong"><img src="https://img.shields.io/badge/%E7%9F%A5%E4%B9%8E-@labuladong-000000.svg?style=flat-square&logo=Zhihu"></a>
<a href="https://space.bilibili.com/14089380"><img src="https://img.shields.io/badge/B站-@labuladong-000000.svg?style=flat-square&logo=Bilibili"></a>
</p>

L
labuladong 已提交
10
![](https://labuladong.gitee.io/pictures/souyisou1.png)
11

L
labuladong 已提交
12
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.1,[手把手刷二叉树系列课程](https://aep.xet.tech/s/3YGcq3) 上线,[第 17 期刷题打卡挑战](https://aep.xet.tech/s/2jPp5X) 最后三天报名!另外,建议你在我的 [网站](https://labuladong.github.io/algo/) 学习文章,体验更好。**
13 14


L
labuladong 已提交
15 16 17 18 19 20 21 22 23

读完本文,你不仅学会了算法套路,还可以顺便解决如下题目:

| LeetCode | 力扣 | 难度 |
| :----: | :----: | :----: |
| [206. Reverse Linked List](https://leetcode.com/problems/reverse-linked-list/) | [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/) | 🟢
| [92. Reverse Linked List II](https://leetcode.com/problems/reverse-linked-list-ii/) | [92. 反转链表 II](https://leetcode.cn/problems/reverse-linked-list-ii/) | 🟠
| - | [剑指 Offer 24. 反转链表](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/) | 🟢
| - | [剑指 Offer II 024. 反转链表](https://leetcode.cn/problems/UHnkqh/) | 🟢
24 25 26

**-----------**

L
labuladong 已提交
27 28
反转单链表的迭代实现不是一个困难的事情,但是递归实现就有点难度了,如果再加一点难度,让你仅仅反转单链表中的一部分,你是否能**够递归实现**呢?

L
labuladong 已提交
29 30
> PS:迭代反转单链表的实现参见 [如何 k 个一组反转链表](https://labuladong.github.io/article/fname.html?fname=k个一组反转链表)。

L
labuladong 已提交
31 32 33 34 35 36 37 38 39 40 41
本文就来由浅入深,step by step 地解决这个问题。如果你还不会递归地反转单链表也没关系,**本文会从递归反转整个单链表开始拓展**,只要你明白单链表的结构,相信你能够有所收获。

```java
// 单链表节点的结构
public class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}
```

L
labuladong 已提交
42
什么叫反转单链表的一部分呢,就是给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变。
L
labuladong 已提交
43

L
labuladong 已提交
44 45
看下力扣第 92 题「反转链表 II」:

L
labuladong 已提交
46
![](https://labuladong.gitee.io/pictures/反转链表/title.png)
L
labuladong 已提交
47 48 49 50 51 52 53

**注意这里的索引是从 1 开始的**。迭代的思路大概是:先用一个 for 循环找到第 `m` 个位置,然后再用一个 for 循环将 `m``n` 之间的元素反转。但是我们的递归解法不用一个 for 循环,纯递归实现反转。

迭代实现思路看起来虽然简单,但是细节问题很多的,反而不容易写对。相反,递归实现就很简洁优美,下面就由浅入深,先从反转整个单链表说起。

### 一、递归反转整个链表

L
labuladong 已提交
54
这也是力扣第 206 题「反转链表」,递归反转单链表的算法可能很多读者都听说过,这里详细介绍一下,直接看代码实现:
L
labuladong 已提交
55 56

```java
L
labuladong 已提交
57
// 定义:输入一个单链表头结点,将该链表反转,返回新的头结点
L
labuladong 已提交
58
ListNode reverse(ListNode head) {
L
labuladong 已提交
59 60 61
    if (head == null || head.next == null) {
        return head;
    }
L
labuladong 已提交
62 63 64 65 66 67 68 69 70 71 72 73 74
    ListNode last = reverse(head.next);
    head.next.next = head;
    head.next = null;
    return last;
}
```

看起来是不是感觉不知所云,完全不能理解这样为什么能够反转链表?这就对了,这个算法常常拿来显示递归的巧妙和优美,我们下面来详细解释一下这段代码。

**对于递归算法,最重要的就是明确递归函数的定义**。具体来说,我们的 `reverse` 函数定义是这样的:

**输入一个节点 `head`,将「以 `head` 为起点」的链表反转,并返回反转之后的头结点**

L
labuladong 已提交
75
明白了函数的定义,再来看这个问题。比如说我们想反转这个链表:
L
labuladong 已提交
76

L
labuladong 已提交
77
![](https://labuladong.gitee.io/pictures/反转链表/1.jpg)
L
labuladong 已提交
78 79 80 81 82 83 84 85 86

那么输入 `reverse(head)` 后,会在这里进行递归:

```java
ListNode last = reverse(head.next);
```

不要跳进递归(你的脑袋能压几个栈呀?),而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果:

L
labuladong 已提交
87
![](https://labuladong.gitee.io/pictures/反转链表/2.jpg)
L
labuladong 已提交
88 89 90

这个 `reverse(head.next)` 执行完成后,整个链表就成了这样:

L
labuladong 已提交
91
![](https://labuladong.gitee.io/pictures/反转链表/3.jpg)
L
labuladong 已提交
92 93 94 95 96 97 98 99 100

并且根据函数定义,`reverse` 函数会返回反转之后的头结点,我们用变量 `last` 接收了。

现在再来看下面的代码:

```java
head.next.next = head;
```

L
labuladong 已提交
101
![](https://labuladong.gitee.io/pictures/反转链表/4.jpg)
L
labuladong 已提交
102 103 104 105 106 107 108 109

接下来:

```java
head.next = null;
return last;
```

L
labuladong 已提交
110
![](https://labuladong.gitee.io/pictures/反转链表/5.jpg)
L
labuladong 已提交
111 112 113 114 115 116

神不神奇,这样整个链表就反转过来了!递归代码就是这么简洁优雅,不过其中有两个地方需要注意:

1、递归函数要有 base case,也就是这句:

```java
L
labuladong 已提交
117 118 119
if (head == null || head.next == null) {
    return head;
}
L
labuladong 已提交
120 121
```

L
labuladong 已提交
122
意思是如果链表为空或者只有一个节点的时候,反转结果就是它自己,直接返回即可。
L
labuladong 已提交
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142

2、当链表递归反转之后,新的头结点是 `last`,而之前的 `head` 变成了最后一个节点,别忘了链表的末尾要指向 null:

```java
head.next = null;
```

理解了这两点后,我们就可以进一步深入了,接下来的问题其实都是在这个算法上的扩展。

### 二、反转链表前 N 个节点

这次我们实现一个这样的函数:

```java
// 将链表的前 n 个节点反转(n <= 链表长度)
ListNode reverseN(ListNode head, int n)
```

比如说对于下图链表,执行 `reverseN(head, 3)`

L
labuladong 已提交
143
![](https://labuladong.gitee.io/pictures/反转链表/6.jpg)
L
labuladong 已提交
144 145 146 147 148 149 150 151

解决思路和反转整个链表差不多,只要稍加修改即可:

```java
ListNode successor = null; // 后驱节点

// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
L
labuladong 已提交
152
    if (n == 1) {
L
labuladong 已提交
153 154 155 156 157 158 159 160 161 162 163
        // 记录第 n + 1 个节点
        successor = head.next;
        return head;
    }
    // 以 head.next 为起点,需要反转前 n - 1 个节点
    ListNode last = reverseN(head.next, n - 1);

    head.next.next = head;
    // 让反转之后的 head 节点和后面的节点连起来
    head.next = successor;
    return last;
L
labuladong 已提交
164
}
L
labuladong 已提交
165 166 167 168 169 170
```

具体的区别:

1、base case 变为 `n == 1`,反转一个元素,就是它本身,同时**要记录后驱节点**

L
labuladong 已提交
171
2、刚才我们直接把 `head.next` 设置为 null,因为整个链表反转后原来的 `head` 变成了整个链表的最后一个节点。但现在 `head` 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 `successor`(第 `n + 1` 个节点),反转之后将 `head` 连接上。
L
labuladong 已提交
172

L
labuladong 已提交
173
![](https://labuladong.gitee.io/pictures/反转链表/7.jpg)
L
labuladong 已提交
174 175 176 177 178

OK,如果这个函数你也能看懂,就离实现「反转一部分链表」不远了。

### 三、反转链表的一部分

L
labuladong 已提交
179
现在解决我们最开始提出的问题,给一个索引区间 `[m, n]`(索引从 1 开始),仅仅反转区间中的链表元素。
L
labuladong 已提交
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221

```java
ListNode reverseBetween(ListNode head, int m, int n)
```

首先,如果 `m == 1`,就相当于反转链表开头的 `n` 个元素嘛,也就是我们刚才实现的功能:

```java
ListNode reverseBetween(ListNode head, int m, int n) {
    // base case
    if (m == 1) {
        // 相当于反转前 n 个元素
        return reverseN(head, n);
    }
    // ...
}
```

如果 `m != 1` 怎么办?如果我们把 `head` 的索引视为 1,那么我们是想从第 `m` 个元素开始反转对吧;如果把 `head.next` 的索引视为 1 呢?那么相对于 `head.next`,反转的区间应该是从第 `m - 1` 个元素开始的;那么对于 `head.next.next` 呢……

区别于迭代思想,这就是递归思想,所以我们可以完成代码:

```java
ListNode reverseBetween(ListNode head, int m, int n) {
    // base case
    if (m == 1) {
        return reverseN(head, n);
    }
    // 前进到反转的起点触发 base case
    head.next = reverseBetween(head.next, m - 1, n - 1);
    return head;
}
```

至此,我们的最终大 BOSS 就被解决了。

### 四、最后总结

递归的思想相对迭代思想,稍微有点难以理解,处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。

处理看起来比较困难的问题,可以尝试化整为零,把一些简单的解法进行修改,解决困难的问题。

L
labuladong 已提交
222 223
值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以递归操作链表可以作为对递归算法的练习或者拿去和小伙伴装逼,但是考虑效率的话还是使用迭代算法更好。

L
labuladong 已提交
224
最后,我在数据结构精品课中讲解了 [单链表的递归实现](https://aep.h5.xeknow.com/s/1RQzXc),应该能够让你进一步加深对递归的理解。
L
labuladong 已提交
225

L
labuladong 已提交
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247


<hr>
<details>
<summary><strong>引用本文的文章</strong></summary>

 - [如何判断回文链表](https://labuladong.github.io/article/fname.html?fname=判断回文链表)
 - [烧饼排序算法](https://labuladong.github.io/article/fname.html?fname=烧饼排序)

</details><hr>




<hr>
<details>
<summary><strong>引用本文的题目</strong></summary>

<strong>安装 [我的 Chrome 刷题插件](https://mp.weixin.qq.com/s/X-fE9sR4BLi6T9pn7xP4pg) 点开下列题目可直接查看解题思路:</strong>

| LeetCode | 力扣 |
| :----: | :----: |
L
labuladong 已提交
248 249
| [2. Add Two Numbers](https://leetcode.com/problems/add-two-numbers/?show=1) | [2. 两数相加](https://leetcode.cn/problems/add-two-numbers/?show=1) |
| [445. Add Two Numbers II](https://leetcode.com/problems/add-two-numbers-ii/?show=1) | [445. 两数相加 II](https://leetcode.cn/problems/add-two-numbers-ii/?show=1) |
L
labuladong 已提交
250 251
| - | [剑指 Offer 24. 反转链表](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/?show=1) |
| - | [剑指 Offer II 024. 反转链表](https://leetcode.cn/problems/UHnkqh/?show=1) |
L
labuladong 已提交
252
| - | [剑指 Offer II 025. 链表中的两数相加](https://leetcode.cn/problems/lMSNwu/?show=1) |
L
labuladong 已提交
253 254 255 256 257

</details>



258
**_____________**
L
labuladong 已提交
259

L
labuladong 已提交
260
**《labuladong 的算法小抄》已经出版,关注公众号查看详情;后台回复关键词「**进群**」可加入算法群;回复「**全家桶**」可下载配套 PDF 和刷题全家桶**
L
labuladong 已提交
261

L
labuladong 已提交
262
![](https://labuladong.gitee.io/pictures/souyisou2.png)
L
labuladong 已提交
263 264


265 266
======其他语言代码======

B
brucecat 已提交
267 268 269 270
[92.反转链表II](https://leetcode-cn.com/problems/reverse-linked-list-ii/)



B
BruceCat 已提交
271 272
### c++

273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
[shilei](https://github.com/ShileiGuo) 提供C++解法代码:

思想:

   1.head表示需要反转的头节点,pre表示需要反转头节点的前驱节点

   2.对于从m到n的节点反转,需要反转n-m次,将head的next节点移动到需要反转链表部分的首部,需要反转链表部分剩余节点依旧保持相对顺序即可

   3.示例 当m=2, n=5时 

   第一次反转:1(pre) 2(head) 3(next) 4 5 反转为 1 3 2 4 5

   第二次反转:1(pre) 3 2(head) 4(next) 5 反转为 1 4 3 2 5

   第三次发转:1(pre) 4 3 2(head) 5(next) 反转为 1 5 4 3 2

```CPP
class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int m, int n) {
        //初始化哨兵节点
        ListNode* dummy=new ListNode(-1);
        //初始化待反转区间的前一个节点
        ListNode* pre=dummy;
        //哨兵节点下一个节点指向head头节点
        dummy->next=head;
        
        //获取待反转节点的前一个节点
        for(int i=0;i<m-1;i++)
            pre=pre->next;        
        //获取待反转节点的第一个节点
        head=pre->next;  
        //迭代反转n-m次,将head的next节点移动到需要反转链表部分的首部
        for(int i=m;i<n;i++){
            ListNode* t=head->next;       
            head->next=t->next;          
            t->next=pre->next;         
            pre->next=t;                
        }
        //返回哨兵节点
        return dummy->next;
    }
};
```
B
BruceCat 已提交
317 318

### python
A
Andrew 已提交
319 320 321 322
[DiamondI](https://github.com/DiamondI) 提供python3版本代码:

思路:递归。时间复杂度为O(n),由于递归调用需要借助栈的空间,因此空间复杂度亦为O(n)。

B
brucecat 已提交
323
```python
A
Andrew 已提交
324 325 326 327 328 329
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
B
brucecat 已提交
330 331 332
  def __init__(self):
    self.__successor = None

A
Andrew 已提交
333
    def __reverseN(self, head: ListNode, n: int) -> ListNode:
B
brucecat 已提交
334 335 336 337 338 339 340 341 342 343 344 345
      if n == 1:  
        # 记录第 n + 1 个节点
        self.__successor = head.next;
        return head;
      # 以 head.next 为起点,需要反转前 n - 1 个节点
      last = self.__reverseN(head.next, n - 1);

      head.next.next = head;
      # 让反转之后的 head 节点和后面的节点连起来
      head.next = self.__successor;
      return last;

A
Andrew 已提交
346
    def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
B
brucecat 已提交
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
      # base case
      if m == 1:
        return self.__reverseN(head, n);
      # 前进到反转的起点触发 base case
      head.next = self.reverseBetween(head.next, m - 1, n - 1);
      return head;
```



### javascript

**递归反转整个链表**

[206. 反转链表](https://leetcode-cn.com/problems/reverse-linked-list/)

```js
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    if (head == null || head.next == null) {
A
Andrew 已提交
370
        return head;
B
brucecat 已提交
371 372 373 374 375 376
    }
    const last = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return last;
};
A
Andrew 已提交
377
```
B
brucecat 已提交
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448



**反转链表前 N 个节点**

这题貌似在leetcode上没找到,就不贴链接了。

```js
let successor = null; // 后驱节点

let reverseListN = function(head, n) {
    if (n === 1) {
        // 记录第 n + 1 个节点
        successor = head.next;
        return head;
    }

    // 以 head.next 为起点,需要反转前 n - 1 个节点
    let last = reverseListN(head.next, n - 1);

    head.next.next = head;
    
    // 让反转之后的 head 节点和后面的节点连起来
    head.next = successor;
    return last;
};
```



**反转链表的一部分**

现在解决我们最开始提出的问题,给一个索引区间 `[m,n]`(索引从 1 开始),仅仅反转区间中的链表元素。

```js
let successor = null; // 后驱节点

let reverseListN = function(head, n) {
    if (n === 1) {
        // 记录第 n + 1 个节点
        successor = head.next;
        return head;
    }

    // 以 head.next 为起点,需要反转前 n - 1 个节点
    let last = reverseListN(head.next, n - 1);

    head.next.next = head;

    // 让反转之后的 head 节点和后面的节点连起来
    head.next = successor;
    return last;
};

/**
 * @param {ListNode} head
 * @param {number} m
 * @param {number} n
 * @return {ListNode}
 */
let reverseBetween = function(head, m, n) {
    // base case
    if (m === 1) {
        return reverseListN(head, n);
    }
    // 前进到反转的起点触发 base case
    head.next = reverseBetween(head.next, m - 1, n - 1);
    return head;
};
```