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

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

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

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

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

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

插入排序算法

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

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

图解原理

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

 

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

 

编号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");}


 

对比结果

 


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • 归并排序和快速排序都用到了分治思想,非常巧妙。我们可以借鉴这个思想,来解决非排序的问题。 归并排序 归并排序的核心...
    被吹落的风阅读 1,327评论 0 3
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 5,725评论 0 13
  • 概述 排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部...
    蚁前阅读 5,170评论 0 52
  • 搞懂基本排序算法 上篇文章写了关于 Java 内部类的基本知识,感兴趣的朋友可以去看一下:搞懂 JAVA 内部类;...
    醒着的码者阅读 1,184评论 3 4
  • 一. 写在前面 要学习算法,“排序”是一个回避不了的重要话题,在分析完并查集算法和常用数据结构之后,今天我们终于可...
    Leesper阅读 2,528评论 0 40