摘自labuladong算法小抄,使用go語言從新描述算法
以前的文章「遞歸反轉鏈表的一部分」講了如何遞歸地反轉一部分鏈表,有讀者就問如何迭代地反轉鏈表,這篇文章解決的問題也須要反轉鏈表的函數,咱們不妨就用迭代方式來解決。數據結構
本文要解決「K 個一組反轉鏈表」,不難理解:框架
這個問題常常在面經中看到,並且 LeetCode 上難度是 Hard,它真的有那麼難嗎?函數
對於基本數據結構的算法問題其實都不難,只要結合特色一點點拆解分析,通常都沒啥難點。下面咱們就來拆解一下這個問題。學習
首先,前文學習數據結構的框架思惟提到過,鏈表是一種兼具遞歸和迭代性質的數據結構,認真思考一下能夠發現這個問題具備遞歸性質。動畫
什麼叫遞歸性質?直接上圖理解,好比說咱們對這個鏈表調用 reverseKGroup(head, 2)
,即以 2 個節點爲一組反轉鏈表:spa
若是我設法把前 2 個節點反轉,那麼後面的那些節點怎麼處理?後面的這些節點也是一條鏈表,並且規模(長度)比原來這條鏈表小,這就叫子問題。3d
咱們能夠直接遞歸調用 reverseKGroup(cur, 2)
,由於子問題和原問題的結構徹底相同,這就是所謂的遞歸性質。指針
發現了遞歸性質,就能夠獲得大體的算法流程:code
一、先反轉以 head
開頭的 k
個元素。
二、將第 k + 1
個元素做爲 head
遞歸調用 reverseKGroup
函數。
三、將上述兩個過程的結果鏈接起來。
總體思路就是這樣了,最後一點值得注意的是,遞歸函數都有個 base case,對於這個問題是什麼呢?
題目說了,若是最後的元素不足 k
個,就保持不變。這就是 base case,待會會在代碼裏體現。
首先,咱們要實現一個 ReverseSingleList
函數反轉一個區間以內的元素。在此以前咱們再簡化一下,給定鏈表頭結點,如何反轉整個鏈表?
// 反轉以 a 爲頭結點的鏈表 func ReverseSingleList(a *ListNode) *ListNode { var ( pre, cur, nxt *ListNode ) pre = nil cur = a nxt = a for cur != nil { nxt = cur.next // 逐個結點反轉 cur.next = pre // 更新指針位置 pre = cur cur = nxt } // 返回反轉後的頭結點 return pre }
此次使用迭代思路來實現的,藉助動畫理解應該很容易。
「反轉以 a
爲頭結點的鏈表」其實就是「反轉 a
到 null 之間的結點」,那麼若是讓你「反轉 a
到 b
之間的結點」,你會不會?
只要更改函數簽名,並把上面的代碼中 null
改爲 b
便可:
/** 反轉區間 [a, b) 的元素,注意是左閉右開 */ func ReverseSingleList(a *ListNode, b *ListNode) *ListNode { var ( pre, cur, nxt *ListNode ) pre = nil cur = a nxt = a //終止的條件改一下就好了 for cur != b { nxt = cur.next // 逐個結點反轉 cur.next = pre // 更新指針位置 pre = cur cur = nxt } // 返回反轉後的頭結點 return pre }
如今咱們迭代實現了反轉部分鏈表的功能,接下來就按照以前的邏輯編寫 reverseKGroup
函數便可:
func ReverseKGroup(head *ListNode, k int) *ListNode { if head == nil { return nil } // 區間 [a, b) 包含 k 個待反轉元素 var ( a, b *ListNode ) b = head a = head for i := 0; i < k; i++ { // 不足 k 個,不須要反轉,base case if b == nil { return head } b = b.next } // 反轉前 k 個元素 newHead := ReverseSingleList(a, b) // 遞歸反轉後續鏈表並鏈接起來 a.next = ReverseKGroup(b, k) return newHead }
解釋一下 for
循環以後的幾句代碼,注意 ReverseSingleList函數是反轉區間 [a, b)
,因此情形是這樣的:
遞歸部分就不展開了,整個函數遞歸完成以後就是這個結果,徹底符合題意: