递归反转链表的一部分.md 14.9 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.github.io/algo/images/souyisou1.png)
11

L
labuladong 已提交
12
**通知:[数据结构精品课](https://aep.h5.xeknow.com/s/1XJHEO) 已更新到 V2.0;[第 13 期刷题打卡](https://mp.weixin.qq.com/s/eUG2OOzY3k_ZTz-CFvtv5Q) 最后一天报名!另外,建议你在我的 [网站](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 29 30 31 32 33 34 35 36 37 38 39
反转单链表的迭代实现不是一个困难的事情,但是递归实现就有点难度了,如果再加一点难度,让你仅仅反转单链表中的一部分,你是否能**够递归实现**呢?

本文就来由浅入深,step by step 地解决这个问题。如果你还不会递归地反转单链表也没关系,**本文会从递归反转整个单链表开始拓展**,只要你明白单链表的结构,相信你能够有所收获。

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

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

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

![](https://labuladong.github.io/algo/images/反转链表/title.png)
L
labuladong 已提交
45 46 47 48 49 50 51

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

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

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

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

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

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

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

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

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

L
labuladong 已提交
75
![](https://labuladong.github.io/algo/images/反转链表/1.jpg)
L
labuladong 已提交
76 77 78 79 80 81 82 83 84

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

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

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

L
labuladong 已提交
85
![](https://labuladong.github.io/algo/images/反转链表/2.jpg)
L
labuladong 已提交
86 87 88

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

L
labuladong 已提交
89
![](https://labuladong.github.io/algo/images/反转链表/3.jpg)
L
labuladong 已提交
90 91 92 93 94 95 96 97 98

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

现在再来看下面的代码:

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

L
labuladong 已提交
99
![](https://labuladong.github.io/algo/images/反转链表/4.jpg)
L
labuladong 已提交
100 101 102 103 104 105 106 107

接下来:

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

L
labuladong 已提交
108
![](https://labuladong.github.io/algo/images/反转链表/5.jpg)
L
labuladong 已提交
109 110 111 112 113 114

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

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

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

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

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

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

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

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

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

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

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

L
labuladong 已提交
141
![](https://labuladong.github.io/algo/images/反转链表/6.jpg)
L
labuladong 已提交
142 143 144 145 146 147 148 149

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

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

// 反转以 head 为起点的 n 个节点,返回新的头结点
ListNode reverseN(ListNode head, int n) {
L
labuladong 已提交
150
    if (n == 1) {
L
labuladong 已提交
151 152 153 154 155 156 157 158 159 160 161
        // 记录第 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 已提交
162
}
L
labuladong 已提交
163 164 165 166 167 168
```

具体的区别:

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

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

L
labuladong 已提交
171
![](https://labuladong.github.io/algo/images/反转链表/7.jpg)
L
labuladong 已提交
172 173 174 175 176

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

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

L
labuladong 已提交
177
现在解决我们最开始提出的问题,给一个索引区间 `[m, n]`(索引从 1 开始),仅仅反转区间中的链表元素。
L
labuladong 已提交
178 179 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

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

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

L
labuladong 已提交
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252


<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 | 力扣 |
| :----: | :----: |
| - | [剑指 Offer 24. 反转链表](https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/?show=1) |
| - | [剑指 Offer II 024. 反转链表](https://leetcode.cn/problems/UHnkqh/?show=1) |

</details>



253
**_____________**
L
labuladong 已提交
254

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

![](https://labuladong.github.io/algo/images/souyisou2.png)
L
labuladong 已提交
258 259


260 261
======其他语言代码======

B
brucecat 已提交
262 263 264 265
[92.反转链表II](https://leetcode-cn.com/problems/reverse-linked-list-ii/)



B
BruceCat 已提交
266 267
### c++

268 269 270 271 272 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
[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 已提交
312 313

### python
A
Andrew 已提交
314 315 316 317
[DiamondI](https://github.com/DiamondI) 提供python3版本代码:

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

B
brucecat 已提交
318
```python
A
Andrew 已提交
319 320 321 322 323 324
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
B
brucecat 已提交
325 326 327
  def __init__(self):
    self.__successor = None

A
Andrew 已提交
328
    def __reverseN(self, head: ListNode, n: int) -> ListNode:
B
brucecat 已提交
329 330 331 332 333 334 335 336 337 338 339 340
      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 已提交
341
    def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
B
brucecat 已提交
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
      # 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 已提交
365
        return head;
B
brucecat 已提交
366 367 368 369 370 371
    }
    const last = reverseList(head.next);
    head.next.next = head;
    head.next = null;
    return last;
};
A
Andrew 已提交
372
```
B
brucecat 已提交
373 374 375 376 377 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



**反转链表前 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;
};
```