数据结构复习指导之插入排序(直接插入排序、折半插入排序、希尔排序)

文章目录

排序

考纲内容

知识框架

复习提示

1.排序的基本概念

1.1排序的定义

2.插入排序

2.1直接插入排序

2.2折半插入排序

2.3希尔排序

知识回顾


排序

考纲内容

(一)排序的基本概念

(二)插入排序

直接插入排序;折半插入排序;希尔排序(shell sort)

(三)交换排序

冒泡排序(bubble sort);快速排序

(四)选择排序

简单选择排序;堆排序

(五)二路归并排序(merge sort)

(六)基数排序

(七)外部排序

(八)排序算法的分析和应用

知识框架

复习提示

堆排序、快速排序和归并排序是本章的重难点。读者应深入掌握各种排序算法的思想、排序过程(能动手模拟)和特征(初态的影响、复杂度、稳定性、适用性等),通常以选择题的形式考查不同算法之间的对比。此外,对于一些常用排序算法的关键代码,要达到熟练编写的程度:看到某特定序列,读者应具有选择最优排序算法(根据排序算法特征)的能力。

1.排序的基本概念

1.1排序的定义

排序,就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。

为了查找方便,通常希望计算机中的表是按关键字有序的。排序的确切定义如下:

输入:n个记录 R₁ ,R₂,...,Rₙ,对应的关键字为k₁, k₂,...,kₙ

输出:输入序列的一个重排R₁' ,R₂',...,Rₙ',使得 k₁'≤k'₂≤...≤kₙ',(其中"≤"可以换成其他的比较大小的符号)。

算法的稳定性 。若待排序表中有两个元素Rᵢ 和Rⱼ ,其对应的关键字相同,即 keyᵢ =keyⱼ ,且在排序前 Rᵢ 在Rⱼ 的前面,若使用某一排序算法排序后,Rᵢ仍然在Rⱼ 的前面,则称这个排序算法是稳定的,否则称这个排序算法是不稳定的

需要注意的是,算法是否具有稳定性并不能衡量一个算法的优劣它主要是对算法的性质进行描述

若待排序表中的关键字不允许重复,排序结果是唯一的,则对于排序算法的选择,稳定与否无关紧要。

注意:对于不稳定的排序算法,只需举出一组关键字的实例,说明它的不稳定性即可

在排序过程中,根据数据元素是否完全存放在内存中,可将排序算法分为两类:

  • 内部排序,是指在排序期间元素全部存放在内存中的排序;
  • 外部排序,是指在排序期间元素无法全部同时存放在内存中,必须在排序的过程中根据要求不断地在内、外存之间移动的排序。

一般情况下,内部排序算法在执行过程中都要进行两种操作:比较和移动

通过比较两个关键字的大小,确定对应元素的前后关系,然后通过移动元素以达到有序。

当然,并非所有的内部排序算法都要基于比较操作,事实上,基数排序就不基于比较操作。

每种排序算法都有各自的优缺点,适合在不同的环境下使用,就其全面性能而言,很难提出一种被认为是最好的算法。

通常可以将排序算法分为插入排序、交换排序、选择排序、归并排序基数排序五大类,后面几节会分别进行详细介绍。

内部排序算法的性能取决于算法的时间复杂度和空间复杂度,而时间复杂度一般是由比较和移动的次数决定的。

注意:大多数的内部排序算法都更适用于顺序存储的线性表

2.插入排序

插入排序是一种简单直观的排序算法,其基本思想是每次将一个待排序的记录按其关键字大小插入前面已排好序的子序列,直到全部记录插入完成。

由插入排序的思想可以引申出三个重要的排序算法:直接插入排序、折半插入排序和希尔排序

2.1直接插入排序

【在本章中,凡是没有特殊注明的,通常默认排序结果为非递减有序序列。】

根据上面的插入排序思想,不难得出一种最简单也最直观的直接插入排序算法。

假设在排序过程中,待排序表 L[1..n]在某次排序过程中的某一时刻状态如下:

要将元素 L(i) 插入已有序的子序列 L[1...i-1],需要执行以下操作(为避免混淆,下面用 L[ ] 表示一个表,而用 L() 表示一个元素):

  • 1)査找出 L(i) 在 L[1..i-1] 中的插入位置 k。
  • 2)将 L[k..i-1]中的所有元素依次后移一个位置。
  • 3)将 L(i) 复制到 L(k)。

为了实现对 L[1..n] 的排序,可以将 L(2)~L(n) 依次插入前面已排好序的子序列,初始 L[1]可以视为一个已排好序的子序列。

上述操作执行n-1次就能得到一个有序的表。

插入排序在实现上通常采用原地排序(空间复杂度为 O(1)),因而在从后往前的比较过程中,需要反复把已排序元素逐步向后挪位,为新元素提供插入空间。

下面是直接插入排序的代码,其中再次用到了前面提到的"哨兵"(作用相同)。

cpp 复制代码
void InsertSort(ElemType A[],int n){
    int i j;
    for(i=2;i<=n;i++)        //依次将A[2]~A[n]插入前面已排序序列
        if(A[i]<A[i-1]){     //若A[i]关键码小于其前驱,将A[i]插入有序表
        A[0]=A[i];           //复制为哨兵,A[0]不存放元素
        for(j=i-1;A[0]<A[j];--j) //从后往前查找待插入位置
            A[j+1]=A[j];     //向后挪位
        A[j+1]=A[0];         //复制到插入位置
    }
}

假定初始序列为 49,38,65,97,76,13,27,,初始时 49 可以视为一个已排好序的子序列,

按照上述算法进行直接插入排序的过程如图8.1所示,括号内是已排好序的子序列。

直接插入排序算法的性能分析如下:

空间效率 :仅使用了常数个辅助单元,因而空间复杂度为O(1)


时间效率:在排序过程中,向有序子表中逐个地插入元素的操作进行了n-1趟,

  • 每趟操作都分为比较关键字和移动元素,而比较次数和移动次数取决于待排序表的初始状态。
  • 在最好情况下,表中元素已经有序,此时每插入一个元素,都只需比较一次而不用移动元素,因而时间复杂度为 O(n)。
  • 在最坏情况下,表中元素顺序刚好与排序结果中的元素顺序相反(逆序),总的比较次数达到最大,总的移动次数也达到最大,总的时间复杂度为O(n²)。
  • 平均情况下,考虑待排序表中元素是随机的,此时可以取上述最好与最坏情况的平均值作为平均情况下的时间复杂度,总的比较次数与总的移动次数均约为n²/4。
  • 因此,直接插入排序算法的时间复杂度为 O(n²)

稳定性:因为每次插入元素时总是从后往前先比较再移动,所以不会出现相同元素相对位置发生变化的情况,即直接插入排序是一个稳定的排序算法。


适用性直接插入排序适用于顺序存储和链式存储的线性表,采用链式存储时无须移动元素

2.2折半插入排序

从直接插入排序算法中,不难看出每趟插入的过程中都进行了两项工作:

①从前面的有序子表中查找出待插入元素应该被插入的位置;

②给插入位置腾出空间,将待插入元素复制到表中的插入位置。

注意到在该算法中,总是边比较边移动元素。

下面将比较和移动操作分离,即先折半查找出元素的待插入位置,然后统一地移动待插入位置之后的所有元素

当排序表为顺序表时,可以对直接插入排序算法做如下改进:因为是顺序存储的线性表,所以查找有序子表时可以用折半查找来实现。

确定待插入位置后,就可统一地向后移动元素。算法代码如下:

cpp 复制代码
void InsertSort(ElemType A[],int n)
    int i j,low,high,mid;
    for(i=2;i<=n;i++){         //依次将A[2]~A[n]插入前面的已排序序列
        A[0]=A[i];             //将 A[i]暂存到 A[0]
        low=1;high=i-1;        //设置折半查找的范围
        while(low<=high){      //折半查找(默认递增有序)
            mid=(low+high)/2;  //取中间点
            if(A[mid]>A[0]) high=mid-1;   //查找左半子表    
            else low=mid+1;    //查找右半子表
        }
        for(j=i-1;j>=high+1;--j)
            A[j+1]=A[j];       //统一后移元素,空出插入位置
        A[high+1]=A[0];        //插入操作
    }
}

命题追踪 ------直接插入排序和折半插入排序的比较(2012)】

从上述算法中,不难看出折半插入排序仅减少了比较元素的次数,时间复杂度约为 O(nlog₂n),

该比较次数与待排序表的初始状态无关,仅取决于表中的元素个数n;

而元素的移动次数并未改变它依赖于待排序表的初始状态。

因此,折半插入排序的时间复杂度仍为 O(n²),但对于数据量不很大的排序表,折半插入排序往往能表现出很好的性能。

折半插入排序是一种稳定的排序算法。

折半插入排序仅适用于顺序存储的线性表

2.3希尔排序

从前面的分析可知,直接插入排序算法的时间复杂度为O(n²),但若待排序列为"正序 "时,其时间效率可提高至 O(n),由此可见它更适用于基本有序的排序表和数据量不大的排序表

希尔排序正是基于这两点分析对直接插入排序进行改进而得来的,又称缩小增量排序

命题追踪 ------希尔排序中各子序列采用的排序算法(2015)】

希尔排序的基本思想是:先将待排序表分割成若干形如 L[i,i+d,i+ 2d....,i+kd]的"特殊"子表,即把相隔某个"增量"的记录组成一个子表,对各个子表分别进行直接插入排序,当整个表中的元素已呈"基本有序"时,再对全体记录进行一次直接插入排序。

命题追踪 ------根据希尔排序的中间过程判断所采用的增量(2014、2018)】

希尔排序的过程如下

先取一个小于n的增量d₁,把表中的全部记录分成d₁组,所有距离为d₁的倍数的记录放在同一组,在各组内进行直接插入排序;

然后取第二个增量d₂ <d₁,重复上述过程,直到所取到的 dₜ=1,即所有记录已放在同一组中,再进行直接插入排序,由于此时已经具有较好的局部有序性,因此可以很快得到最终结果。

到目前为止,尚未求得一个最好的增量序列。

仍以2.1节的关键字为例,假定第一趟取增量d₁=5,将该序列分成5个子序列,即图中第2行至第6行,分别对各子序列进行直接插入排序,结果如第7行所示;

假定第二趟取增量 d₂=3,分别对三个子序列进行直接插入排序,结果如第11行所示;

最后对整个序列进行一趟直接插入排序,整个排序过程如图 8.2 所示。

希尔排序算法的代码如下:

cpp 复制代码
void ShellSort(ElemType A[],int n){
//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
    int dk,i,j;
    for(dk=n/2;dk>=1;dk=dk/2)        //增量变化(无统一规定)
        for(i=dk+1;i<=n;++i)
            if(A[i]<A[i-dk]){        //需将A[]插入有序增量子表
                A[0]=A[i];           //暂存在 A[0]
                for(j=i-dk;j>0 && A[0]<A[j];j-=dk)
                    A[j+dk]=A[j];    //记录后移,查找插入的位置
                A[j+dk]=A[0];        //插入
            }
}

希尔排序算法的性能分析如下:

空间效率: 仅使用了常数个辅助单元,因而空间复杂度为O(1)


时间效率:因为希尔排序的时间复杂度依赖于增量序列的函数,这涉及数学上尚未解决的难题,所以其时间复杂度分析比较困难。

  • 当n在某个特定范围时,希尔排序的时间复杂度约为
  • 在最坏情况下希尔排序的时间复杂度为O(n²)。

稳定性:当相同关键字的记录被划分到不同的子表时,可能会改变它们之间的相对次序,

因此希尔排序是一种不稳定的排序算法。例如,图82中49与49的相对次序已发生了变化。


适用性:希尔排序仅适用于顺序存储的线性表。

知识回顾

相关推荐
XiaoLeisj27 分钟前
【递归,搜索与回溯算法 & 综合练习】深入理解暴搜决策树:递归,搜索与回溯算法综合小专题(二)
数据结构·算法·leetcode·决策树·深度优先·剪枝
Jasmine_llq1 小时前
《 火星人 》
算法·青少年编程·c#
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
Lenyiin1 小时前
01.02、判定是否互为字符重排
算法·leetcode
鸽鸽程序猿2 小时前
【算法】【优选算法】宽搜(BFS)中队列的使用
算法·宽度优先·队列
Jackey_Song_Odd2 小时前
C语言 单向链表反转问题
c语言·数据结构·算法·链表
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
乐之者v2 小时前
leetCode43.字符串相乘
java·数据结构·算法
A懿轩A3 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组