插入排序算法和归并排序算法的分水岭,你造吗?

Posted Java软件编程之家

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了插入排序算法和归并排序算法的分水岭,你造吗?相关的知识,希望对你有一定的参考价值。

很多情况下,只要跟算法相关的可能都有现成的开源工具包拿来就用,比如排序算法

现实业务开发时,为了提升效率,确实建议使用现成的成熟的开源工具。

但是,我们如果只知道使用开源工具包的话,如果有一天你去面试被问到的话...

所以,我们应用抽点时间,去学习我们常用的工具包或框架的实现思想和细节。以备急需。

这也就是我们需要不停学习的原因之一吧。

插入排序算法

排序算法场景不用多说了,比如棋牌游戏中一键排序扑克牌,比如电商系统按销量从高到低排序等等。

排序算法实现有多种,比如插入排序,希尔排序,快速排序,冒泡排序,归并排序等等。排序算法其实大有讲究,每一种算法在不同的输入规模需要的时间均不一样。用专业术语来说就是他们的时间复杂度不同。下面我们主要来看一下插入排序和归并排序的实现原理和不同输入规模下需要的计算时间。

图解原理

下面我们通过画图+文字方式来讲解插入排序原理。先假定某此会议有四个人,会议要求他们坐成一排,且按编号从左边开始从小到大排列,假定他们到场初始化坐位编号排序为:

 

现在需要按照会议要求按编号从小到大进行排序,其中一人就提议按照插入排序法方式进行位置的调整,从第二位开始(插入排序法一般从第二项开始)进行,具体如下:

 

编号5开始调整座位:

 

 

插入排序算法和归并排序算法的分水岭,你造吗?

编号3调整座位:

 

插入排序算法和归并排序算法的分水岭,你造吗?

编号6调整座位:

 

 

插入排序算法和归并排序算法的分水岭,你造吗?


可以看到,现在顺序变成:

 

 

插入排序算法和归并排序算法的分水岭,你造吗?

代码实现

排序算法:

 

public static void insertSortAsc(Integer[] source) { if (source == null || source.length < 1) { return; } int len = source.length; //i = 1表示从第二项开始,下标从0开始 for (int i = 1; i < len; i++) { //当前项 int currentVal = source[i]; //当前项的前面项最大索引 int j = i - 1; //j >= 0表示当前存在哥们,需要继续询问 //currentVal < source[j]表示询问你比我大么? while (j >= 0 && currentVal < source[j]) { //进入这里表示前面项确实比我大 //source[j + 1] = source[j]表示那你往后挪一步 source[j + 1] = source[j]; //继续向前询问 j--; } //前面哥们比自己小,或者前面没有哥们,回退一步回来坐下 source[j + 1] = currentVal; }}

 

测试:

public static void main(String[] args) {
Integer[] test = {7,5,3,6};
System.out.println("排序前:" + JSON.toJSONString(test)); insertSortAsc(test); System.out.println("排序后:" + JSON.toJSONString(test));}


 

输出:

 

插入排序算法和归并排序算法的分水岭,你造吗?

 

复杂度分析

插入排序算法的时间复杂度范围为: O(n) - O(n * n),其中n为输入项的数量,比如上面输入项为4项,最好情况下时间复杂度为O(n)。最好情况一般指输入项本身已经排好顺序。最差和平均情况时间复杂度为O(n * n),最差情况一般指输入项本身按目标排序反向顺序。时间一般指执行所耗费的时间

 

插入排序算法空间复杂度为O(1),表示不管输入项数量大小,运行核心计算所需要的空间相较于其他排序算法来说是固定的,空间一般指内存使用。

归并排序算法

归并算法类似二分查找,它遵循分治法范畴,分治法的核心思想是将原问题分解为几个规模较小但类似于原问题的子问题。然后通过递归的方式去求解这些子问题。最后一层一层的合并子问题的解来得到原问题的解。

分治法在每层递归一般有以下三个步骤:

分解:分解原问题为若干子问题

求解:递归求解子问题

合并:合并子问题的解得到原问题的解

归并排序算法实现分治法其原理如下:

分解:递归分解待排序的n个元素的序列/数组为n/2个子序列/数组

求解:递归排序一分为二的两个子序列

合并:递归合并一分为二已经排好序的序列

文字描述可能太抽象和枯燥,下面我们图解算法方式讲解算法的实现原理

图解原理

我们复用上面的案例来讲解通过归并算法如何实现会议座位从左到右从小到大排序。

上面待排序项如下:

 

插入排序算法和归并排序算法的分水岭,你造吗?

分解归并排序如下:

 

插入排序算法和归并排序算法的分水岭,你造吗?

上图完全展示了归并算法的核心思想,分解 -> 排序 -> 归并。就拿5736排序来讲解,具体步骤如下:

分解:5736大致对半分解为两组子问题5736,继续将57大致按对半分解为两组57。同样也将36按对半分解为36两组子问题,此时对n项输入分解为6个子问题来处理

求解:从低往上排序,例如7排序后得到75排序后得到5,然后进行递归合并。

首次归并将75归并为一组,将36归并为一组。归并过程进行求解(排序),将75排序得到57,将36排序得到36

继续归并将5736归并为5736。归并过程进行求解(排序)所以最后得到3657

合并:由于合并和求解在递归过程并行,所以合并操作在求解中已经有所体现。

核心解释:每次排序是将两个子问题进行排序,每个子问题实际是放到两个序列/数组上的,比如75这两个分别放到不同的数组。然后在循环迭代体将这两个进行比较,把小的放前面,大的放后面保存到另一个数组中。

代码实现

递归按n/2进行分解,当分解为最小子问题时(递归到只有一个元素不可再分解时),递归开始回升,进入归并排序,回升一层,归并排序一层

/** * 通过分治算法实现归并排序 * * @param source 需要排序的List * @param beginIndex 开始下标 从0开始,包含开始下标 * @param endIndex 结束下标 从0开始, 包含结束下标 */public static void mergeSortAsc(List<Integer> source, int beginIndex, int endIndex) { //当前面一次递归只有一个元素时,开始回升当前递归 if (beginIndex < endIndex) { int subIndex = (beginIndex + endIndex) / 2; //排序左边段,当递归回升后左边所有已经排好序 mergeSortAsc(source, beginIndex, subIndex); //排序右边段,当递归回升后右边所有已经排好序 mergeSortAsc(source, subIndex + 1, endIndex); //合并并排序前面递归排好序的两段 doMergeSortAsc(source, beginIndex, subIndex, endIndex); }}


归并排序:

private static void doMergeSortAsc(List<Integer> source, int beginIndex, int subBeginIndex, int endIndex) {
if (SetsUtils.isEmpty(source)) { return; } if (endIndex > source.size()) { throw new RuntimeException("endIndex Not greater than source size"); }
int num1 = subBeginIndex - beginIndex + 1; int num2 = endIndex - subBeginIndex;
List<Integer> left = SetsUtils.list(num1); List<Integer> right = SetsUtils.list(num2);
for (int i = 0; i < num1; i++) { left.add(source.get(beginIndex + i)); } left.add(num1, Integer.MAX_VALUE);
for (int i = 0; i < num2; i++) { right.add(source.get(subBeginIndex + i + 1)); } right.add(num2, Integer.MAX_VALUE);

int i = 0; int j = 0;
for (int k = beginIndex; k <= endIndex; k++) { if (left.get(i) > right.get(j)) { source.set(k, left.get(i)); i++; } else { source.set(k, right.get(j)); j++; } }}


测试:

 

public static void main(String[] args) {
List<Integer> test = SetsUtils.list(); test.add(7); test.add(5); test.add(3); test.add(6);
System.out.println("排序前:" + JSON.toJSONString(test)); mergeSortAsc(test, 0, test.size() -1); System.out.println("排序后:" + JSON.toJSONString(test));
}

 

输出:

 

 

复杂度分析

归并排序算法的时间复杂度为: O(nlogn)),其中n为输入项的数量。时间一般指执行所耗费的时间

 

归并排序算法空间复杂度为O(n),输入项数量越大,运行核心计算所需要的空间相较于其他排序算法来说越大,空间一般指内存使用。

对比

对比一般需要得出适用场景的结论,一般通过分析时间复杂度和空间复杂度进行定论,很多人喜欢讨论时间换空间,空间换时间,这些都是一些术语而已,进一步是要详细分析计算出算法的具体时间复杂度和空间复杂度,然后得出结论。

我们如何在多种不同的排序算法中选择一种算法,其实者首先取决于我们的关注的,比如我们的计算机有足够的空间去浪费,我们需要的是速度,例如电商网站的商品排序。那么我们就应该分析不同算法的时间复杂度然后进行对比。相反,如果我们更多的关注内存占用而不关心时间,例如大数据分析的任务调度,那么我们就应该分析不同算法的空间复杂度然后进行对比。由于时间问题,本篇不详细计算插入排序和归并排序的时间复杂度,后面再写一篇文章进行补充,笔者这里先给出答案:

1、插入排序法和归并排序法的时间复杂度对比取决于输入规模

2、当输入规模大于1W时归并排序法优于插入排序法

3、相反,当输入规模小于1W时,插入排序法优于归并排序法

4、当输入规模达到10W以上时,不推荐使用插入排序法

5、当输入规模达到20W以上时,不推荐使用归并排序法

对比代码

public static void main(String[] args) { List<Integer> test = SetsUtils.list(); int i = 10; Random r = new Random(); while (i++ < 50000) { test.add(r.nextInt(10000)); } List<Integer> test1 = new ArrayList<>(test); List<Integer> test2 = new ArrayList<>(test);
long t = System.currentTimeMillis();
System.out.println(JSON.toJSONString(test1)); mergeSortAsc(test1, 0, test1.size() - 1); System.out.println(JSON.toJSONString(test1));
long t2 = System.currentTimeMillis();
System.out.println("归并排序算法耗时:" + (t2 - t) + "ms");
long t3 = System.currentTimeMillis();
System.out.println(JSON.toJSONString(test2)); insertSortAsc(test2); System.out.println(JSON.toJSONString(test2));
long t4 = System.currentTimeMillis();
System.out.println("插入排序算法耗时:" + (t4 - t3) + "ms");}


 

对比结果

 


以上是关于插入排序算法和归并排序算法的分水岭,你造吗?的主要内容,如果未能解决你的问题,请参考以下文章

算法之-归并排序算法,插入排序算法

常见排序算法的实现(归并排序快速排序堆排序选择排序插入排序希尔排序)

算法导论之插入排序和归并排序

Java排序算法分析与实现:快排冒泡排序选择排序插入排序归并排序

常见排序算法基本原理及实现(快排,归并,堆排,直接插入.....)

常见排序算法基本原理及实现(快排,归并,堆排,直接插入.....)