簡易版的TimSort排序演算法

来源:http://www.cnblogs.com/nullzx/archive/2016/10/30/6014471.html
-Advertisement-
Play Games

歡迎探討,如有錯誤敬請指正 如需轉載,請註明出處http://www.cnblogs.com/nullzx/ 1. 簡易版本TimSort排序演算法原理與實現 TimSort排序演算法是Python和Java針對對象數組的預設排序演算法。TimSort排序演算法的本質是歸併排序演算法,只是在歸併排序演算法上進行... ...


歡迎探討,如有錯誤敬請指正

如需轉載,請註明出處http://www.cnblogs.com/nullzx/


1. 簡易版本TimSort排序演算法原理與實現

TimSort排序演算法是Python和Java針對對象數組的預設排序演算法。TimSort排序演算法的本質是歸併排序演算法,只是在歸併排序演算法上進行了大量的優化。對於日常生活中我們需要排序的數據通常不是完全隨機的,而是部分有序的,或者部分逆序的,所以TimSort充分利用已有序的部分進行歸併排序。現在我們提供一個簡易版本TimSort排序演算法,它主要做了以下優化:

1.1利用原本已有序的片段

首先規定一個最小歸併長度檢查數組中原本有序的片段,如果已有序的長度小於規定的最小歸併長度,則通過插入排序對已有序的片段進行進行擴充(這樣做的原因避免歸併長度較小的片段,因為這樣的效率比較低)。將有序片段的起始索引位置和已有序的長度入棧。

1.2避免一個較長的有序片段和一個較小的有序片段進行歸併,因為這樣的效率比較低:

(1)如果棧中存在已有序的至少三個序列,我們用X,Y,Z依次表示從棧頂向下的三個已有序列片段,當三者的長度滿足X+Y>=Z時進行歸併。

   (1.1)如果X是三者中長度最大的,先將X,Y,Z出棧,應該先歸併Y和Z,然後將Y和Z歸併的結果入棧,最後X入棧

   (1.2)否則將X和Y出棧,歸併後結果入棧。註意,實際上我們不會真正的出棧,寫代碼中有一些技巧可以達到相同的效果,而且效率更高。

(2)如果不滿足X+Y>=Z的條件或者棧中僅存在兩個序列,我們用X,Y依次表示從棧頂向下的兩個已有序列的長度,如果X>=Y則進行歸併,然後將歸併後的有序片段結果入棧。

1.3在歸併兩個已有序的片段時,採用了所謂的飛奔(gallop)模式,這樣可以減少參與歸併的數據長度

假設需要歸併的兩個已有序片段分別為X和Y,如果X片段的前m個元素都比Y片段的首元素小,那麼這m個元素實際上是不需要參與歸併的,因為歸併後這m個元素仍然位於原來的位置。同理如果Y片段的最後n個元素都比X的最後一個元素大,那麼Y的最後n個元素也不必參與歸併。這樣就減少了歸併數組的長度(簡易版沒有這麼做),也較少了待排序數組與輔助數組之間數據來回覆制的長度,進而提高了歸併的效率。

2. Java源代碼

package datastruct;

import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Random;
import java.util.Scanner;

public class SimpleTimSort<T extends Comparable<? super T>>{
	//最小歸併長度
	private static final int MIN_MERGE = 16;
	//待排序數組
	private final T[] a;
	//輔助數組
	private T[] aux;
	//用兩個數組表示棧
	private int[] runsBase = new int[40];
	private int[] runsLen = new int[40];
	//表示棧頂指針
	private int stackTop = 0;
	
	@SuppressWarnings("unchecked")
	public SimpleTimSort(T[] a){
		this.a = a;
		aux = (T[]) Array.newInstance(a[0].getClass(), a.length);
	}
	
	//T[from, to]已有序,T[to]以後的n元素插入到有序的序列中
	private void insertSort(T[] a, int from, int to, int n){
		int i = to + 1;
		while(n > 0){
			T tmp = a[i];
			int j;
			for(j = i-1; j >= from && tmp.compareTo(a[j]) < 0; j--){
				a[j+1] = a[j];
			}
			a[++j] = tmp;
			i++;
			n--;
		}
	}
	
	//返回從a[from]開始,的最長有序片段的個數
	private int maxAscendingLen(T[] a, int from){
		int n = 1;
		int i = from;
		
		if(i >= a.length){//超出範圍
			return 0;
		}
		
		if(i == a.length-1){//只有一個元素
			return 1;
		}
		
		//至少兩個元素
		if(a[i].compareTo(a[i+1]) < 0){//升序片段
			while(i+1 <= a.length-1 && a[i].compareTo(a[i+1]) <= 0){
				i++;
				n++;
			}
			return n;
		}else{//降序片段,這裡是嚴格的降序,不能有>=的情況,否則不能保證穩定性
			while(i+1 <= a.length-1 && a[i].compareTo(a[i+1]) > 0){
				i++;
				n++;
			}
			//對降序片段逆序
			int j = from;
			while(j < i){
				T tmp = a[i];
				a[i] = a[j];
				a[j] = tmp;
				j++;
				i--;
			}
			return n;
		}
	}
	
	//對有序片段的起始索引位置和長度入棧
	private void pushRun(int base, int len){
		runsBase[stackTop] = base;
		runsLen[stackTop] = len;
		stackTop++;
	}
	
	//返回-1表示不需要歸併棧中的有序片段
	public int needMerge(){
		if(stackTop > 1){//至少兩個run序列
			int x = stackTop - 2;
			//x > 0 表示至少三個run序列
			if(x > 0 && runsLen[x-1] <= runsLen[x] + runsLen[x+1]){
				if(runsLen[x-1] < runsLen[x+1]){
					//說明 runsLen[x+1]是runsLen[x]和runsLen[x-1]中最大的值
					//應該先合併runsLen[x]和runsLen[x-1]這兩段run
					return --x;
				}else{
					return x;
				}
			}else
			if(runsLen[x] <= runsLen[x+1]){
				return x;
			}else{
				return -1;
			}
		}
		return -1;
	}
	
	//返回後一個片段的首元素在前一個片段應該位於的位置
	private int gallopLeft(T[] a, int base, int len, T key){
		int i = base;
		while(i <= base + len - 1){
			if(key.compareTo(a[i]) >= 0){
				i++;
			}else{
				break;
			}
		}
		return i;
	}
	
	//返回前一個片段的末元素在後一個片段應該位於的位置
	private int gallopRight(T[] a, int base, int len, T key){
		int i = base + len -1;
		while(i >= base){
			if(key.compareTo(a[i]) <= 0){
				i--;
			}else{
				break;
			}
		}
		return i;
	}
	
	public void mergeAt(int x){
		int base1 = runsBase[x];
		int len1 = runsLen[x];
		
		int base2 = runsBase[x+1];
		int len2 = runsLen[x+1];
		
		//合併run[x]和run[x+1],合併後base不用變,長度需要發生變化
		runsLen[x] = len1 + len2; 
		if(stackTop == x + 3){
			//棧頂元素下移,省去了合併後的先出棧,再入棧
			runsBase[x+1] = runsBase[x+2];
			runsLen[x+1] = runsLen[x+2];
		}
		stackTop--;
		
		//飛奔模式,減小歸併的長度
		int from = gallopLeft(a, base1, len1, a[base2]);
		if(from == base1+len1){
			return;
		}
		int to = gallopRight(a, base2, len2, a[base1+len1-1]);
		
		//對兩個需要歸併的片段長度進行歸併
		System.arraycopy(a, from, aux, from, to - from + 1);
		int i = from;
		int iend = base1 + len1 - 1;
		
		int j = base2;
		int jend = to;
		
		int k = from;
		int kend = to;
		
		while(k <= kend){
			if(i > iend){
				a[k] = aux[j++];
			}else
			if(j > jend){
				a[k] = aux[i++];
			}else
			if(aux[i].compareTo(aux[j]) <= 0){//等號保證排序的穩定性
				a[k] = aux[i++];
			}else{
				a[k] = aux[j++];
			}
			k++;
		}
	}
	
	//強制歸併已入棧的序列
	private void forceMerge(){
		while(stackTop > 1){
			mergeAt(stackTop-2);
		}
	}
	
	//timSort的主方法
	public void timSort(){
		//n表示剩餘長度
		int n = a.length; 
		
		if(n < 2){
			return;
		}
		
		//待排序的長度小於MIN_MERGE,直接採用插入排序完成
		if(n < MIN_MERGE){
			insertSort(a, 0, 0, a.length-1);
			return;
		}
		
		int base = 0;
		while(n > 0){
			int len = maxAscendingLen(a, base);
			if(len < MIN_MERGE){
				int abscent = n > MIN_MERGE ?  MIN_MERGE - len : n - len;
				insertSort(a, base, base + len-1, abscent);
				len = len + abscent;
			}
			pushRun(base, len);
			n = n - len;
			base = base + len;
			
			int x;
			while((x  = needMerge()) >= 0 ){
				mergeAt(x);
			}
		}
		forceMerge();
	}
	
	public static void main(String[] args){
		
		//隨機產生測試用例
		Random rnd = new Random(System.currentTimeMillis());
		boolean flag = true;
		while(flag){
			
			//首先產生一個全部有序的數組
			Integer[] arr1 = new Integer[1000];
			for(int i = 0; i < arr1.length; i++){
				arr1[i] = i;
			}
			
			//有序的基礎上隨機交換一些值
			for(int i = 0; i < (int)(0.1*arr1.length); i++){
				int x,y,tmp;
				x = rnd.nextInt(arr1.length);
				y = rnd.nextInt(arr1.length);
				tmp = arr1[x];
				arr1[x] = arr1[y];
				arr1[y] = tmp;
			}
			
			//逆序部分數據
			for(int i = 0; i <(int)(0.05*arr1.length); i++){
				int x = rnd.nextInt(arr1.length);
				int y = rnd.nextInt((int)(arr1.length*0.01)+x);
				if(y >= arr1.length){
					continue;
				}
				while(x < y){
					int tmp;
					tmp = arr1[x];
					arr1[x] = arr1[y];
					arr1[y] = tmp;
					x++;
					y--;
				}
			}
			
			Integer[] arr2 = arr1.clone();
			Integer[] arr3 = arr1.clone();
			Arrays.sort(arr2);
			
			SimpleTimSort<Integer> sts = new SimpleTimSort<Integer>(arr1);
			sts.timSort();
			
			//比較SimpleTimSort排序和庫函數提供的排序結果比較是否一致
			//如果沒有列印任何結果,說明排序結果正確
			if(!Arrays.deepEquals(arr1, arr2)){
				for(int i = 0; i < arr1.length; i++){
					if(!arr1[i].equals(arr2[i])){
						System.out.printf("%d: arr1 %d  arr2 %d\n",i,arr1[i],arr2[i]);
					}
				}
				System.out.println(Arrays.deepToString(arr3));
				flag = false;
			}
		}
	}
}

3.TimSort演算法應當註意的問題

TimSort演算法只會對連續的兩個片段進行歸併,這樣才能保證演算法的穩定性。

最小歸併長度和棧的長度存在一定的關係,如果增大最小歸併長度,則棧的長度也應該增大,否則可能引起棧越界的風險(代碼中棧是通過長度為40的數組來實現的)。

4.完整版的TimSort演算法

實際上,完整版的TimSort演算法會在上述簡易TimSort演算法上還有大量的優化。比如有序序列小於最小歸併長度時,我們可以利用類似二分查找的方式來找到應該插入的位置來對數組進行長度擴充。再比如飛奔模式中採用二分查找的方式查找第二個序列的首元素在第一個序列的位置,同時還可以使用較小的輔助空間完成歸併,有興趣的同學可以查看Java中的源代碼來學習。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ref_out { class Program { static void Method1(ref int ...
  • 文檔目錄 本節內容: 創建動態Web Api控制器 ForAll 方法 重寫 ForAll ForMethods Http 動詞 WithVerb 方法 HTTP 特性 命名約定 Api 瀏覽器 RemoteService 特性 動態Javascript代理 AJAX 參數 單獨服務腳本 Angul ...
  • 在網上收集。。。 C#的值類型,引用類型,棧,堆,ref,out C# 的類型系統可分為兩種類型,一是值類型,一是引用類型,這個每個C#程式員都瞭解。還有托管堆,棧,ref,out等等概念也是每個C#程式員都會接觸到的概念,也是C#程式員面試經常考到的知識,隨便搜搜也有無數的文章講解相關的概念,貌似 ...
  • 文檔目錄 本節內容: 簡介 AbpApiController 基類 本地化 其它 過濾 審計日誌 授權 防偽造過濾 工作單元 結果包裝和異常處理 結果緩存 驗證 模塊綁定器 本地化 其它 審計日誌 授權 防偽造過濾 工作單元 結果包裝和異常處理 結果緩存 驗證 簡介 通過Abp.Web.Api的nu ...
  • 最近在做一個項目的時候,需要增加一個日誌的功能,需要使用Log4Net記錄日誌,把數據插入到Oracle資料庫,經過好久的研究終於成功了。把方法記錄下來,以備以後查詢。 直接寫實現方法,分兩步完成: 1、使用NuGet Manager管理工具,增加對Oracle.ManagedDataAccess. ...
  • 最近準備下PostgreSQL資料庫開發的相關知識,本文把總結的PPT內容通過博客記錄分享,本隨筆的主要內容是介紹PostgreSQL資料庫的基礎信息,以及如何在我們的開發框架中使用PostgreSQL資料庫,希望大家多多提意見。 ...
  • 【百度百科】 LIBSVM是臺灣大學林智仁(Lin Chih-Jen)教授等開發設計的一個簡單、易於使用和快速有效的SVM模式識別與回歸的軟體包,他不但提供了編譯好的可在Windows系列系統的執行文件,還提供了源代碼,方便改進、修改以及在其它操作系統上應用;該軟體對SVM所涉及的參數調節相對比較少 ...
  • 1.向SharedPreferences 中存儲字元串 2.從SharedPreferences 中獲取存儲的字元串 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...