数据结构之排序(二)——插入排序(直接插入排序、折半插入排序、希尔排序)

插入排序

  • 基本思想:每步将一个待排序的对象,按其关键码大小,插入到前面已经排好序的一组对象的适当位置上,直到对象全部插入为止。
    边插入边排序的思想,保证子序列中随时都是排好序的。

  • 基本操作:有序插入

    • 在有序序列中插入一个元素,保持序列有序,有序长度不断增加。
    • 起初,a[0]是长度为1的子序列。然后,逐一将a[1]至a[n-1]插入到有序子序列中。
  • 有序插入方法

    • 在插入a[i]之前,数组a的前半段(a[0]到[i-1])是有序段后半段(a[i]到a[n-1])是停留于输入次序的“无序段”
    • 插入a[i]使a[0]~a[i-1]有序,也就是要为a[i]找到有序位置j( 0 ≤ j ≤ i 0\le j\le i 0ji),将a[i]插入到a[j]的位置上。
  • 插入位置

    • 插在中间
      在这里插入图片描述
    • 插在最前面
      在这里插入图片描述
    • 插在最后面
      在这里插入图片描述
  • 根据找插入位置方法的不同,把插入排序分为:

    • 顺序法定位插入位置:直接插入法
    • 二分法定位插入位置:二分插入排序
    • 缩小增量多遍插入排序:希尔排序


直接插入排序

  • 直接插入排序——采用顺序查找法查找插入位置。

  • 例子说明:如下图,绿色部分示已经排好序的序列,红色部分是待排序的序列。
    在这里插入图片描述
    首先,复制插入的元素,x=a[i],然后从第i-1个元素开始,从后往前查找,若当前元素a[j]比x大,则a[j]往后移一位,即a[j+1]=a[j],若当前元素a[j]不比x大,则说明找到插入位置了,此时插入位置为j+1,即a[j+1]=x。

  • 具体步骤

    • 复制插入元素
    x=a[i];
    
    • 记录后移,查找插入位置
    for (j = i - 1; j >= 0 && x < a[i]; j--)
    a[j + 1] = a[j];
    
    • 插入到正确位置
    a[j+1]=x;
    
  • 直接插入排序,使用哨兵
    在这里插入图片描述

    • 将待排序元素复制为哨兵
    L.r[0]=L.r[i];
    
    • 记录后移,查找插入位置
    for (j = i - 1; L.r[0].key < L.[j].key; --j)
    L.r[j + 1] = L.r[j];
    
    • 插入到正确位置
    L.r[j+1]=L.r[0];
    
  • 算法描述:

void InsertSort(SqList &L)
{
	int i, j;
	for (int i = 2; i <= L.length; ++i)
	{
		if (L.r[i].key < L.r[i - 1].key)        //若当前待排序元素值比前一个大,则无需改变其位置        
		{
			L.r[0] = L.r[i];                    //复制为哨兵
			for (int j = i - 1; L.r[0].key < L.r[j].key; --j)
			{
				L.r[j + 1] = L.r[j];            //记录后移
			}
			L.r[j + 1] = L.r[0];                //插入到正确位置
		}
	}
}
  • 性能分析
    实现排序的基本操作有两个:
    1.“比较”序列中两个关键字的大小;
    2.“移动”记录。
    • 最好的情况:关键字在记录序列中顺序有序。例如,序列如下,{11,25,32,47,56,70,81,85,92,96},
      • “比较”次数 ∑ i = 2 n 1 = n − 1 \sum\limits_{{\rm{i}} = 2}^n 1 = n - 1 i=2n1=n1
      • “移动”次数:0
    • 最坏的情况:关键字在记录序列中逆序有序。
      • “比较”次数 ∑ i = 2 n i = ( n + 2 ) ( n − 1 ) 2 \sum\limits_{{\rm{i}} = 2}^n i = \frac{{(n + 2)(n - 1)}}{2} i=2ni=2(n+2)(n1)
      • “移动”次数 ∑ i = 2 n ( i + 1 ) = ( n + 4 ) ( n − 1 ) 2 \sum\limits_{{\rm{i}} = 2}^n {(i + 1)} = \frac{{(n + 4)(n - 1)}}{2} i=2n(i+1)=2(n+4)(n1)
    • 平均情况
      • “比较”次数 ∑ i = 1 n − 1 ( i + 1 2 ) = ( n + 2 ) ( n − 1 ) 4 \sum\limits_{{\rm{i}} = 1}^{n - 1} {(\frac{{i + 1}}{2})} = \frac{{(n + 2)(n - 1)}}{4} i=1n1(2i+1)=4(n+2)(n1)
      • “移动”次数 ∑ i = 1 n − 1 ( i + 1 2 + 1 ) = ( n + 6 ) ( n − 1 ) 4 \sum\limits_{{\rm{i}} = 1}^{n - 1} {(\frac{{i + 1}}{2} + 1)} = \frac{{(n + 6)(n - 1)}}{4} i=1n1(2i+1+1)=4(n+6)(n1)
    • 时间复杂度
      • 原始数据越接近有序,排序速度越快
      • 最坏情况下(输入数据是逆有序的), T w ( n ) = O ( n 2 ) Tw(n)=O(n^2) Tw(n)=O(n2)
      • 平均情况下,耗时差不多是最坏情况的一半, T e ( n ) = O ( n 2 ) Te(n)=O(n^2) Te(n)=O(n2)
      • 要提高查找速度
        • 减少元素的比较次数
        • 减少元素的移动次数


折半插入排序
在这里插入图片描述
上图中,我们是在绿色部分寻找插入位置,而绿色部分是一个已完成排序的一个有序序列,所以在查找插入位置时,还可以考虑折半查找法

  • 查找插入位置时使用折半查找法
    在这里插入图片描述
  • 算法描述
void BInsertSort(SqList &L)
{
	int low, high,mid;
	for (int i = 2; i <= L.length; ++i)        //依次插入第2到第n个元素
	{
		L.r[0] = L.r[i];                       //当前插入元素存到哨兵位置
		low = 1;                               //采用折半查找法查找插入位置
		high = i - 1;
		while (low<=high)
		{
			mid = (low + high) / 2;
			if (L.r[0].key < L.r[mid].key)
				high = mid - 1;
			else
				low = mid + 1;
		}                                     //循环结束,high+1为插入位置
		for (int j = i - 1; j >= high + 1; --j) //移动元素
			L.r[j + 1] = L.r[j];
		L.r[high + 1] = L.r[0];               //插入到正确位置
	}
}
  • 性能分析
    • 折半查找比顺序查找快,所以折半插入排序就平均性能来说比直接插入排序要快。
    • 他所需要的关键码的比较次数与待排序对象序列的初始排序无关,仅依赖于对象个数。在插入第i个对象时,需要经过 ⌊ log ⁡ 2 i ⌋ + 1 \left\lfloor {\log _2^i} \right\rfloor {\rm{ + 1}} log2i+1次关键码比较,才能确定它应插入的位置。
      • 当n很大时,总关键码的比较次数要比直接插入排序的最坏情况要好很多,但比其最坏情况要差。
      • 在对象的初始排列已经按关键码排好序或接近有序时,直接插入排序比折半插入排序执行的关键码的比较次数要少。
    • 折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列
      • 减少了比较次数,但没有减少移动次数
      • 平均性能优于直接插入排序
    • 时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1),是一种稳定的排序方法。


希尔排序

  • 希尔排序思想的出发点:

    • 1.相比直接插入排序,折半插入排序减少了比较的次数,但没有减少移动次数。所以,在这里,能不能比较一次,就移动一大步,而不是移动一步
    • 2.直接插入排序在原序列基本有序时,以及待排序的记录个数较少时,效率较高。
  • 基本思想:先将整个待排序序列分割成若干个子序列,分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
    即,

    • 缩小增量
    • 多遍插入排序
  • 例子
    在这里插入图片描述

  • 思路

    • 1.定义增量序列 D k : D M &gt; D M − 1 &gt; ⋯ &gt; D 1 = 1 D_k:D_M&gt;D_{M-1}&gt;\cdots&gt;D_1=1 Dk:DM>DM1>>D1=1
      在刚才的例子中: D 3 = 5 , D 2 = 3 , D 1 = 1 D_3=5,D_2=3,D_1=1 D3=5,D2=3,D1=1
    • 2.对每个 D k D_k Dk进行“ D k D_k Dk-间隔” 插入排序( k = M , M − 1 , ⋯ &ThinSpace; , 1 k=M,M-1,\cdots,1 k=M,M1,,1)
  • 特点

    • 一次移动,移动位置较大,跳跃式地接近排序后的最终位置
    • 最后一次只需要少量移动
    • 增量序列必须是递减的,最后一个必须是1
    • 增量序列应该是互质的
  • 算法描述

void ShellSort(SqList &L, int dlta[], int t)// dlta为增量数组
{
	for (int k = 0; k < t; ++k)
		ShellInsert(L, dlta[k]);   //一次增量为dlta[k]的插入排序
}

void ShellInsert(SqList &L, int dk)
{
	for (int i = dk + 1; i <= L.length; ++i)
	{
		if (L.r[i].key < L.r[i - dk].key)
		{
			L.r[0] = L.r[i];
			for (int j = i - dk; j > 0 && (L.r[0].key < L.r[j].key); j = j - dk)
				L.r[j + dk] = L.r[j];
			L.r[j + dk] = L.r[0];
		}
	}
}
  • 性能分析
    • 时间复杂度与增量序列取值有关
      • Hibbard增量序列
        • D k = 2 k − 1 D_k=2^{k-1} Dk=2k1——相邻元素互质
        • 最坏情况: T w o r s t = O ( n 3 / 2 ) T_{worst}=O(n^{3/2}) Tworst=O(n3/2)
        • 猜想(为证明): T a v g = O ( n 5 / 4 ) T_{avg}=O(n^{5/4}) Tavg=O(n5/4)
      • Sedgewick增量序列
        • {1,5,19,41,109,…}—— 9 ∗ 4 i − 9 ∗ 2 i + 1 或 4 i − 3 ∗ 2 i + 1 9*4^i-9*2^i+1或4^i-3*2^i+1 94i92i+14i32i+1
        • 猜想(为证明): T a v g = O ( n 7 / 6 ) &ThickSpace;&ThickSpace;&ThickSpace;&ThickSpace; T w o r s t = O ( n 4 / 3 ) T_{avg}=O(n^{7/6})\;\;\;\;T_{worst}=O(n^{4/3}) Tavg=O(n7/6)Tworst=O(n4/3)
    • 希尔排序是一种不稳定的排序算法
      在这里插入图片描述
    • 时间复杂度是n和d的函数:
      O ( n 1.25 ) 到 O ( 1.6 n 1.25 ) — — 经 验 公 式 O(n^{1.25})到O(1.6n^{1.25})——经验公式 O(n1.25)O(1.6n1.25)
      空间复杂度为 O ( 1 ) O(1) O(1)
      是一种不稳定的排序方法·
    • 如何选择最佳的d序列,目前尚未解决。
      最后一个增量必须为1,无除了1之外的公因子。
      不宜在链式存储结构上实现。
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值