之前的文章「遞歸反轉鏈表的一部分」講了如何遞歸地反轉一部分鏈表,有讀者就問如何迭代地反轉鏈表,這篇文章解決的問題也需要反轉鏈表的函數,我們不妨就用迭代方式來解決。
本文要解決「K 個一組反轉鏈表」,不難理解:

這個問題經常在面經中看到,而且 LeetCode 上難度是 Hard,它真的有那么難嗎?
對於基本數據結構的算法問題其實都不難,只要結合特點一點點拆解分析,一般都沒啥難點。下面我們就來拆解一下這個問題。
一、分析問題
首先,前文學習數據結構的框架思維提到過,鏈表是一種兼具遞歸和迭代性質的數據結構,認真思考一下可以發現這個問題具有遞歸性質。
什么叫遞歸性質?直接上圖理解,比如說我們對這個鏈表調用 reverseKGroup(head, 2)
,即以 2 個節點為一組反轉鏈表:

如果我設法把前 2 個節點反轉,那么后面的那些節點怎么處理?后面的這些節點也是一條鏈表,而且規模(長度)比原來這條鏈表小,這就叫子問題。

我們可以直接遞歸調用 reverseKGroup(cur, 2)
,因為子問題和原問題的結構完全相同,這就是所謂的遞歸性質。
發現了遞歸性質,就可以得到大致的算法流程:
1、先反轉以 head
開頭的 k
個元素。

2、將第 k + 1
個元素作為 head
遞歸調用 reverseKGroup
函數。

3、將上述兩個過程的結果連接起來。

整體思路就是這樣了,最后一點值得注意的是,遞歸函數都有個 base case,對於這個問題是什么呢?
題目說了,如果最后的元素不足 k
個,就保持不變。這就是 base case,待會會在代碼里體現。
二、代碼實現
首先,我們要實現一個 reverse
函數反轉一個區間之內的元素。在此之前我們再簡化一下,給定鏈表頭結點,如何反轉整個鏈表?
// 反轉以 a 為頭結點的鏈表
ListNode reverse(ListNode a) {
ListNode pre, cur, nxt;
pre = null; cur = a; nxt = a;
while (cur != null) {
nxt = cur.next;
// 逐個結點反轉
cur.next = pre;
// 更新指針位置
pre = cur;
cur = nxt;
}
// 返回反轉后的頭結點
return pre;
}

這次使用迭代思路來實現的,借助動畫理解應該很容易。
「反轉以 a
為頭結點的鏈表」其實就是「反轉 a
到 null 之間的結點」,那么如果讓你「反轉 a
到 b
之間的結點」,你會不會?
只要更改函數簽名,並把上面的代碼中 null
改成 b
即可:
/** 反轉區間 [a, b) 的元素,注意是左閉右開 */
ListNode reverse(ListNode a, ListNode b) {
ListNode pre, cur, nxt;
pre = null; cur = a; nxt = a;
// while 終止的條件改一下就行了
while (cur != b) {
nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
// 返回反轉后的頭結點
return pre;
}
現在我們迭代實現了反轉部分鏈表的功能,接下來就按照之前的邏輯編寫 reverseKGroup
函數即可:
ListNode reverseKGroup(ListNode head, int k) {
if (head == null) return null;
// 區間 [a, b) 包含 k 個待反轉元素
ListNode a, b;
a = b = head;
for (int i = 0; i < k; i++) {
// 不足 k 個,不需要反轉,base case
if (b == null) return head;
b = b.next;
}
// 反轉前 k 個元素
ListNode newHead = reverse(a, b);
// 遞歸反轉后續鏈表並連接起來
a.next = reverseKGroup(b, k);
return newHead;
}
解釋一下 for
循環之后的幾句代碼,注意 reverse
函數是反轉區間 [a, b)
,所以情形是這樣的:

遞歸部分就不展開了,整個函數遞歸完成之后就是這個結果,完全符合題意:

三、最后說兩句
從閱讀量上看,基本數據結構相關的算法文章看的人都不多,我想說這是要吃虧的。
大家喜歡看動態規划相關的問題,可能因為面試很常見,但就我個人理解,很多算法思想都是源於數據結構的。我們公眾號的成名之作之一,「學習數據結構的框架思維」就提過,什么動規、回溯、分治算法,其實都是樹的遍歷,樹這種結構它不就是個多叉鏈表嗎?你能處理基本數據結構的問題,解決一般的算法問題應該也不會太費事。
那么如何分解問題、發現遞歸性質呢?這個只能多練習,也許后續可以專門寫一篇文章來探討一下,本文就到此為止吧,希望對大家有幫助!
我最近精心制作了一份電子書《labuladong的算法小抄》,分為【動態規划】【數據結構】【算法思維】【高頻面試】四個章節,共 60 多篇原創文章,絕對精品!限時開放下載,在我的公眾號 labuladong 后台回復關鍵詞【pdf】即可免費下載!
歡迎關注我的公眾號 labuladong,技術公眾號的清流,堅持原創,致力於把問題講清楚!