外排序演算法介紹
外排序演算法介紹
1.背景
在大資料場景,待排序的資料檔案可能很大,計算機記憶體不能容納巨大的資料檔案,這時候對大資料檔案就不能單純的使用快速排序、堆排序等內部排序演算法了,得采取一些其他的排序策略。 針對大資料場景的排序,目前最普遍的做法是對待排序的大檔案進行分割,讓大檔案變成一個個很小的、記憶體足夠容納的資料片段檔案,然後對這些分片檔案分別排序,最後通過歸併等演算法將這些片段檔案合併,最終輸出有序的結果。其中,將多個有序片段合併的排序演算法,被稱為外部排序演算法。2.外部排序演算法基本思想
外部排序常採用的排序方法是歸併排序,這種歸併方法由兩個不同的階段組成:
(1)採用適當的內部排序方法對輸入檔案的每個片段進行排序,將排好序的片段(成為歸併段)寫到外部儲存器中(通常由一個可用的磁碟作為臨時緩衝區),這樣臨時緩衝區中的每個歸併段的內容是有序的。
(2)利用歸併演算法,歸併第一階段生成的歸併段,直到只剩下一個歸併段為止。
2.1 兩路歸併
對於已採用內排序演算法完成排序的有序片段,外排序歸併最簡單使用的是2路歸併。每次讀入2路有序片段的前m個元素進行歸併,若輸出緩衝區已滿,則將已歸併好的元素寫入檔案;若其中一路m個元素歸併完成,讀入該路剩下的前m個元素。重複交替執行,直到所有元素都歸併完成為止,則當前檔案的元素為有序的。2.2 k路歸併
2.2.1 敗者樹原理
敗者樹,顧名思義,即記錄勝敗者的樹形結構。實際上,這種資料結構的最初靈感來源就是來自於比賽中記錄勝敗得分的。只不過在敗者樹中,父節點記錄的是敗者節點,而勝者節點繼續上浮比較。
典型的4-路敗者樹結構如下圖所示:
(1)多路平衡歸併演算法具體實現
a.初始化操作
b[0..k],其中0~k-1為k個葉節點,存放k路歸併片段的首地址,k為虛擬記錄,該關鍵字取可能的最小值minkey
ls[0..k-1],其中1~k-1存放不含葉節點的敗者樹的敗者編號,0存放最後勝出的編號
b.處理步驟
a)建敗者樹ls[0..k-1]
b)重複下列操作直至k路歸併完畢:
將b[ls[0]]寫至輸出歸併段;
補充記錄(某歸併段變空時,補充資料),調整敗者樹。
2.2.2 堆排序、勝者樹及敗者樹的聯絡
k路歸併一般採用堆進行排序,利用完全二叉樹的性質,可以很快更新,保持堆的性質。然而堆操作次數還是不夠精簡,因此有人進一步提出了勝者樹和敗者樹的資料結構來進行多路歸併。 勝者樹與敗者樹的葉子節點記錄的都是資料,勝者樹中間節點記錄的是勝者對應的標號,而敗者樹中間節點記錄的是敗者對應的標號。同時敗者樹需要一個額外節點來記錄最終勝者。 由於敗者樹的更新只需將子節點與父節點比較,而勝者樹的更新需要與父節點和子節點比較,因此在實際應用中採用敗者樹更好。2.2.3 三種演算法的相同點
這三種演算法的空間和時間複雜度都是一樣的,調整一次的時間複雜度都是O(logN)的,都利用了二叉樹的性質。
2.2.4 三種演算法的不同點
最早只有用堆來完成k路歸併,但是堆每次取出最小值之後,把最後一個數放到堆頂,調整堆的時候,每次都要選出父節點的兩個孩子節點的最小值,然後再用孩子節點的最小值和父節點進行比較,所以每調整一層需要比較兩次。
為了簡化比較過程,就有了勝者樹,如圖:
但不足的是,勝者樹在節點上升的時候首先需要獲得父節點,然後再獲得兄弟節點後,再比較。 為了進一步減少比較次數,於是就有了敗者樹,如下圖:
在使用敗者樹的時候,每個新元素上升時,只需要獲得父節點並比較即可。
所以總的來說,敗者樹減少了訪存的時間。現在程式的主要瓶頸在於訪存,計算倒可以忽略不計了。
3. 敗者樹演算法實現
例:合併k路有序連結串列
測試樣例:
1 輸入:[ 2 1->4->5, 3 1->3->4, 4 2->6 5 ] 6 輸出:1->1->2->3->4->4->5->6
程式碼實現:
1 import sys 2 3 # Definition for singly-linked list. 4 class ListNode: 5 def __init__(self, x): 6 self.val = x 7 self.next = None 8 9 class Solution: 10 def mergeTwoLists(self, l1, l2): 11 """ 12 :type l1: ListNode 13 :type l2: ListNode 14 :rtype: ListNode 15 """ 16 17 if l1 == None: 18 return l2 19 if l2 == None: 20 return l1 21 22 dummy = ListNode(0) 23 head = dummy 24 while l1 != None and l2 != None: 25 if l1.val <= l2.val: 26 head.next = l1 27 l1 = l1.next 28 head = head.next 29 else: 30 head.next = l2 31 l2 = l2.next 32 head = head.next 33 34 while l1 != None: 35 head.next = l1 36 l1 = l1.next 37 head = head.next 38 while l2 != None: 39 head.next = l2 40 l2 = l2.next 41 head = head.next 42 43 return dummy.next 44 45 def adjust(self, s, listsLen, lists, loserTree): 46 #構成完全二叉樹,按完全二叉樹索引 47 t = (s + listsLen) // 2 48 # 比較當前節點和父節點的大小,若大於,則更新,並將勝者儲存在索引0位置 49 while t > 0: 50 if lists[s].val > lists[loserTree[t]].val: 51 s, loserTree[t] = loserTree[t], s 52 t = t // 2 53 loserTree[0] = s 54 55 def mergeKLists(self, lists): 56 """ 57 :type lists: List[ListNode] 58 :rtype: ListNode 59 """ 60 # 去掉list中的None 61 while None in lists: 62 lists.remove(None) 63 # 若當前序列小於等於1,則返回結果 64 listsLen = len(lists) 65 if listsLen < 1: 66 return None 67 68 if listsLen == 1: 69 return lists[0] 70 # 使用內排序演算法,歸併路數不超過16,然後使用外排序進一步歸併 71 while len(lists) > 16: 72 i, j = 0, len(lists) - 1 73 while i < j: 74 lists[i] = self.mergeTwoLists(lists[i], lists[j]) 75 lists.pop() 76 i += 1 77 j -= 1 78 79 # 初始化新節點,儲存歸併結果 80 dummy = ListNode(-sys.maxsize) 81 head = dummy 82 listsLen = len(lists) 83 84 # 使用外排序進行歸併,若其中某路已歸併完,則構造新的敗者樹 85 while listsLen > 0 and listsLen : 86 # 初始化敗者樹 87 loserTree = [listsLen] * listsLen 88 lists.append(ListNode(-sys.maxsize)) 89 for i in range(listsLen): 90 self.adjust(i, listsLen, lists, loserTree) 91 92 # k-歸併,將每次勝者新增到連結串列的尾部,並讀取下一個數,並更新敗者樹 93 while lists[loserTree[0]] != None: 94 pos = loserTree[0] 95 dummy.next = lists[pos] 96 dummy = dummy.next 97 lists[pos] = lists[pos].next 98 99 # 如果某一路歸併完畢,則需要移除這一路 100 if lists[pos] == None: 101 break 102 # 更新敗者樹 103 self.adjust(pos, listsLen, lists, loserTree) 104 105 # 去掉歸併完的路 106 while None in lists: 107 lists.remove(None) 108 listsLen -= 1 109 110 return head.next
4.演算法分析
每次從k個組中的首元素中選一個最小的數,加入到新組,這樣每次都要比較k-1次,故演算法複雜度為O((n-1)(k-1))。
而如果使用敗者樹,可以在O(logk)的複雜度下得到最小的數,演算法複雜度將為O((n-1)logk), 對於大資料場景的外部排序來說,這是一個不小的提高。
參考:
https://leetcode.com/problems/merge-k-sorted-lists/
https://blog.csdn.net/haolexiao/article/details/53488314
https://zhuanlan.zhihu.com/p/36618960
https://www.zhihu.com/question/35144290/answer/148681658