简易版的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中的源代码来学习。

时间: 2024-12-16 01:44:01

简易版的TimSort排序算法的相关文章

TimSort排序算法及一个问题分析

摘要 排序算法简析 代码入口 排序算法 获取两个有序数组A和B 找到待归并区间 准备操作 归并操作 TimSort的优化归并操作 问题解析 问题解析 问题原因 解决方案 参考 摘要 简单介绍了传统归并排序算法,以及Java API提供的TimSort优化后的归并排序算法. 并且分析了代码中出现的一个问题原因与解决方案. 敬请忽略文中的灵魂画风. 排序算法简析 代码入口 Collections.sort.List.sort.Arrays.sort方法是逐级调用的关系,最终的底层是Arrays.so

排序算法的时空复杂度、稳定性分析

1.基本概念 2.时空复杂度 3.稳定性 4.使用情况分析 排序算法总结(C语言版)已介绍排序算法的基本思想和C语言实现,本文只介绍时空复杂度和稳定性. 1.基本概念 时间复杂度: 一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多.一个算法的语句执行次数称为语句频度或时间频度.记为T(n).n称为问题的规模,当n不断变化时,时间频度T(n)也会不断变化.但有时我们想知道它变化时呈现什么规律,为此,引入时间复杂度概念.若有某个辅助函数f(n),使得当n趋近

排序算法Java版,以及各自的复杂度,以及由堆排序产生的top K问题

常用的排序算法包括: 冒泡排序:每次在无序队列里将相邻两个数依次进行比较,将小数调换到前面, 逐次比较,直至将最大的数移到最后.最将剩下的N-1个数继续比较,将次大数移至倒数第二.依此规律,直至比较结束.时间复杂度:O(n^2) 选择排序:每次在无序队列中"选择"出最大值,放到有序队列的最后,并从无序队列中去除该值(具体实现略有区别).时间复杂度:O(n^2) 直接插入排序:始终定义第一个元素为有序的,将元素逐个插入到有序排列之中,其特点是要不断的 移动数据,空出一个适当的位置,把待插

排序算法总结(C语言版)

1.    插入排序 1.1     直接插入排序 1.2     Shell排序 2.    交换排序 2.1     冒泡排序 2.2     快速排序 3.    选择排序 3.1     直接选择排序 3.2     堆排序 4.    归并排序 4.1     二路归并排序 4.2     自然合并排序 5.    分布排序 5.1     基数排序 1.插入排序 1.1      直接插入排序 将已排好序的部分num[0]~num[i]后的一个元素num[i+1]插入到之前已排好序的

【JavaScript】【算法】JavaScript版排序算法

JavaScript版排序算法:冒泡排序.快速排序.插入排序.希尔排序(小数据时,希尔排序会比快排快哦) 1 //排序算法 2 window.onload = function(){ 3 var array = [0,1,2,44,4, 4 324,5,65,6,6, 5 34,4,5,6,2, 6 43,5,6,62,43, 7 5,1,4,51,56, 8 76,7,7,2,1, 9 45,4,6,7,8]; 10 //var array = [4,2,5,1,0,3]; 11 array

经典排序算法(Java版)

经典排序算法(Java版)  转载 1.冒泡排序 Bubble Sort最简单的排序方法是冒泡排序方法.这种方法的基本思想是,将待排序的元素看作是竖着排列的“气泡”,较小的元素比较轻,从而要往上浮.在冒泡排序算法中我们要对这个“气泡”序列处理若干遍.所谓一遍处理,就是自底向上检查一遍这个序列,并时刻注意两个相邻的元素的顺序是否正确.如果发现两个相邻元素的顺序不对,即“轻”的元素在下面,就交换它们的位置.显然,处理一遍之后,“最轻”的元素就浮到了最高位置:处理二遍之后,“次轻”的元素就浮到了次高位

可视化对比十多种排序算法(C#版)

本文由 伯乐在线 - smilesisi 翻译自 Kanasz Robert.欢迎加入技术翻译小组.转载请参见文章末尾处的要求. 在这篇文章中,我会向大家展示一些排序算法的可视化过程.我还写了一个工具,大家可对比查看某两种排序算法. 下载源码 – 75.7 KB 下载示例 – 27.1 KB 引言 首先,我认为是最重要的是要理解什么是“排序算法”.根据维基百科,排序算法(Sorting algorithm)是一种能将一串数据依照特定排序方式进行排列的一种算法.最常用到的排序方式是数值顺序以及字典

常见排序算法(JS版)

常见排序算法(JS版)包括: 内置排序,冒泡排序,选择排序,插入排序,希尔排序,快速排序(递归 & 堆栈),归并排序,堆排序,以及分析每种排序算法的执行时间. index.html 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>twobin 常见排序算法 (JS版) </title> 5 <meta http-equiv="content-type" content=&

JavaScript版几种常见排序算法

今天发现一篇文章讲“JavaScript版几种常见排序算法”,看着不错,推荐一下原文:http://www.w3cfuns.com/blog-5456021-5404137.html 算法描述: * 冒泡排序:最简单,也最慢,貌似长度小于7最优* 插入排序: 比冒泡快,比快速排序和希尔排序慢,较小数据有优势* 快速排序:这是一个非常快的排序方式,V8的sort方法就使用快速排序和插入排序的结合* 希尔排序:在非chrome下数组长度小于1000,希尔排序比快速更快* 系统方法:在forfox下系