MergeSort归并排序对已经反向排好序的输入时复杂度为O(n^2),而TimSort就是针对这种情况,对MergeSort进行优化而产生的,平均复杂度为nO(log n),最好的情况为O(n),最坏情况nO(log n)。并且TimSort是一种稳定性排序。思想是先对待排序列进行分区,然后再对分区进行合并,看起来和MergeSort步骤一样,但是其中有一些针对反向和大规模数据的优化处理。

通过一个例子来说:ArrayList中的sort(),调用了Arrays.sort()

@Override

@SuppressWarnings("unchecked")

public void sort(Comparator super E> c) {

final int expectedModCount = modCount;

Arrays.sort((E[]) elementData, 0, size, c);

if (modCount != expectedModCount) {

throw new ConcurrentModificationException();

}

modCount++;

}

Arrays.sort()

public static void sort(T[] a, int fromIndex, int toIndex,

Comparator super T> c) {

if (c == null) {

sort(a, fromIndex, toIndex);

} else {

rangeCheck(a.length, fromIndex, toIndex);

if (LegacyMergeSort.userRequested)

legacyMergeSort(a, fromIndex, toIndex, c);

else

TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0);

}

}

1. 当Comparator == null时,调用sort(a, fromIndex, toIndex);如下

public static void sort(Object[] a, int fromIndex, int toIndex) {

rangeCheck(a.length, fromIndex, toIndex);

//该分支将被删除

if (LegacyMergeSort.userRequested)

legacyMergeSort(a, fromIndex, toIndex);

else

ComparableTimSort.sort(a, fromIndex, toIndex, null, 0, 0);

}

/** To be removed in a future release. */

private static void legacyMergeSort(Object[] a,

int fromIndex, int toIndex) {

Object[] aux = copyOfRange(a, fromIndex, toIndex);

mergeSort(aux, a, fromIndex, toIndex, -fromIndex);

}

sort方法,参数[frimeIndex, toIndex)左闭右开,采用的算法能够保证稳定性,相等元素按原来顺序排列。传入的数组部分有序则能保证事件复杂度远小于nlg(n);若完全杂乱无序,则为n;若数组中存在连续升序或降序的情况都能被很好的利用起来

(这里的稳定是指比较相等的数据在排序之后仍然按照排序之前的前后顺序排列。对于基本数据类型,稳定性没有意义,而对于对象类型,稳定性是比较重要的,因为对象相等的判断可能只是判断关键属性,最好保持相等对象的非关键属性的顺序与排序前一直;)

1) 当LegacyMergeSort.userRequested为true的情况下(该分支会在未来被弃用),采用legacyMergeSort,否则采用ComparableTimSort。

(为什么会被弃用,如一开始所说的。TimSort就应运而生,包括接下来介绍的ComparableTimSort,与前者基本相同唯一区别的是后者需要对象是Comparable可比较的,不需要特定Comparator,而前者利用提供的Comparator进行排序)

LegacyMergeSort.userRequested的字面意思大概就是“用户请求传统归并排序”的意思,通过System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");

设置

mergeSort()

//To be removed in a future release.未来会弃用

@SuppressWarnings({"unchecked", "rawtypes"})

private static void mergeSort(Object[] src,

Object[] dest,

int low,

int high,

int off) {

int length = high - low;

// Insertion sort on smallest arrays

if (length < INSERTIONSORT_THRESHOLD) { //7

for (int i=low; i

for (int j=i; j>low &&

((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)

swap(dest, j, j-1);

return;

}

// Recursively sort halves of dest into src

int destLow = low;

int destHigh = high;

low += off;

high += off;

int mid = (low + high) >>> 1;

mergeSort(dest, src, low, mid, -off);

mergeSort(dest, src, mid, high, -off);

// If list is already sorted, just copy from src to dest. This is an

// optimization that results in faster sorts for nearly ordered lists.

//这里说的是:mid两侧的元素都是有序的,若此时src[mid-1] <=src[mid],

则不用在进行比较,节省时间

if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {

System.arraycopy(src, low, dest, destLow, length);

return;

}

// Merge sorted halves (now in src) into dest

for(int i = destLow, p = low, q = mid; i < destHigh; i++) {

if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)

dest[i] = src[p++];

else

dest[i] = src[q++];

}

}

上面的代码:当数组大小小于7时,采用插入排序,否则采用归并排序

TimSort的重要思想是分区与合并

分区

分区的思想是扫描一次数组,把连续正序列(如果是升序排序,那么正序列就是升序序列)(也就是后面所指的run),如果是反序列,把分区里的元素反转一下。 例如

1,2,3,6,4,5,8,6,4 划分分区结果为

[1,2,3,6],[4,5,8],[6,4]

然后反转反序列

[1,2,3,6],[4,5,8],[4,6]

合并

考虑一个极端的例子,比如分区的长度分别为 10000,10,1000,10,10,我们当然希望是先让10个10合并成20, 20和1000合并成1020如此下去, 如果从从左往右顺序合并的话,每次都用到10000这个数组和去小的数组合并,代价太大了。所以我们可以用一个策略来优化合并的顺序。

2) 接着上面的例子,1)的LegacyMergeSort情况说完接下来调用的是ComparableTimSort.sort

static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {

assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

int nRemaining = hi - lo;

if (nRemaining < 2)

return; // Arrays of size 0 and 1 are always sorted

// If array is small, do a "mini-TimSort" with no merges

//当数组大小小于32是,调用“mini-TimeSort”

if (nRemaining < MIN_MERGE) { //32

int initRunLen = countRunAndMakeAscending(a, lo, hi);

binarySort(a, lo, hi, lo + initRunLen);

return;

}

..........未完待续

}

首先来分析该函数中当数组大小小于32时,调用的“mini-TimeSort”情况:

一开始调用了countRunAndMakeAscending(a, lo, hi)函数,得到一个长度initRunLen

@SuppressWarnings({"unchecked", "rawtypes"})

private static int countRunAndMakeAscending(Object[] a, int lo, int hi) {

assert lo < hi;

int runHi = lo + 1;

if (runHi == hi)

return 1;

// Find end of run, and reverse range if descending

if (((Comparable) a[runHi++]).compareTo(a[lo]) < 0) { // Descending

while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) < 0)

runHi++;

reverseRange(a, lo, runHi);

} else { // Ascending

while (runHi < hi && ((Comparable) a[runHi]).compareTo(a[runHi - 1]) >= 0)

runHi++;

}

return runHi - lo;

}

该函数有啥用?数组a,求从lo开始连续的升序或降序(会被反转变成升序)的元素个数。求出的这个长度有啥用途?优化接下来调用的函数

binarySort(a, lo, lo + force, lo + runLen);代码如下

//start参数传进来的是lo+runLen,现在数组情况是a[lo, lo+runlen-1]为升序,

a[lo+runLen, hi)为乱序,该方法就是从lo+runLen开始往后一个个取出来与前面有序数组进行比较排序,

采用二分法

@SuppressWarnings({"fallthrough", "rawtypes", "unchecked"})

private static void binarySort(Object[] a, int lo, int hi, int start) {

assert lo <= start && start <= hi;

if (start == lo)

start++;

for ( ; start < hi; start++) {

Comparable pivot = (Comparable) a[start];

// Set left (and right) to the index where a[start] (pivot) belongs

int left = lo;

int right = start;

assert left <= right;

/*

* Invariants:

* pivot >= all in [lo, left).

* pivot < all in [right, start).

*/

while (left < right) {

int mid = (left + right) >>> 1;

if (pivot.compareTo(a[mid]) < 0)

right = mid;

else

left = mid + 1;

}

assert left == right;

/*

* The invariants still hold: pivot >= all in [lo, left) and

* pivot < all in [left, start), so pivot belongs at left. Note

* that if there are elements equal to pivot, left points to the

* first slot after them -- that's why this sort is stable.

* Slide elements over to make room for pivot.

*/

int n = start - left; // The number of elements to move

// Switch is just an optimization for arraycopy in default case

switch (n) {

case 2: a[left + 2] = a[left + 1];

case 1: a[left + 1] = a[left];

break;

default: System.arraycopy(a, left, a, left + 1, n);

}

a[left] = pivot;

}

}

前面提到的在数组大小<32情况下,采用“mini-TimeSort”,实质是二分排序,利用countRunAndMakeAscending求得的长度来进行优化。

int start参数传进来的是lo+runLen,现在数组情况是a[lo, lo+runlen-1]为升序,a[lo+runLen, hi)为乱序,该方法就是从lo+runLen开始往后一个个取出来与前面有序数组进行比较排序,采用二分法。该函数时间复杂度为nlg(n),不过在最坏情况下需要n^2次移动。

现在来分析数组数目>32的情况:

接着上面未完的代码

/**

* March over the array once, left to right, finding natural runs,

* extending short natural runs to minRun elements, and merging runs

* to maintain stack invariant.

*/

ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);

int minRun = minRunLength(nRemaining);

do {

// 找出下个分区的起始位置,方法上面介绍过

int runLen = countRunAndMakeAscending(a, lo, hi);

// 如果run stack中的run太小, 就扩展至min(minRun, nRemaining)

if (runLen < minRun) {

int force = nRemaining <= minRun ? nRemaining : minRun;

binarySort(a, lo, lo + force, lo + runLen);

runLen = force;

}

// 把run放到run stack, 条件满足会进行合并

ts.pushRun(lo, runLen);

ts.mergeCollapse();

// Advance to find next run

lo += runLen;

nRemaining -= runLen;

} while (nRemaining != 0);

// Merge all remaining runs to complete sort

assert lo == hi;

//合并剩下的run

ts.mergeForceCollapse();

assert ts.stackSize == 1;

我们来分析代码:首先ComparableTimSort ts = new ComparableTimSort(a, work, workBase, workLen);创建了ComparableTimSort对象

int stackLen = (len < 120 ? 5 :

len < 1542 ? 10 :

len < 119151 ? 24 : 49);

runBase = new int[stackLen];

runLen = new int[stackLen];

构造函数里对这三个变量赋值,他们是干嘛的?

/**

* A stack of pending runs yet to be merged. Run i starts at

* address base[i] and extends for len[i] elements. It's always

* true (so long as the indices are in bounds) that:

*

* runBase[i] + runLen[i] == runBase[i + 1]

*

* so we could cut the storage for this, but it's a minor amount,

* and keeping all the info explicit simplifies the code.

*/

private int stackSize = 0; // run的个数,run指的是分区

private final int[] runBase; // runBase[0]第一个分区里第一个元素下标,runBase[1]第二个分区第一个元素下标.....

private final int[] runLen; // runLen[0]第一个分区长度...

接着调用的是minRunLength,返回的数要么小于16,要么是16,要么介于[16, 32]之间

private static int minRunLength(int n) {

assert n >= 0;

int r = 0; // Becomes 1 if any 1 bits are shifted off

while (n >= MIN_MERGE) { //32

r |= (n & 1);

n >>= 1;

}

return n + r;

}

mergeCollapse:什么时候会进行合并呢?之所以进行判断是为了防止这样的情况:1000,10,100,10,10,这是五个分区的长度,最好的情况是先将小的分区合并,最后在和最大的分区合并,这个方法就是这个目的

//后两个分区的和大于前一个分区,则中间的分区与最小的分区先合并

//否则合并后两个分区

private void mergeCollapse() {

while (stackSize > 1) {

int n = stackSize - 2;

if (n > 0 && runLen[n-1] <= runLen[n] + runLen[n+1]) {

if (runLen[n - 1] < runLen[n + 1])

n--;

mergeAt(n);

} else if (runLen[n] <= runLen[n + 1]) {

mergeAt(n);

} else {

break; // Invariant is established

}

}

}

2. 当Comparable != null时,调用的方法类似

在Comparator != null情况下主要调用了TimSort.sort,看TimSort的代码与ComparableTimSort几乎一样,只是在元素比较时用了调用者给的Comparator来进行比较。

暂时分析到这.........

java中mergesort函数怎么用_MergeSort与TimSort,ComparableTimSort相关推荐

  1. Java中的函数传递

    转载自  Java中的函数传递 在C和C++中,函数的传递可以通过函数指针来实现.在C#中,函数传递可以通过委托.Action.Func来实现.Java中没有函数指针.没有委托,那函数要如何传递呢? ...

  2. java 柯里化_函数式编程(Java描述)——Java中的函数及其柯里化

    本文继续上一篇的内容 在Java中,函数可以表现为一个普通的方法.一个lambda表达式,又或者方法引用,甚至是匿名类.本文不会介绍匿名类这种形式. 方法 Java中的方法,Java使用方法这一概念来 ...

  3. Java中split函数的用法及使用示例

    Java中split函数的用法及使用示例 2010-05-04 10:21 日志原文:http://lhgc.blog.sohu.com/80444801.html java.lang.string. ...

  4. java秃顶_【本人秃顶程序员】在Java中使用函数范式提高代码质量

    ←←←←←←←←←←←← 快!点关注 在一个范式和技术堆栈一直在变化的世界中,保持竞争力和提高生产力和质量的斗争有时候证明是一项挑战. 在本文中,我想首先展示一下函数编程(FP)的优势,特别是加强Ja ...

  5. Java中Math函数的使用

    Java中Math函数的使用 说到Java中的Math函数,大家肯定不陌生,但是在真正使用的时候却犯了难,那么多方法,我们到底需要使用哪个呢? 为此,我特地研究了一些Math常用函数的使用,以方便大家 ...

  6. java中sort函数的使用

    java中sort函数的使用 写在前面 sort函数的基本格式 第一种基本格式 第二种基本格式 第三种基本格式cmp 函数的基本格式 写在前面 想必大家都知道C++里面的sort用过的都知道这个函数是 ...

  7. java中isnan函数_isNaN()函数以及JavaScript中的示例

    java中isnan函数 Prerequisite: NaN property in JavaScript 先决条件: JavaScript中的NaN属性 isNaN()函数 (isNaN() fun ...

  8. JAVA中split函数的用法

    JAVA中split函数的用法 只写经常使用的,并不完整. 1.基本用法,将字符串按照指定字符串进行分割,例如: public class Main {public static void main( ...

  9. Java中Math函数详解

    Java中Math函数的使用 算术计算 Math.sqrt() :计算平方根 Math.cbrt() : 计算立方根 Math.pow(a, b) : 计算a的b次方 Math.max( , ) : ...

  10. Java中pow函数的使用

    Java中的pow函数不能直接使用,要引用 错误示例如下: //水仙花数for(int i=100;i<=999;i++){int gewei=i%10;int shiwei=(i/10)%10 ...

最新文章

  1. Java基础笔记12
  2. SQL 注入 OrderBy/0ctf simplesqlin
  3. 凑微分公式_武忠祥真题班归纳(更新至多元函数微分学)
  4. java 强弱软虚_Java的四种引用,强弱软虚,用到的场景
  5. 物联网可编程高灵活度IoT网关或集线器是解决方案
  6. 【机器学习】传统目标检测算法之HOG
  7. ETL异构数据源Datax_工具部署_02
  8. corners边框_第11天|16天搞定前端,CSS的圆角边框,让人赏心悦目
  9. C语言/C++常见字符串函数
  10. iphone查看html源码的app,使用扩展App在Safari上查看源代码
  11. Linux Pthread学习记录
  12. 通用权限管理系统设计篇
  13. 【数据分析】面经(搬运)
  14. vivo x9s支持html,vivo X9s屏幕材质_vivo X9s屏幕分辨率-太平洋IT百科
  15. EOS核心仲裁论坛 | 保障你财产安全的应急措施
  16. 【Graph Neural Network 图神经网络】3.Spatial-based Graph Convolutional Networks 基于空间的图卷积网络
  17. 去除噪声 matlab 论文,基于MATLAB的语音去噪开题报告
  18. 【心电信号】基于多种滤波去除心电信号基线漂移含Matlab源码
  19. 运行前端项目之html
  20. 怎么听外国 播客_设计您想听的播客

热门文章

  1. 小区门禁卡可以复制到手机上吗_手机NFC可以复制小区用的门禁卡吗?步骤是什么?...
  2. Tomcat发布项目时,更改浏览器地址栏图标
  3. Python入门学习—元组/字符串(FishC)
  4. Windows Tomcat 内存溢出解决方法
  5. 西门子dcs系统组态手册下载_和利时FM148C卡件伊春DCS系统
  6. 全球完美打通元宇宙、DeFi、NFT的区块链游戏平台
  7. 托马斯微积分 从入门到失望
  8. 三菱PLC定位控制1
  9. 文章发送到多平台软件:融媒宝
  10. t470键盘拆解_做工保持良好水准 ThinkPad T470笔记本拆机解析