Golang從合併鏈表聊遞歸

從合併鏈表聊遞歸

遞歸是工程師最常見的一種解決問題的方式,但是有時候不容易真正掌握。有人說是看起來很簡單,自己寫起來會費點勁。

最著名的例子就是斐波那契數列(Fibonacci sequence),通過尋找遞推公式來計算出結果。
而最近刷到的一道合併鏈表的演算法題,也可以使用遞歸來實現。下面看看題目描述吧:

將兩個升序鏈表合併為一個新的 升序 鏈表並返回。新鏈表是通過拼接給定的兩個鏈表的所有節點組成的。

示例:

輸入:1->2->4, 1->3->4
輸出:1->1->2->3->4->4

來源:力扣(LeetCode)

先拋出本人觀點,遞歸的關鍵是:找到邊界條件遞歸公式

分析一下題目,可以發現用第一個鏈表l1的頭部節點來去和l2的節點對比,如果大於l2的當前節點,那麼偏移l1的next和l2繼續對比大小。反之如果l1的頭節點對比L2的當前節點更小,那麼就需要對l2做類似處理。
這種不斷對比和偏移的過程,可以總結出一種遞歸公式。
用偽程式碼寫法就是:

if l1.val < l2.val:
    l1.next = mergeTwoList(l1.next, l2)
    
    return l1
else:
   l2.next = mergeTwoList(l1, l2.next)
   return l2

而邊界條件就是在不斷偏移的時候,走到某個鏈表的最後一個節點為止,偽程式碼就是:

if l1 === null:
    return l2

if l2 === null:
    return l1

用golang來實現,程式碼也很清晰:

type ListNode struct {
	Val int
	Next *ListNode
}

func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
	if l1 == nil {
		return l2
	}

	if l2 == nil {
		return l1
	}

	if l1.Val < l2.Val {
		l1.Next = mergeTwoLists(l1.Next, l2)
		return l1
	} else {
		l2.Next = mergeTwoLists(l1, l2.Next)
		return l2
	}
}

在LeetCode裡面提交,運行回饋如下:

執行結果:
通過
顯示詳情
執行用時:
0 ms
, 在所有 Go 提交中擊敗了
100.00%
的用戶
記憶體消耗:
2.6 MB
, 在所有 Go 提交中擊敗了
63.64%
的用戶

可以看到遞歸是非常消耗記憶體的,它循環調用,猶如爾羅斯套娃,一層一層返回內層的調用結果。

如果要優化的話可以使用迭代方式來實現,程式碼需要做一些調整:

func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
    head := &ListNode{}
	result := head
	for l1 != nil && l2 != nil {
		if l1.Val < l2.Val {
			head.Next = l1
			head = head.Next
			l1 = l1.Next
		} else {
			head.Next = l2
			head = head.Next
			l2 = l2.Next
		}
	}

	if l1 == nil {
		head.Next = l2
	}

	if l2 == nil {
		head.Next = l1
	}

	return result.Next
}

可以看出需要創建一個頭部指針來做偏移,而最終result作為一個合成結果鏈表來存儲結果。
最後提交執行,發現結果數據稍微好看了一丟丟:

執行用時:
4 ms
, 在所有 Go 提交中擊敗了
62.28%
的用戶
記憶體消耗:
2.5 MB
, 在所有 Go 提交中擊敗了
100.00%
的用戶

由於在數據量不大的情況下,其實性能差距也不大,所以使用遞歸也是沒有毛病的。