1. 程式人生 > >Timsort原理介紹

Timsort原理介紹

    Timsort是結合了合併排序(merge sort)和插入排序(insertion sort)而得出的排序演算法,它在現實中有很好的效率。Tim Peters在2002年設計了該演算法並在Python中使用(TimSort 是 Python 中 list.sort 的預設實現)。該演算法找到資料中已經排好序的塊-分割槽,每一個分割槽叫一個run,然後按規則合併這些run。Pyhton自從2.3版以來一直採用Timsort演算法排序,現在Java SE7和Android也採用Timsort演算法對陣列排序。

內容

1 操作

   1.1 run的最小長度

   1.2 優化run的長度

   1.3 合併run

   1.4 合併步驟

    1.5 Galloping模型

2 效能 

Timsort的核心過程

       TimSort 演算法為了減少對升序部分的回溯和對降序部分的效能倒退,將輸入按其升序和降序特點進行了分割槽。排序的輸入的單位不是一個個單獨的數字,而是一個個的塊-分割槽。其中每一個分割槽叫一個run。針對這些 run 序列,每次拿一個 run 出來按規則進行合併。每次合併會將兩個 run合併成一個 run。合併的結果儲存到棧中。合併直到消耗掉所有的 run,這時將棧上剩餘的 run合併到只剩一個 run 為止。這時這個僅剩的 run 便是排好序的結果。

綜上述過程,Timsort演算法的過程包括

(0)如何陣列長度小於某個值,直接用二分插入排序演算法

(1)找到各個run,併入棧

(2)按規則合併run

1 操作

     現實中的大多資料通常是有部分已經排好序的,Timsort利用了這一特點。Timsort排序的輸入的單位不是一個個單獨的數字,而是一個個的分割槽。其中每一個分割槽叫一個“run“(圖1)。針對這個 run 序列,每次拿一個 run 出來進行歸併。每次歸併會將兩個 run 合併成一個 run。每個run最少要有2個元素。Timesor按照升序和降序劃分出各個run:run如果是是升序的,那麼run中的後一元素要大於或等於前一元素(a[lo] <= a[lo + 1] <= a[lo + 2] <= ...);如果run是嚴格降序

的,即run中的前一元素大於後一元素(a[lo] >  a[lo + 1] >  a[lo + 2] >  ...),需要將run 中的元素翻轉(這裡注意降序的部分必須是“嚴格”降序才能進行翻轉。因為 TimSort 的一個重要目標是保持穩定性stability。如果在 >= 的情況下進行翻轉這個演算法就不再是 stable)。

1.1 run的最小長度

    run是已經排好序的一塊分割槽。run可能會有不同的長度,Timesort根據run的長度來選擇排序的策略。例如如果run的長度小於某一個值,則會選擇插入排序演算法來排序。run的最小長度(minrun)取決於陣列的大小。當陣列元素少於64個時,那麼run的最小長度便是陣列的長度,這是Timsort用插入排序演算法來排序。當陣列元素大於等於63時,For larger arrays, a number, referred to as minrun, is chosen from the range 32 to 65, such that the size of the array, divided by the minimum run size, is equal to, or slightly smaller than, a power of two. The final algorithm for this simply takes the six most significant bits of the size of the array, adds one if any of the remaining bits are set, and uses that result as the minrun. This algorithm works for all cases, including the one in which the size of the array is smaller than 64.

圖1 run

1.2  優化run的長度

       優化run的長度是指當run的長度小於minrun時,為了使這樣的run的長度達到minrun的長度,會從陣列中選擇合適的元素插入run中。這樣做使大部分的run的長度達到均衡,有助於後面run的合併操作。

1.3 合併run

       劃分run和優化run長度以後,然後就是對各個run進行合併。合併run的原則是 run合併的技術要保證有最高的效率。當Timsort演算法找到一個run時,會將該run在陣列中的起始位置和run的長度放入棧中,然後根據先前放入棧中的run決定是否該合併run。Timsort不會合並在棧中不連續的run(Timsort does not merge non-consecutive runs because doing this would cause the element common to all three runs to become out of order with respect to the middle run.

Timsort會合並在棧中2個連續的run。X、Y、Z代表棧最上方的3個run的長度(圖2),當同時不滿足下面2個條件是,X、Y這兩個run會被合併,直到同時滿足下面2個條件,則合併結束:

(1) X>Y+Z

(2) Y>Z

例如:如果X<Y+Z,那麼X+Y合併為一個新的run,然後入棧。重複上述步驟,直到同時滿足上述2個條件。當合並結束後,Timsort會繼續找下一run,然後找到以後入棧,重複上述步驟,及每次run入棧都會檢查是否需要合併2個run。

圖2 合併run

 1.4 合併run步驟

       合併2個相鄰的run需要臨時儲存空閒,臨時儲存空間的大小是2個run中較小的run的大小。Timsort演算法先將較小的run複製到這個臨時儲存空間,然後用原先儲存這2個run的空間來儲存合併後的run(圖3)。

圖3 臨時儲存空間


          簡單的合併演算法是用簡單插入演算法,依次從左到右或從右到左比較,然後合併2個run。為了提高效率,Timsort用二分插入演算法(binary merge sort)。先用二分查詢演算法/折半查詢演算法(binary search)找到插入的位置,然後在插入。
         例如,我們要將A和B這2個run 合併,且A是較小的run。因為A和B已經分別是排好序的,二分查詢會找到B的第一個元素在A中何處插入(圖4)。同樣,A的最後一個元素找到在B的何處插入,找到以後,B在這個元素之後的元素就不需要比較了(圖5)。這種查詢可能在隨機數中效率不會很高,但是在其他情況下有很高的效率。

圖4 run合併過程1

                                                                         圖5 run合併過程2

1.5 Galloping 模型

2 效能

     根據資訊學理論,在平均情況下,比較排序不會比O(n log n)更快。由於Timsort演算法利用了現實中大多數資料中會有一些排好序的區,所以Timsort會比
O(n log n)快些。對於隨機數沒有可以利用的排好序的區,Timsort時間複雜度會是log(n!)。下表是Timsort與其他比較排序演算法時間複雜度(time complexity)的比較。

  空間複雜度(space complexities)比較

說明:

JSE 7對物件進行排序,沒有采用快速排序,是因為快速排序是不穩定的,而Timsort是穩定的。

下面是JSE7 中Timsort實現程式碼中的一段話,可以很好的說明Timsort的優勢:

 A stable, adaptive, iterative mergesort that requires far fewer than n lg(n) comparisons when running on partially sorted arrays, while offering performance comparable to a traditional mergesort when run on random arrays.  Like all proper mergesorts, this sort is stable and runs O(n log n) time (worst case).  In the worst case, this sort requires temporary storage space for n/2 object  references; in the best case,  it requires only a small constant amount of space.

大體是說,Timsort是穩定的演算法,當待排序的陣列中已經有排序好的數,它的時間複雜度會小於n logn。與其他合併排序一樣,Timesrot是穩定的排序演算法,最壞時間複雜度是O(n log n)。在最壞情況下,Timsort演算法需要的臨時空間是n/2,在最好情況下,它只需要一個很小的臨時儲存空間