数据结构——排序(十种常用内排序算法)

排序是计算即程序设计中的重要操作,对于一些操作序列排序后可以提高其算法效率。下面介绍几种典型的、常用的排序方法。

基础概念和排序方法概述

  1. 基础概念
  1. 排序(Sorting)
    指按关键字的非递减顺序对一组记录进行排列的操作。

  2. 排序的稳定性
    当一组 n 个任意无序排列记录的关键字都不相同,那么对于排序结果的序列唯一;相反,若其中序列存在几个记录的关键字相同,则排序所得的结果不唯一。对于关键字相同的几个记录,若排序前这几个记录的排列先后顺序和排序后的先后顺序一致,那么这个排序算法就是稳定的,相反,则不稳定。对于稳定和不稳定的排序算法各有各的适用场所。

  3. 内部排序和外部排序
    由于待排序记录的数量不同,使得排序过程中数据所占用的存储设备会有所不同,可以将排序方法分为两大类:内部排序,指的时待排序记录全部存放在计算机内存中进行排序;外部排序,指的是待排序记录量较大内存无法一次性容纳全部,在排序过程中需要对外存进行访问。

  1. 内部排序方法的分类

内部排序的过程是一个逐步扩大记录的有序序列长度的过程。在排序过程中可以将排序记录区分为两个区域:有序序列区和无序序列区。
根据逐步扩大记录有序序列长度的原则不同,可以将内部排序分为以下几类:

①插入类:将无序序列中的一个或几个记录插入到有序序列中,从而增加记录的有序序列长度。主要包含:直接插入排序、折半插入排序和希尔排序。
②交换类:通过 ‘交换’ 无序序列中的记录从而得到其中关键字最小或最大的记录,加入有序序列中,以此增加有序序列长度。主要包含:冒泡排序和快速排序。
③选择类:从记录的无序序列中 ‘选择’ 关键字最小或者最大的记录,加入到有序序列,以此增加有序序列长度。主要包含:简单选择排序、树形选择排序和堆排序。
④归并类:通过归并两个或两个以上的记录有序子序列,逐步增加有序序列长度。主要包含:2-路归并排序时其中最重要的
⑤分配类:唯一一类不需要进行关键字比较的排序方法,排序时主要利用分配和收集两种基本操作来完成。基数排序时其中最重要的。

  1. 待排序记录的存储方式

①顺序表:记录之间的次序关系由其存储位置决定,实现排序需要移动记录。
②链表:记录之间的次序由指针决定,实现排序的时候需要修改指针。被称为链表排序
③地址排序:记录本身存储在一组地址连续的存储单元中,同时设定一个指示各个记录存储位置的地址向量,排序过程中移动地址向量中这些记录的地址,排序结束后再按照地址向量中的值调整记录的存储位置。

  1. 排序算法效率的评价指标

①执行时间:排序操作,在顺序存储中,时间主要消耗在关键字之间的比较和记录的移动上。
②辅助空间:空间复杂度由算法除了存储记录所占的空间外所需的辅助空间决定。

以下排序方法的待排序记录的存储方式皆为顺序表存储,有序均为递增序列。定义如下:

#define M 20
typedef int Keytype;
typedef struct {
   
	Keytype K;
}Rtype;
typedef struct {
   
	Rtype r[M + 1];
	int length;
}SqList;                    //顺序表类型

插入排序

思想:每一趟将一个待排序的记录,按照其关键字的大小插入到已经有序的一组记录的适当位置,直到全部记录插入为止。

直接插入排序

  1. 基本操作为:将一条记录插入到已经排好序的有序表中,从而得到一个新的、记录数量增 1 的有序表。
  2. 算法步骤:
    ①待排序的记录存储在数组 r[1…n] 中,初始时 r [1] 为一个有序序列;
    ②将 r [i] (1 <= i <= n)在有序序列 r [1…i-1] 中查找到位置并插入,直到将r [n] 在有序序列 r [1…n-1] 插入形成一个表长为 n 的有序序列.
    ③查找位置:将待插入记录的关键字和有序序列的关键字逐一比较,直到比待插入位置的前后记录一大一小,然后将待插入位置的后面记录全部后移一位。

假如现在已经有十个人按照从低到高的顺序排好队伍,现在新来一个来排队的。最简单的就是从最高的一个个比较,直到比后面低,比前面高。
从最高的开始比较,直到比后面一个低,比前一个高。将后面比他高的全部后退一个人的位置,新来的人插入到这个位置。(将已经排好队的个数减到 1 个,就是从初始开始的情况。)

书上实例:
在这里插入图片描述

代码

///直接插入排序
void Insert_sort(SqList &L) {
   
	int n = L.length;
	int j = 0;

	//假设有序序列为 r[1],从i=2开始不断插入
	for (int i = 2; i <= n; ++i) {
     
		//如果待插入关键字比有序序列的最后一个(最大值)大,无需进行任何操作
		if (L.r[i].K < L.r[i - 1].K) {
     
		//否则,则逐一从后往前比较直到哨兵,并将比他大的后移一位。
			L.r[0].K = L.r[i].K;     //将待插入关键字放到r[0],设置为哨兵

			L.r[i].K = L.r[i - 1].K; //将if里面比较过的结果直接使用,下面for循环少比较一次。
			
			//逐一与记录比较,比待插入关键字比记录关键字大则将记录后移一位,直到哨兵为止。(哨兵的作用所在)
			for (j = i - 2; L.r[0].K < L.r[j].K; --j)
				L.r[j + 1].K = L.r[j].K;

			//找到插入位置,将关键字插入。
			L.r[j + 1].K = L.r[0].K;
		}
	}
}

算法分析

  1. 时间复杂度

从算法来看,排序基本操作:比较关键字大小和移动记录
最优情况:序列本身就为递增序列,只进行从 i =2 开始的每一次与前一个比较。则最后比较次数达最小值 n-1,移动次数为0.
最劣情况:序列本身是递减序列。每一个记录插入时,比较了 i 次(与前面 i-1 有序序列的分别比较一次和跟哨兵比较一次),移动了 i+1(有序序列的 i -1 次加上将待插入记录移动哨兵 r[ 0 ],和最后插入时将哨兵内的记录移动至插入位置)。总的比较次数为KCN,总的记录移动次数RMN达到最大值。
在这里插入图片描述
由于排序序列中出现各种可能性的概率相同,取最好和最坏情况的平均值约为n²/4。则时间复杂度为 O(n²)。

  1. 空间复杂度

直接插入排序只需要一个记录的辅助空间 r [0],所以空间复杂度为O(1).

  1. 算法特点

①稳定排序
②算法简单,容易实现
③也适用于链式存储,需要修改单链表的指针
④更适合于初始记录基本有序(正序)的情况,由时间复杂度可以直到,当初始记录无序,n 较大时复杂度高。

折半插入排序

  1. 基本操作为:将一条记录插入到已经排好序的有序表中,从而得到一个新的、记录数量增 1 的有序表。前述直接插入排序查找位置用的顺序查找法,而使用折半查找来实现就是折半插入排序。
  2. 算法步骤:
    ①待排序的记录存储在数组 r[1…n] 中,初始时 r [1] 为一个有序序列;
    ②将 r [i] (1 <= i <= n)在有序序列 r [1…i-1] 中查找到位置并插入,直到将r [n] 在有序序列 r [1…n-1] 插入形成一个表长为 n 的有序序列.
    ③查找位置:将待插入记录的关键字和有序序列的关键字进行折半比较,直到比待插入位置的前后记录一大一小,然后将待插入位置的后面记录全部后移一位。
    ④折半比较:插入 r [i] 时,在有序序列中 r [1…i-1],令low = 1,high = i-1,两个的中间值 mid = (1+i-1)/2。 由中间记录r [mid] 分为r [low…mid-1] 上半区和 r [mid+1…high] 下半区,根据插入记录 r[i] 与中间记录 r [mid] 的比较结果。若比中间记录大,分到下半区;反之上半区。再对分到的半区进行以上重复操作,直到 low 大于 high 。

假如现在已经有十个人按照从低到高的顺序排好队伍,现在新来一个来排队的。新来的看起来不高不矮,从低或从高开始逐一比较太麻烦,从中间一个比较,若比中间高,则减少对最低的到中间的这一半的比较量。
与第一个到第十个的中间第五个比较;若比第五个高,再和第六个到第十个的中间第八个比较;若比第八个低,再和第六个和第八个的中间第七个比较;比第七个低,则再和第六个和第六个的中间第六个比较;若比第六个高,则插入到第六个和第七个中间。将后面比他高的全部后退一个人的位置,新来的人插入到这个位置。(将已经排好队的个数减到 1 个,就是从初始开始的情况。)

代码

///折半插入排序
void Binsert_sort(SqList& L) {
   
	printf("\n折半插入排序....\n");
	int n = L.length;
	int j = 0, high, low, mid;

	//假设有序序列为 r[1],从i=2开始不断插入
	for (int i = 2; i <= n; ++i) {
   
		L.r[0] = L.r[i];  //借用 r[0] 临时存储待插入元素
		low = 1;          //初始化半区边界
		high = i - 1;
		while (low <= high) {
   
			mid = (low + high) / 2;
			//和中间记录比较,决定分区
			if (L.r[i].K < L.r[mid].K) high = mid - 1;
			else
				low = mid + 1;
		}
		//上循环结束的条件为low>high,由于是与mid对应记录的比较然后再修改high,low
		//最后循环结束的条件必然是mid=low=high,待插入记录小于r[mid],则应该插入到此时
		//的r[high],而因为high=mid-1也就是high-1结束了循环,所以后序需要将high+1全部
		//后移并将待插入记录插入r[high+1]
		for (j = i - 1; j >= high + 1; --j)
			L.r[j + 1] = L.r[j];
		L.r[high + 1] = L.r[0];
	}
}

算法分析

  1. 时间复杂度

折半插入排序所进行的关键字比较次数和待排序序列的原始排列无关,仅依赖于记录的个数。折半查找就类似一颗二叉树,比根小分在根的左子树,以此类推。折半查找的比较次数和树的深度有关,那么比较次数为 [ ㏒₂i ]+1。而折半插入排序和直接插入排序记录移动的次数是相同的,依赖于初始序列。
在平均情况下,折半比较次数低于直接插入,但是记录的移动次数不变。所以折半插入排序的时间复杂度仍为 O(n²)

  1. 空间复杂度

只需要一个辅助空间 r[0],空间复杂度为 O(1)

  1. 算法特点

①稳定排序
②只能用于顺序存储结构
③适合记录无序,n较大的时候

希尔排序

  1. 基本操作为:总结可知直接插入排序,当记录个数较少且待排序序列关键字基本有序时,效率较高。希尔排序从“减少排序个数”和“序列基本有序”进行改进。希尔排序的实质是采用分组排序,先将序列分割为几组,从而减少待排序数据量;然后再分割为数据量比前一个多的几组,重复操作。这样几次分组操作后,就可以达到序列基本有序,再对全体记录进行一次直接插入排序。
  2. 算法步骤:
    希尔的分组不是将记录分段,而是间隔一个增量,将这些记录分成一组。
    ①待排序的记录存储在数组 r[1…n] ,一个增量序列存储在数组 d[n] 中(d[i] > d[i+1] & d[ n ] = 1)
    ②取 d[ i ] (1<i<n) 把全部记录分组,间隔 d[i] 的记录分在一组,各个组中进行直接插入排序
    ④最后取 d[ n ] = 1,也就是直接插入排序

假如现在有十个身高不一的人乱序排着队伍。按着一个个插入的方式太慢了,所以现在间隔 i 个会将十个人分为 i+1 个组,组内进行排序。颇有局部解决达到总体最优的味道。这样快速也可以避免假设最高的在最前面,最低的在最后面,如果按照直接插入或折半排序的话会移动九个人的位置,而如果这俩通过增量分组恰好分在一组,则只需要进行少量的移动。

书上实例:
在这里插入图片描述

代码

///希尔排序
int d[3] = {
    3,2,1 };             //增量序列
void Shell_sortd(SqList& L,int d) {
   
	int n = L.length;
	int j = 0;

	//假设分组序列为 r[1],r[1+d],r[1+2d]...,进行直接插入排序
	for (int i = d+1; i <= n; ++i) {
   
		//如果待插入关键字比有序序列的最后一个(最大值)大,无需进行任何操作
		if (L.r[i].K < L.r[i - d].K) {
   
			L.r[0].K = L.r[i].K;     //暂存待插入值


			
  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值