利用遞迴方法實現連結串列反轉、前N個節點反轉以及中間部分節點反轉
阿新 • • 發佈:2020-09-06
### 一、反轉整個連結串列
**問題**:定義一個函式,輸入一個連結串列的頭節點,反轉該連結串列並輸出反轉後連結串列的頭節點。
**示例:**
```java
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL
```
```java
//單鏈表的實現結構
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x;}
}
```
反轉連結串列利用迭代不難實現,如果使用遞迴則有些許難度。
**首先來看原始碼實現:**
```java
ListNode reverse(ListNode head) {
if(head == null || head.next == null)
return head;
ListNode ret = reverse(head.next);
head.next.next = head;
head.next = null;
return ret;
}
```
是否看起來不知所云,而又被這如此簡潔的程式碼所震撼?讓我們一起探索一下其中的奧祕。
**對於遞迴演算法,最重要的是明確遞迴函式的定義。**
我們的`reverse`函式的定義如下:
**輸入一個節點`head`,將以`head`為起點的連結串列反轉,並返回反轉之後的頭節點。**
![](https://img2020.cnblogs.com/blog/2134418/202009/2134418-20200905222603142-1095795832.gif)
明白了函式的定義後,在來看這個問題。比如我們想反轉這個連結串列
![截圖2020-09-05 21.06.50](https://tva1.sinaimg.cn/large/007S8ZIlgy1gig4tmomavj32990u0dj9.jpg)
那麼輸入`reverse(head)`後,會在`ListNode ret = reverse(head.next);`進行遞迴
**不要跳進遞迴!**(你的腦袋能壓幾個棧呀?)
根據`reverse`函式的定義,函式呼叫後會返回反轉之後的頭節點,我們用變數`ret`接收
![截圖2020-09-05 21.14.18](https://tva1.sinaimg.cn/large/007S8ZIlgy1gig4tm51hlj317k0g4wfo.jpg)現在再來看一下程式碼
`head.next.next = head;`
![截圖2020-09-05 21.16.19](https://tva1.sinaimg.cn/large/007S8ZIlgy1gig4tjjd3gj31860ieq4e.jpg)
接下來:
```java
head.next = null;
return ret;
```
![截圖2020-09-05 21.18.16](https://tva1.sinaimg.cn/large/007S8ZIlgy1gig4tlhsaij317m0he3zw.jpg)
再跳出這層遞迴就會得到:
![截圖2020-09-05 21.19.41](https://tva1.sinaimg.cn/large/007S8ZIlgy1gig4tirt6hj318s0hyq47.jpg)
神不神奇,這樣整個連結串列就反轉過來了!
遞迴程式碼就是這麼簡潔優雅,但要注意兩個問題:
1、遞迴函式要有base case,不然就會一直遞迴,導致棧溢位
`if (head == null || head.next == null) return head;`
即連結串列為空或只有一個節點,直接返回
2、當連結串列遞迴反轉後,新的頭節點為`ret`,而`head`變成了最後一個節點,應該令連結串列的某尾指向`null`
`head.next = null;`
理解這兩個問題之後,我們可以進一步深入研究連結串列反轉的問題,接下來的問題其實均為在這個演算法上的擴充套件。
### 二、反轉連結串列前N個節點
接下來我們來看這個問題:
**問題:**反轉連結串列前N個節點,並返回連結串列頭節點
**說明:**1 <= N <= 連結串列長度
**示例:**
```java
輸入: 1->2->3->4->5->NULL, n = 4
輸出: 4->3->2->1->5->NULL
```
解決思路和反轉整個連結串列差不多,只需稍加修改
```java
ListNode successor = null; // 後驅節點(第 n + 1 個節點)
ListNdoe reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next;
return head;
}
// 以 head.next 為起點,需要反轉前 n - 1 個節點
ListNode ret = reverseN(head.next, n - 1);
head.next.next = head;
head.next = successor; // 將反轉後的 head 與後面節點連線
return ret;
}
```
具體區別:
1、base case 變為`n == 1`, 同時需要**記錄後驅節點**。
2、之前把`head.next` 設定為`null`,因為整個連結串列反轉後,`head`變為最後一個節點。
現在`head`節點在遞迴反轉後不一定為最後一個節點,故應記錄後驅`successor`(第 n + 1 個節點), 反轉之後將`head`連線上。
![](https://img2020.cnblogs.com/blog/2134418/202009/2134418-20200905222640702-566505710.gif)
OK,如果這個函式你也能看懂,就離實現**反轉一部分連結串列**不遠了。
### 三、反轉連結串列的一部分
現在我們開始解決這個問題,給一個索引區間`[m, n]`(索引從1開始),僅僅反轉區間中的連結串列元素。
**說明:**1 <= m <= n <= 連結串列長度
**示例:**
```java
輸入: 1->2->3->4->5->NULL, m = 2, n = 4
輸出: 1->4->3->2->5->NULL
```
猛一看很難想到思路。
試想一下,如果`m == 1`,就相當於反轉連結串列的前 n 元素嘛,也就是我們剛才實現的功能:
```java
ListNode reverseBetween(ListNode head, int m, int n) {
//base case
if (m == 1) {
return reverseN(head, n); // 相當於反轉前 n 個元素
}
// ...
}
```
那如果`m != 1` 該怎麼辦?
如果把`head`的索引視為`1`,那麼我們是想從第`m`個元素開始反轉;
如果把`head.next`的索引視為`1`,那麼我們是想從第`m - 1` 個元素開始反轉;
如果把`head.next.next`的索引視為`1`,那麼我們是想從第`m - 2` 個元素開始反轉;
......
區別於迭代思想,這就是遞迴的思想,所以我們可以完成程式碼:
```java
ListNode reverseBetween(ListNode head, int m, int n) {
// base case
if (m == 1) {
return reverseN(head, n);
}
// 遞迴前進到觸發 base case (m == 1)
head.next = reverseBetween(head.next, m - 1, n - 1);
return head;
}
```
至此,我們終於幹掉了