【算法】1 由插入排序看如何分析和设计算法

插入排序及其解决思路

算法的作用自然不用多说,无论是在校学生,还是已经工作多年,只要想在计算机这条道路走得更远,算法都是必不可少的。

就像编程语言中的“Hello World!”程序一般,学习算法一开始学的便是排序算法。排序问题在日常生活中也是很常见的,说得专业点:

输入是:n个数的一个序列 <a1,a2,...,an1,an> <script type="math/tex" id="MathJax-Element-441"> </script>
输出是:这n个数的一个全新的序列 <a,1,a,2,...,a,n1,a,n> <script type="math/tex" id="MathJax-Element-442"> </script>,其特征是 a,1a,2...a,n1a,n

举个例子,在本科阶段学校往往要求做的实验中大多是“学生管理系统”、“信息管理系统”、“图书管理系统”这些。就比如“学生管理系统”中的分数,每当往里面添加一个新的分数时,便会和其他的进行比较从而得到由高到低或由低到高的排序。

我本人是不太喜欢做这种管理系统的…… 再举个比较有意思的例子。

大家肯定都玩过扑克牌,撇开部分人不说,相信大部分童鞋都热衷于将牌按序号排好。那么这其中就蕴含着算法的思想:

  • 1)手中的扑克牌有2种可能:没有扑克牌(为空)或者有扑克牌且已排好序。
  • 2)从桌上抓起一张牌后,从左往右(从右往左)依次进行比较,最终选择一个合适的位置插入。

简单的说,插入排序的精髓在于“逐个比较”。

在列出代码之前,先来看看下面的第一张图,我画的不是太好,就是有没有经过排序的 “8,7,4,2,3,9”几个数字,根据上面的描述,将排序过程描述为:

  • 1)将第二个数字“7”和“8”比较,发现7更小,于是将“8”赋值到“7”所在的位置,然后将7赋值给“8”所在的位置。
  • 2)将”4“移到”7“所在的位置,”7“和”8“后移一位。
  • 3)同样的步骤,将”2“和”3“移到”4“的前面。
  • 4)”9“比前面的数字都大,故不移动。

这里写图片描述

仅仅是这样的描述还是不够的,我们需要更加专业一点。

  • 1)设置一个循环,从第二个数字开始(索引为1)不断与前面的数字相比。
  • 2)每次循环开始时作为比较的数的索引为j,设置temp为其值。(因为在前面也看到了,将”8“赋值到”7“的位置时,如果不将”7“保存起来,那么便丢失了这个数字。)
  • 3)取得j的前一位i。
  • 4)只要i仍在数组中,并且索引为i处的值大于temp,就将i后一位的值设为i处的值,同时将i减1。
  • 5)在i不在数组中或i处的值不必temp大时结束第四部的循环,然后将i后一位的值设置为temp。

将上面这部分描述翻译为insertion_sort函数,下面是完整的测试程序。

// C Plus Plus
// A是数组,j是遍历整个数组的索引,temp是每次取出来作比较的值
void insertion_sort() {
    for(int j = 1; j < n; j++) {
        int temp = A[j];
        int i = j-1;
        while (i >= 0 && A[i] > temp) {
            A[i+1] = A[i];
            i = i-1;
        }
        A[i+1] = temp;
    }
}
updated at 2016/09/24
// Java
    public int[] insertion(int[] nums) {
        for (int i = 1; i < nums.length; i++) {
            int tmp = nums[i];
            int prev = i - 1;
            while (prev >= 0 && nums[prev] > tmp) {
                nums[prev + 1] = nums[prev];
                prev -= 1;
            }
            nums[prev + 1] = tmp;
        }
        return nums;
    }

下面是能够帮助我们理解算法的正确性的循环不变式的三条性质:

初始化:循环第一次迭代之前,它为真。
保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真。
终止:在循环终止时,不变式能够提供一个有助于证明算法正确性的性质。

就比如上面排序的例子,终止意味着在最后一次迭代时,由传入数组元素构成的子数组元素都已排好序,因此此时子数组就等同与原数组,于是循环终止。

学习如何分析算法

继续分析排序算法,我们知道排序10000个数肯定要比排序10个数所花费的时间更长,但除了输入的项数外就没有其他的影响因素吗?当然有,比如说输入的序列的已被排序的程度,如果是“23456781”这个序列,我们仅仅需要将1放到首位就好,而输入是”87654321“,我们就需要将7到1依次与其前面的数字进行比较。

关于算法的分析也有两个定义:

1)输入规模,当考虑的是排序算法时,那么规模就指的是项数;如果考虑的是图算法,那么规模就是顶点数和边数。
2)运行时间,名义上来说就是算法执行的时间,但实际上我们在分析一个算法时考量的算法执行的操作数或步数。

下面我们通过前面排序算法的伪代码来分析它的运行时间。

INSERTION-SORT(A)
1   for j = 2 to A.length                  // 代价c1,次数n   
2       temp=A[j];                         // 代价c2,次数n-1
3       // 将A[j]插入到已排序的A[1..j-1]      // 代价0,次数n-1
4       i=j-1;                             // 代价c4,次数n-1
5       while i>0 and A[i]>temp            // 代价c5  
6           A[i+1]=A[i];                   // 代价c6
7           i=i-1;                         // 代价c7       
8       A[i+1]=temp;                       // 代价c8,次数n-1

代价为c1处的次数为n应该比较好理解对吧,从j=1到j=n一共有n步,j=n也应该包括在内,因为这是算法终止的情况。而j=n时,程序直接终止了,所以在代价c2、c3、c7处次数都为n-1。

那么在while循环中呢,代价为c4的时候次数为多少呢,很显然应该是 nj=2tj ,而c5和c6在while循环里总有一次它不会去执行,因此次数为 nj=2(tj1)

将代价和次数相乘,便得到了该算法所需的总时间:
T(n)=c1n+c2(n1)+c4(n1)+c5nj=2tj+c6nj=2(tj1)+c7nj=2(tj1)+c8(n1)

除此之外我们还可以来对算法进行最好和最坏情况的分析:
1)在最好情况下,也就是整个输入数组其实都是排好序的,那么它根本没法进入while循环,也就是说当i取初值j-1时,有 A[i]temp ,从而对 j=2,3,4...n 都有 tj=1

那么算法的总时间也就可以算出来了:

  • 16
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 29
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 29
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值