深入浅出理解排序算法系列(二) 插入排序算法的实现与性能分析

概述:

       插入排序的基本原理是在有序序列中插入一个元素,保持序列有序。也就是说每次将一个待排序的元素,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。本文具体为大家讲解插入排序的三种具体实现方法:

  • 直接插入排序(Straight Insert Sort)
  • 二分插入排序 (Bisection Insert Sort)
  • 希尔排序 (Shell Sort)

本系列的文章包含:

1、排序算法概述

2、插入排序算法的实现与性能分析

3、交换排序算法的实现与性能分析

4、选择排序算法的实现与性能分析

5、归并排序算法的实现与性能分析

6、基数排序算法的实现与性能分析

7、总结:各种内部排序方法的比较

(注:先列出框架,后续不断更新中;本系列文章的算法实现全都基于Java语言,掌握其算法核心原理后使用任何语言都是可以的)

本文作者:Horace_hr       

作者博客地址:https://blog.csdn.net/Horace_hr

本文地址:https://blog.csdn.net/Horace_hr/article/details/81557157(转载请标明出处,谢谢!)

一、插入排序的分类

       现实生活中,当玩扑克牌游戏的时候,将新取的牌插入到已有序排列的牌中,这就是插入排序。我们根据插入位置的不同,以图示为大家做说明。

深入浅出理解排序算法系列(二) 插入排序算法的实现与性能分析_第1张图片

常见的插入排序主要有两种:直接插入排序希尔排序。为帮助小伙伴们进一步学习解插入排序,本文在直接插入排序的基础上补充二分插入排序

深入浅出理解排序算法系列(二) 插入排序算法的实现与性能分析_第2张图片

 

二、直接插入排序算法

(一)算法实现方法

       直接插入排序的要求是假设待排序的元素存放在数组arary[0..n-1]中,开始时候先将第0个记录组成一个有序的子表,然后依次将后续的记录插入到这个有序子表中去,并且保持子表的有序性。

       具体步骤如下:

  • 将array[i]暂存在临时变量temp中;
  • 将temp与array[j](j=i-1,i-2,...,0)依次比较,当temp=array[j]为止(此时j+1即为array[i]插入的位置);
  • 将temp插入到底j+1的位置上;
  • 令i=1,2,3...,n-1,重复上述前三步骤;

     我们以Array[12,15,9,20,6,31,24]为例实现简单插入排序,排序过程如下图:

深入浅出理解排序算法系列(二) 插入排序算法的实现与性能分析_第3张图片

/**
 * 直接插入排序算法示例
 * @param array
 * @return
 */
public int[] StraightInsertSort(int[] array) {
   int i, j, temp;
   for (i = 1; i < array.length; i++) { //n-1次扫描
       temp = array[i];            //定义临时变量,将待插入的第i个元素暂存在temp中
       for (j = i - 1; j >= 0 && temp < array[j]; j--) {
           array[j + 1] = array[j];    //把前面比array[i]大的元素往后移动
        }    
       array[j + 1] = temp;//当temp>=array[j]的时候,此时将array[i]插入到第j+1的位置上
    }
    return array;
}

【思考】

       以上算法实现了简单插入排序,我们用“j>=0”用来控制下表越界,但这并不是极好的写法,因为每次循环都需要多做一步的条件判断,显然排序效率不高,因此我们可以通过添加监视哨的方法做优化。原理如下:

  • 将待排序的N个元素从下标为1的存储单元开始依次存放在数组array中;
  • 将array[0]设置为一个“监听哨”,在查找之前先将array[i]赋值给array[0],每循环一次只需要进行元素的比较,不需要关心下标是否越界;
  • 当比较到第0个位置的时候,array[0]==array[i]是肯定成立的,所以结束循环。由于通过设置“监听哨”,只需要一个循环判断条件就OK了,这将提高算法运行的效率。

具体代码实现如下

/**
 * 直接插入排序算法优化(加上监视哨,减少循环判断,提高算法效率)
 * @param array
 * @return
 */
public int[] StraightInsertSort(int[] array) {
	int i, j;
	for (i = 1; i < array.length; i++) { //n-1次扫描
		array[0] = array[i];             //将待插入的第i个元素暂存在array[0]中
		for (j = i - 1; array[0] < array[j]; j--){
			array[j + 1] = array[j];     //把前面比array[i]大的元素往后移动
		}
		array[j + 1] = array[0];         //当array[0]>=array[j]的时候,此时将array[i]插入到第j+1的位置上
	}
	return array;
}

 注意】使用array[0]作为监听哨之后,具有N个存储单元的顺序表只能存放N-1个元素,因此注意顺序表的存储长度。

(二)算法性能分析

  1. 空间复杂度:仅用了一个辅助单元,空间复杂度为O(1);
  2. 时间复杂度:根据插入array[i]时元素比较的次数Ci和移动次数Mi的最大值、最小值、平均值来计算;

深入浅出理解排序算法系列(二) 插入排序算法的实现与性能分析_第4张图片

    3、算法稳定性:稳定;

【思考】

      从直接插入排序算法的时间复杂度上,其实可以看出,当原始数据越接近有序,排序速度越快,在最坏情况下待排序序列按照关键词逆序排列,时间复杂度为O(n^2),平均情况下算法复杂度仍为O(n^2)。所以为了提高排序速度,我们要

  • 减少元素的比较次数
  • 减少元素的移动次数

     如何优化算法呢?考虑个问题,当直接插入排序时候循环遍历有序序列,顺序查找待插入元素array[i]的插入位置,这个顺序查找的过程是否可以优化呢?答案是可以的,我们可以使用二分插入算法!

 

三、二分插入排序算法

       二分插入排序使用到二分法思想,如果对此不了解小伙伴可熟悉此知识点(在此分享一篇相对不错的博客《二分查找各种情况大总结》,供大家参考)

(一)算法实现方法

深入浅出理解排序算法系列(二) 插入排序算法的实现与性能分析_第5张图片

       如上图,给定一个待排序序列array数组,将array[4]位置上的元素7插入到前段的有序序列中,现将array[4]的值存入临时变量temp中,设置left、right指针,left指针从下标0开始向右移动,right指针从下标i-1开始向左移动,循环比较中值与待插入元素7的大小,从而确定array[i]的插入位置。

具体代码实现如下:

/**
 * 二分插入排序算法示例
 * @param array
 * @return
 */
public int[] BisectionInsertSort(int[] array) {
	int i, j, left, right, mid, temp;
	for (i = 1; i < array.length; i++) {
		temp = array[i];   //临时存储待插入元素array[i]
		left = 0;
		right = i - 1;  //设置查找段的起点和终点
		while (left <= right) { //二分定位
			mid = (left + right) / 2;
			if (temp < array[mid])
				right = mid - 1;
			else
				left = mid + 1;
		}
		for (j = i - 1; j >= left; j--)
			array[j + 1] = array[j]; //元素右移
		array[left] = temp;   //此时,left指针的位置就是array[i]要插入的位置,插入结束
	}
	return array;
}

(二)算法性能分析

       与直接插入排序算法相比,二分插入排序只是减少了元素的比较次数,但是元素的移动次数没有改变,所以时间复杂度的阶层不变,还是O(n^2)。但实际上算法运行的速度在同等条件下会快很多;算法的空间复杂度仍为O(1)。

【思考】

      前面提到的简单插入排序算法和二分插入排序算法都没有改变元素移动的次数,即每次都是比较一次、移动一次。是否可以增大元素移动的步幅呢?也就是说能不能比较一次,移动一大步呢?事实上是OK的,接下来我们要来分析更为高效算法——希尔排序。

 

四、希尔排序算法

(一)算法原理

      希尔排序又称增量排序,基本思路是先选取一个小于n的整数di(为增量),然后把排序表中的n个元素分为di个子表,从下标为0的元素开始,间隔为di的元素组成一个子表,然后在各个子表之间进行直接插入排序,使得子表有序,然后逐渐减少增量di重复上述操作,知道增量为1后再进行最后一次间隔为1的排序,最终完成整个序列有序。

      由于增量di选取的不同,形成的增量序列也不同,希尔排序算法的时间效率分析也是不同的。增量序列的选取一般没有严格规定,但结合实践操作经验来看,我们建议增量序列的选择遵循以下原则:

  • 增量序列的值必须是递减的,且最后一个增量d1必须是1;
  • 增量一般介于1与 \sqrt{n}之间

举个例子:

深入浅出理解排序算法系列(二) 插入排序算法的实现与性能分析_第6张图片

                                        (图片来源:http://www.cnblogs.com/chengxiao/p/6104371.html)

       结合上图不难看出,希尔排序的好处就是只要简单的几步骤,就可以实现元素位置跳跃式的改变,移动步幅加大,提高排序效率!

(二)算法实现方法

/**
 * 希尔排序算法
 * 
 * @param array
 * @return
 */
public int[] ShellSort(int[] array, int[] d) { //d[]为增量数组
	int i, j, temp;
	for (int k = 0; k < d.length; k++) { //增量数组中有几个增量元素,就循环几次
		int dk = d[k]; //根据增量,将array数组中的若干元素分为若干个子表
		for (i = dk; i < array.length; i++) {
			temp = array[i]; //①在每个子表中进行直接插入排序
			for (j = i - dk; j >= 0 && temp < array[j]; j -= dk) { 
				array[j + dk] = array[j]; //②与直接插入排序不同的是,每次移动的步幅为dk而非1
			}
			array[j + dk] = temp;  //③array[i]存储在j+dk位置上,完成插入排序
		}
	}
	return array;
}

【注意】如果大家仔细观察,会发现,当增量为1即dk=1,代码中①②③处的实现不就是直接 插入排序吗?这也证明了希尔排序和直接插入排序的相关联性,希尔排序是直接插入排序在移动步频上的升级版本!

(三)算法性能分析

    1、空间复杂度:仅用了一个辅助单元,空间复杂度为O(1);

    2、时间复杂度:前面讲到,分析希尔排序算法的时间复杂性是很困难的,因为和增量序列选择的有关。结合现有算法优化经验,我们推荐使用如下两种增量序列:

   3、算法稳定性:不稳定

 

五、总结

1、原始数据越接近有序,换句话说要处理的逆有序要少,排序的速度会越快。因此插入排序适用于对相对有序的序列进行排序。

2、在一般情况下,希尔排序算法更高效,编程的简单特点使它成为对适度地大量的输入数据经常选用的算法。

插入排序的时间复杂度如下:


 引用

     本文参考资料为《数据结构——Java语言描述》清华大学出版社以及中国大学MOOC《数据结构》(陈卫卫老师主讲)

请各位小伙伴不吝赐教,多多批评!

你可能感兴趣的:(算法,排序算法,直接插入排序,二分插入排序,希尔排序)