LeetCode 23 Hard,K個鏈表歸併

鏈接

https://leetcode.com/problems/merge-k-sorted-lists/

難度

Hard

描述

Merge k sorted linked lists and return it as one sorted list. Analyze and describe its complexity.

給定K個有序鏈表,要求將它所有的元素歸併到一個鏈表,並且有序。

樣例:

Input:  [     1->4->5,    1->3->4,    2->6  ]  Output: 1- >1->2->3->4->4->5->6

題解

按照慣例,我們還是先來看最簡單的解法,也就是暴力法。

暴力

這題當中,暴力的方法也很簡單,非常簡單粗暴,那就是先把所有元素取出來,排序之後再放到鏈表當中去。但是這麼做說實話有點脫褲子放屁,我們分析一下複雜度也會發現,假設所有的元素個數是n,那麼最後的複雜度應該就是排序所消耗的複雜度,也就是,和K沒有一點關係,而且我們也完全沒有用上這K個鏈表當中的元素都是有序的這個信息,顯然這是不科學的。

我們對上面的純暴力方法稍稍做一些優化,想辦法把K個鏈表當中元素有序的信息用上。用上的方法也很簡單,我們之前做歸併排序的時候曾經做過兩個數組的歸併,我們用同樣的方法,只不過我們這次換成是K個鏈表而已。也就是說我們每次遍歷這K個鏈表的頭元素,從其中取出最小的那個元素插入最後的結果鏈表當中,當所有鏈表為空的時候,說明所有的元素已經歸併完了,那麼進行返回。

我們來試着寫一下代碼:

class Solution:      def mergeKLists(self, lists: List[ListNode]) -> ListNode:          import sys            ret = ListNode(0)          pnt = ret            while True:              mini = sys.maxsize              node = None              # 遍歷所有的鏈表              for i, l in enumerate(lists):                  if l is None:                      continue                  if l.val < mini:                      mini = l.val                      node = i              # 如果node為None,說明所有的元素都已經歸併結束了              if node is None:                  break              pnt.next = ListNode(mini)              pnt = pnt.next              lists[node] = lists[node].next          return ret.next

這種算法的複雜度是多少呢?稍微算一下就知道,我們每一次選擇數的時候,都需要遍歷K個鏈表的頭指針。一共有n個元素,所以總體的複雜度是。和之前暴力的方法相比如何呢?其實是半斤八兩,這兩者誰大誰小完全取決於K和的大小關係。一般來說n不會超過一千萬,所以不會很大,粗略估算,不會超過30,而K很有可能超過30。也就是說大部分情況下這種方法的運算時間要比暴力還要長。

看起來這個用上了鏈表內元素大小關係濃眉大眼的歸併法,還不如之前簡單粗暴的暴力來得管用。實在是有點粉刺。

歸併

我們回想一下從前,在之前的問題當中,我們遇到比較多的往往是兩個數組的歸併,這次是K個鏈表,因此複雜度增加了許多。那麼我們能不能把這K個鏈表看成是兩兩鏈表的組合呢?我們每次將這些鏈表兩兩分組,然後歸併之後再次兩兩分組歸併,直到最後只剩下一個鏈表為止,這種方案會不會更優一些呢?

我們畫張圖來看看這一過程:

我們橫向來看,在每一次merging階段當中,我們都遍歷了全部的元素,所以每一層總體的複雜度是。那麼,我們一共又遍歷了多少層呢?也不難,我們每次merging,鏈表的數量都會減少一半。一共有K個鏈表,那麼顯然,應該要merging 次,也就是說有個merging階段,所以總體的複雜度是。由於K不會太大,所以這種方法顯然要優於和.

我們先來寫出兩個鏈表歸併的代碼,之後再遞歸的形式調用即可:

class Solution:        def mergeTwoList(self, l1, l2):          ret = ListNode(0)          pnt = ret          while True:              """              鏈表沒辦法像數組那樣在末尾插入標兵了              所以只能人工判斷是否為空的情況              """              if l1 is None and l2 is None:                  break              if l1 is None:                  pnt.next = ListNode(l2.val)                  pnt = pnt.next                  l2 = l2.next              elif l2 is None:                  pnt.next = ListNode(l1.val)                  pnt = pnt.next                  l1 = l1.next              else:                  if l1.val < l2.val:                      pnt.next = ListNode(l1.val)                      l1 = l1.next                  else:                      pnt.next = ListNode(l2.val)                      l2 = l2.next                  pnt = pnt.next          return ret.next        def dfs(self, lists, l, r):          """          遞歸合併K個鏈表          這裡的l和r維護的是閉區間          """          if l > r:              return None          if l == r:              return lists[l]          else:              mid = (l + r) // 2              return self.mergeTwoList(self.dfs(lists, l, mid), self.dfs(lists, mid+1, r))        def mergeKLists(self, lists: List[ListNode]) -> ListNode:          # 傳入l=0, r=len(lists)-1,因為是閉區間          return self.dfs(lists, 0, len(lists)-1)

優先隊列

到這裡還沒有結束,下面要介紹的是可以說是我個人覺得這題最棒的解法了,就是使用我們之前說的優先隊列。還記得嗎,優先隊列可以自動保證隊列當中的元素有序。只要利用優先隊列,然後將當前第一個元素作為優先級,讓優先隊列幫我們維護隊列當中的順序。通過使用優先隊列,我們的代碼無論是可讀性還是編碼複雜度都會大大降低。

如果有新關注的,或者是已經遺忘了優先隊列用法的同學可以點擊下方鏈接,回顧一下優先隊列的用法:Python應用——優先隊列與heapq

class Solution:        def mergeKLists(self, lists: List[ListNode]) -> ListNode:          class Node:              """              存儲在優先隊列當中的節點              """              def __init__(self, val, arr):                  self.val = val                  self.arr = arr                # 重載判斷大小的比較函數              def __lt__(self, other):                  return self.val < other.val            import heapq          arr = []          # 將所有鏈表插入優先隊列當中          for l in lists:              if l is not None:                  heapq.heappush(arr, Node(l.val, l.next))            ret = ListNode(0)          pnt = ret            while len(arr) > 0:              # 從優先隊列頭部獲取元素,需要注意,pop之後元素會從隊列中刪除              node = heapq.heappop(arr)              val, l = node.val, node.arr              pnt.next = ListNode(val)              pnt = pnt.next                # 獲取完頭部元素之後,將剩下的鏈表插入回隊列當中              if l is not None:                  heapq.heappush(arr, Node(l.val, l.next))            return ret.next

這種方法從代碼複雜度上要比上一種要小很多,但是我們來分析一下會發現,對於優先隊列而言,每次插入的複雜度是,由於我們一共有K個鏈表,所以插入的複雜度是,一共有n個元素,所以最終的複雜度依然是和歸併的方法是一樣的。但是數據結構的使用簡化了我們計算的過程,這也是我們之所以學習各種數據結構的原因和意義。

今天這道題呢雖然掛的難度是Hard,但其實並不難,哪怕是我們暴力求解都可以通過。因此希望大家不要被它上面寫的難度所嚇到,另外,這題對於優先隊列的應用也非常經典,非常值得學習。

今天的文章就是這些,如果覺得有所收穫,請順手點個在看或者轉發吧,你們的舉手之勞對我來說很重要。