简易版的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中的源代码来学习。
简易版的TimSort排序算法的更多相关文章
- 剑指offer第二版-总结:排序算法
1.排序算法比较: 2.java实现 快排: /** * 快排 * * @since 2019年2月26日 下午1:37:34 * @author xuchao */ public class Qui ...
- Python版常见的排序算法
学习笔记 排序算法 目录 学习笔记 排序算法 1.冒泡排序 2.选择排序 3.插入排序 4.希尔排序 5.快速排序 6.归并排序 7.堆排序 排序分为两类,比较类排序和非比较类排序,比较类排序通过比较 ...
- hdu2083 简易版之最短距离 排序水题
给出数轴n个坐标,求一个点到所有点距离总和最小.排序后最中间一个点或两个点之间就是最优 #include<stdio.h> #include<algorithm> using ...
- 十大经典排序算法总结——JavaScrip版
首先,对于评述算法优劣术语的说明: 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面:即排序后2个相等键值的顺序和排序之前它们的顺序相同 不稳定:如果a原本在b的前面,而a=b,排序之后a ...
- JavaScript版几种常见排序算法
今天发现一篇文章讲“JavaScript版几种常见排序算法”,看着不错,推荐一下原文:http://www.w3cfuns.com/blog-5456021-5404137.html 算法描述: * ...
- 常见排序算法(JS版)
常见排序算法(JS版)包括: 内置排序,冒泡排序,选择排序,插入排序,希尔排序,快速排序(递归 & 堆栈),归并排序,堆排序,以及分析每种排序算法的执行时间. index.html <! ...
- JavaScript版排序算法
JavaScript版排序算法:冒泡排序.快速排序.插入排序.希尔排序(小数据时,希尔排序会比快排快哦) //排序算法 window.onload = function(){ var array = ...
- 十大经典排序算法的JS版
前言 个人博客:Damonare的个人博客 如遇到问题或有更好的优化方法,可以: 提issue给我 或是pull requests 我都会看到并处理,欢迎Star. 这世界上总存在着那么一些看似相似但 ...
- 常见排序算法总结(java版)
一.冒泡排序 1.原理:相邻元素两两比较,大的往后放.第一次完毕,最大值在最大索引处. 即使用相邻的两个元素一次比价,依次将最大的数放到最后. 2.代码: public static void bub ...
随机推荐
- Linq的分页
真有趣. C#里面的List对象.set对象,都可以直接使用Linq(这是因为,它们都实现了接口IEnumable?),比如说:Where().OrderBy()什么的.假如有点SQL基础的人,一看这 ...
- 推荐Linux管理员不可不知十大PHP安全要点 - SCutePHP
PHP是使用最广泛的脚本编程语言之一.市场份额颇能说明其主导地位.PHP 7已推出,这个事实让这种编程语言对当前的开发人员来说更具吸引力.尽管出现了一些变化,但是许多开发人员对PHP的未来持怀疑态度. ...
- php js数组问题
<script type="text/javascript"> var a = new Array(); a = "a"; a = "b& ...
- kafka的log存储解析——topic的分区partition分段segment以及索引等
转自:http://blog.csdn.net/jewes/article/details/42970799 引言 Kafka中的Message是以topic为基本单位组织的,不同的topic之间是相 ...
- 基于lcov实现的增量代码UT覆盖率检查
背景介绍 配合CppUTest单元测试框架,lcov提供了一套比较完整的工程工具来对UT覆盖率进行度量.但对有些团队来说,历史负担太重,大量的遗留代码没有相应的UT.在这种情况下,对新增代码进行覆盖率 ...
- Hibernate中Criteria的完整用法
1,CriteriaHibernate 设计了 CriteriaSpecification 作为 Criteria 的父接口,下面提供了 Criteria和DetachedCriteria .2,De ...
- iOS开发UI高级手势识别器
####手势识别器 UIGestureRecognizer类 ·UITapGestureRecognizer(轻击) ·UIPinchGestureRecognizer(捏合) ·UIPanGestu ...
- 通用EF框架
之前我老大去网上找了一个DAL里面操作数据库的通用类: public class DALHelper { public static List<T> Search<T>() w ...
- myeclipse导入项目出现jquery错误(有红叉)
今天导入了一个项目,但是进去之后jquery出现了红叉,如图(事实上在我没调好之前两个jquery文件都有叉号) 怎么调呢?右键jquery文件,选择MyEclipse->Exclude Fro ...
- SageCRM 快速获取连接中的SID的方法
经常需要使用ajax来修改页面的功能,包括联动.动态加载等. SageCRM的页面必须有SID的,所以要方便的获取它. var getKey = function(key,Url) { if(argu ...