从这一篇博文开始,我们将开始讨论排序算法。所谓排序算法,就是将给定数据根据关键字进行排序,最终实现数据依照关键字从小到大或从大到小的顺序存储。而这篇博文,就是要介绍一种简单的排序算法——插入排序(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. Python 获取当前路径的方法

    Python2.7 中获取路径的各种方法 sys.path 模块搜索路径的字符串列表.由环境变量PYTHONPATH初始化得到. sys.path[0]是调用Python解释器的当前脚本所在的目录. ...

  2. Redis-事务即简单锁应用

    Redis支持简单的事务, Redis允许一组命令在单一步骤中执行, 事务有两个属性 事务是一个单独的隔离操作, 事务中所有的命令都会序列化, 按照顺序执行. Redis事务是原子性的, 即要么都执行 ...

  3. (1)xcode基本设置和控制器等介绍

    1.在IOS应用程序中,如果没有对storyBoard进和设置它的界面是非常大,有时候如果把元素放在右边会出现运行程序时超出显示界面而不显示的问题.为了解决这个问题我们通常会在用模拟器设置调试界面的时 ...

  4. python--代码统计(进阶版)

    在上一篇的随笔中发表了代码统计小程序,但是发表后,我发现,以前写的代码怎么办 写了那么多,怎么就从0开始了呢,,,,我还是个孩子啊,不能这么残忍 于是,代码统计进阶版:统计当前目录下所有指定文件类型的 ...

  5. POJ-1915 Knight Moves (BFS)

    Knight Moves Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 26952   Accepted: 12721 De ...

  6. 中国大学MOOC-翁恺-C语言程序设计习题集-解答汇总

    中国大学MOOC-翁恺-C语言程序设计习题集 PAT 习题集 02-0. 整数四则运算(10) 时间限制 400 ms 内存限制 65536 kB 代码长度限制 8000 B 判题程序 Standar ...

  7. netstat命令---输出网络相关的信息

    简介 Netstat 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Member ...

  8. Python查询SQLserver数据库备份(抛砖引玉)

    通过python pymssql直接访问SQLserver数据库,查找其数据库mode,这个脚本具有很强的抛砖引玉特性: 1.可以巡检多台多数据库服务器 2.query内容可以多样化,譬如查询死锁.连 ...

  9. Learn c language the one day

    第一个c程序 #include<stdio.h> int main() { ; ; printf_s("请输入两个数字:"); scanf_s("%d,%d& ...

  10. python实战===输入密码以******的形式在cmd中展示

    #设置密码输入,显示为****** import msvcrt,sys def pwd_input(): chars = [] while True: try: newChar = msvcrt.ge ...