从这一篇博文开始,我们将开始讨论排序算法。所谓排序算法,就是将给定数据根据关键字进行排序,最终实现数据依照关键字从小到大或从大到小的顺序存储。而这篇博文,就是要介绍一种简单的排序算法——插入排序(Insertion Sort)。

  为了使精力专注于排序算法本身,而不是对数据的分析、处理,若无特殊说明,我们每一篇介绍排序算法的博文,均做如下假定:

  1.数据存储于一个数组之中,且数据个数N即数组大小

  2.数据类型即int

  3.排序目标为从小到大

  那么,插入排序是怎样的算法呢?其实插入排序的思想来源就是“插队”。

  首先我们想象一下这个现实场景:有一个长度为n的队伍,队伍中每个人都比前面的人要高,你是新来的第n+1个人,现在位于队尾,请问你该怎么找到自己应处的位置,以使得队伍保持原有顺序(从矮到高)?

  这个问题所有人都会解,那就是:我与前一个人比较,若我更矮,则我站到他前面,持续此比较直至我比我前面的人高或同样高,或者我来到第一名为止。

  这个问题的解法,就是插入排序的根本。

  现在我们将场景转换到数组中:有一个大小为n的int型数组a,a[0]至a[n-2]已按从小到大排好序,但是第a[n-1]即最后一个元素是“新来的”,现在要将新元素放到正确的位置上,以使得数组保持从小到大的顺序,该怎么做?

  同样的,我们令新元素不断地与前一个元素比较,若小于前一个元素则两者交换位置,直至新元素大于等于前一个元素,或新元素到达a[0]时停止。

void unfinishedInsertionSort(int *a, unsigned int n)
{
//“新元素”的当前位置CurrentPosition从N-1开始//只要新元素尚未到达a[0],且前一个元素小于新元素,则不断地令新元素与前一个元素交换位置
for (int CurPos = n - 1;CurPos != && a[CurPos - ] > a[CurPos];--CurPos)
swap(&a[CurPos], &a[CurPos - ]);
}

  上述想法就是插入排序的雏形:若元素Xn前面的X0到Xn-1均已排好序,那么Xn只需要不断地向前“插入”,直至前一个元素与Xn的大小关系符合顺序,或Xn到达第一个位置即可完成排序。

  不难发现,雏形中尚待解决的问题就是:如何令Xn前面的所有元素排好顺序?

  这个问题可以尝试用递归的想法解决:要令Xn排好序,就需要令X0到Xn-1有序,而要令Xn-1排好序,则需要令X0到Xn-2有序……最后会发现,要想令X1排好序,则需要令X0到X0有序,而X0到X0一定是有序的,因为只有一个元素,也就是说此刻递归的基准情形出现了,而根据X0到X0的有序,可以得到X0到X1的有序,X0到X2的有序,直至得到X0到Xn-1的有序,也就有了X0到Xn的有序。因此这个递归的想法可行,而且这个想法就是插入排序从雏形到完整的解决思想。

  

  将上述想法与插入排序的雏形相结合后,我们就得到了插入排序的实现方法:设数组a有n个元素,令下标x从1递增至n-1,对于每个a[x]我们都执行一次“插队”操作(即插入排序的雏形操作)。

  下面为插入排序的例程:

void InsertionSort(int *a, unsigned int size)
{
int temp; //temp用于暂存执行插入的元素的值,使用temp可以避免元素间的交换
int CurPos; //CurPos表示执行插入的元素当下所处的下标 //StartPos表示执行插入操作的元素开始插入时的下标
//令StartPos从1递增至size-1,对于每个a[StartPos],我们执行向前插入的操作
for (int StartPos = ;StartPos < size;++StartPos)
{
temp = a[StartPos];
for (CurPos = StartPos;CurPos != && a[CurPos - ] > temp;--CurPos)
a[CurPos] = a[CurPos - ]; //令前一个元素后移,相当于令当前元素前移,但循环结束后记得令temp执行真实的移动 a[CurPos] = temp;
}
}

  计算插入排序的时间复杂度并不难,最坏的情况是数组中元素恰好完全反序,此时插入排序的内循环必然执行至CurPos==0为止,而外循环从StartPos=1至StartPos=size-1共size-1次,每一次内循环执行StartPos次,即内循环总共执行1+2+3+……+size-1次,即size(size-1)/2次,即O(n2)

  大部分人在学习C语言时就接触过冒泡排序,所以我们将不再对冒泡排序进行介绍。从时间复杂度上看,插入排序和冒泡排序是一样的,都是O(n2),但是在实际执行时,插入排序会比冒泡排序好得多,原因就是在数据“部分有序”时,插入排序可以减少很多比较次数,而冒泡排序的比较则是“固定的”。

  举例来说,现有数据1,2,3,4,5,7,6。若插入排序则需比较7次,交换1次(元素2,3,4,5,7均一次比较即结束,元素6与7比较一次,交换,再与5比较一次,结束)。而冒泡排序则需比较6+5+4+3+2+1=21次,交换1次。

  造成冒泡排序与插入排序间差异的主要原因就是:插入排序的比较“更充分地利用了已存在的顺序信息”,而冒泡排序无论如何都需要(N-1)+(N-2)+(N-3)+……+1次比较。其实在数据“接近有序”的情况下,插入排序几乎是最快的排序,完全有序的数据其只需要N-1次比较即结束排序。可以说在O(n2)这个级别的排序算法中,插入排序是绝对的首选。

  不过我们再次回顾上述例子,会发现两个排序算法虽然需要的比较次数不同,但需要的交换次数却是相同的,即使你换一个数据序列,这两个排序算法需要的交换次数依然是一样多,这是为什么呢?我们下一篇博文将揭开这个秘密。

  附:选择排序也是常见的初学排序算法,它需要的比较次数为N+N-1+N-2+……+2,比冒泡排序还要多N-1次,但是它的交换次数有可能比插入和冒泡都要少,比如数据5,4,3,2,1需按从小到大排序,若使用插入或冒泡排序,将需要10次交换,而选择排序的交换操作只需要2次。但是实际使用时插入排序依然比选择排序优先考虑,因为:

  选择排序的“实质交换”虽然可以更少,但形式上来说,选择排序是固定执行N次交换的:每一趟我们都会找出当前最小元素然后将其交换至正确位置,所以肯定有N次交换。只不过会出现“当前元素恰好在正确位置上”的情况,从而没有“实质交换”罢了,但代价依然是有的,比如判断当前元素位置与目标位置是否不同,或直接执行自己与自己的交换。此外,在选出当前最小元素时,我们都认为是“比较操作”,然而实际上这里面混杂着很多赋值操作。而这些不起眼的操作都会使得选择排序没有理想的那么快。

深入浅出数据结构C语言版(16)——插入排序的更多相关文章

  1. 数据结构C语言版 表插入排序 静态表

    数据结构C语言版 表插入排序.txt两个人吵架,先说对不起的人,并不是认输了,并不是原谅了.他只是比对方更珍惜这份感情./*  数据结构C语言版 表插入排序  算法10.3 P267-P270  编译 ...

  2. 深入浅出数据结构C语言版(5)——链表的操作

    上一次我们从什么是表一直讲到了链表该怎么实现的想法上:http://www.cnblogs.com/mm93/p/6574912.html 而这一次我们就要实现所说的承诺,即实现链表应有的操作(至于游 ...

  3. 深入浅出数据结构C语言版(1)——什么是数据结构及算法

    在很多数据结构相关的书籍,尤其是中文书籍中,常常把数据结构与算法"混合"起来讲,导致很多人初学时对于"数据结构"这个词的意思把握不准,从而降低了学习兴趣和学习信 ...

  4. 深入浅出数据结构C语言版(8)——后缀表达式、栈与四则运算计算器

    在深入浅出数据结构(7)的末尾,我们提到了栈可以用于实现计算器,并且我们给出了存储表达式的数据结构(结构体及该结构体组成的数组),如下: //SIZE用于多个场合,如栈的大小.表达式数组的大小 #de ...

  5. 深入浅出数据结构C语言版(17)——希尔排序

    在上一篇博文中我们提到:要令排序算法的时间复杂度低于O(n2),必须令算法执行"远距离的元素交换",使得平均每次交换减少不止1逆序数. 而希尔排序就是"简单地" ...

  6. 深入浅出数据结构C语言版(19)——堆排序

    在介绍优先队列的博文中,我们提到了数据结构二叉堆,并且说明了二叉堆的一个特殊用途--排序,同时给出了其时间复杂度O(N*logN).这个时间界是目前我们看到最好的(使用Sedgewick序列的希尔排序 ...

  7. 深入浅出数据结构C语言版(20)——快速排序

    正如上一篇博文所说,今天我们来讨论一下所谓的"高级排序"--快速排序.首先声明,快速排序是一个典型而又"简单"的分治的递归算法. 递归的威力我们在介绍插入排序时 ...

  8. 深入浅出数据结构C语言版(4)——表与链表

    在我们谈论本文具体内容之前,我们首先要说明一些事情.在现实生活中我们所说的"表"往往是二维的,比如课程表,就有行和列,成绩表也是有行和列.但是在数据结构,或者说我们本文讨论的范围内 ...

  9. 深入浅出数据结构C语言版(3)——递归简论

      相信学习过C语言的读者都已经接触过递归(不论是谭浩强的C程序设计还是C Primer Plus都有递归程序),本文就是对递归的基本原则进行简要介绍.首先,我们写一个基本的递归函数作为例子: int ...

随机推荐

  1. PHP通过phpmailer批量发送邮件功能

    前端页面代码: 注意:目前发送人使用的qq邮箱支持的不是特别友好.建议使用网易 新浪 163等其他邮箱. 需要用到phpmailer包 下载地址:https://sourceforge.net/pro ...

  2. LoadRunner性能测试-loadrunner工具破解

    Loadrunner11破解 破解工具下载:http://pan.baidu.com/disk/home?errno=0&errmsg=Auth%20Login%20Sucess&&a ...

  3. 引水入城[NOI2010 ]

    题目描述 在一个遥远的国度,一侧是风景秀美的湖泊,另一侧则是漫无边际的沙漠.该国的行政区划十分特殊,刚好构成一个N行M列的矩形,如上图所示,其中每个格子都代表一座城市,每座城市都有一个海拔高度. 为了 ...

  4. Hibernate批量操作(一)

    在项目的开发过程之中,我们常会遇到数据的批量处理问题.在持久层采用Hibernate框架时,在进行批量操作时,需要考虑Hibernate实现机制带来的一些问题. 我们知道在每个Hibernate Se ...

  5. git分支管理之分支管理策略

    分支管理策略 阅读: 246888 通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息. 如果要强制禁用Fast forward模式,Git就 ...

  6. Java中StringBuffer类append方法的使用

    public static void testAppend() { StringBuffer sb = new StringBuffer("This is a StringBuffer!&q ...

  7. 53. Maximum Subarray【leetcode】

    53. Maximum Subarray[leetcode] Find the contiguous subarray within an array (containing at least one ...

  8. 在Entity Framework 中用 Code First 创建新的数据库

    在Entity Framework 中用 Code First 创建新的数据库 (原文链接) 本文将逐步介绍怎样用Code First 创建新数据库,使用在代码中定义类和API中提供的特性(Attri ...

  9. 在WebGL场景中使用2DA*寻路

      这篇文章将讨论如何在一个自定义的地面网格上进行简单的2D寻路,以及确定路径后如何使用基于物理引擎的运动方式使物体沿路径到达目标地点.读者需要预先对WebGL和Babylonjs知识有一些了解,可以 ...

  10. SpringMVC详解(一)------入门实例

    本系列教程我们将详细的对SpringMVC进行介绍,相信你在学完本系列教程后,一定能在实际开发中运用自如. 1.什么是 SpringMVC ? 在介绍什么是 SpringMVC 之前,我们先看看 Sp ...