九、排序算法

不稳定排序

一句话记忆:“考研复习痛苦啊,情绪不稳定快些选堆好友来聊天吧”。
这里,“快”指快速排序,“些”指希尔排序, “选 ”指简单选择排序,“堆”指堆排序, 这4种
是不稳定的,其他自然都是稳定的。

时间复杂度

O(nlogn)
快速排序、堆排序

辅助空间

O(1)
直接插入排序
堆排序
快速排序
O(n)
归并排序

数据结构研究的顺序

首先了解逻辑结构,然后为这种逻辑结构设计一种存储结构,最后在这种存储结构上设计一些操作

知识框架

在这里插入图片描述

(一)排序的基本概念

排序(sort)

sort:排序、分类

稳定性

排序算法的分类

(二)插入排序

1.直接插入排序

  1. 算法介绍
    待排序表L[1…n]在某次排序过程中的某一时刻状态如下:
    在这里插入图片描述

  2. 算法代码

void insertSort(int A[],int n)//待排关键字存储在arr[]中,默认为整型,个数为n
{
	int temp,i,j;
	/*【第一层循环】挑选出关键字*/
	for(i=l;i<n;++i)//循环从1开始是因为:第一个关键字下标为0,把它当成一个有序序列
	{
		temp=A[i]; //将待插入关键字暂存于temp中
		j=i-1;//i是无序序列中第一个元素,-1是无序序列左边的元素,j是有序序列最右边元素,从左往右扫描有序序列,所以从i-1开始
		/*【第二层循环】完成了从待排关键字之前的关键字开始从左往右扫描,如果大于待排关键字,则后移一位*/
		while(j>=0 && temp<A[j])//j>=0:从左往右扫描,有序序列关键字从0开始;temp<arr[j]:当temp>arr时停止反应在循环中就是temp<arr[j]时扫描继续进行;&&:循环条件同时满足-且
		{
			A[j+1]=A[j];//会覆盖
			--j; 
		}
		A[j+1]=temp;//找到插入位置,将temp中暂存的待排关键字插入;j+1因为,指示箭头j总是指在待插入位置前一个位置,j+1为要插入位置
		}
	}
}

++i:先将i的值加1,然后使用新的值来计算表达式
a=++i:①i=i+1;②a=i【先自增,再赋值】

for循环
for(表达式1循环变量赋初值;表达式2循环条件表达式;表达式3循环变量调整)
循环体
while循环
while(循环条件表达式)
循环体
简记:先判断循环表达式,若为真,再执行循环体,若为假,程序不会执行循环体

2.折半插入排序

3.希尔排序(shell sort)缩小增量排序

(三) 交换类的排序

交换类排序的核心是“交换”,即每一趟排序,都通过一系列的“交换”动作,让一个关键字排到它最终的位置上。
设想军训刚开始,一群学生要排队,教官说:“你比你旁边的高,你俩换一下。怎么换完还比下一个高?继续换……”最后这个同学将被换到最终位置。这就是“交换”类的排序。
属于交换类排序的有
冒泡排序(刚才排队的例子)、快速排序。

1.冒泡排序(bubble sort)

  1. 算法介绍
    起泡排序又称冒泡排序。它是通过一系列的“交换”动作完成的。首先第一个关键字和第二个关键字比较,如果第一个大,则二者交换,否则不交换;然后第二个关键字和第三个关键字比较,如果第二个大,则二者交换,否则不交换…。一直按这种方式进行下去,最终最大的那个关键字被交换到了最后,一趟起泡排序完成。经过多趟这样的排序,最终使整个序列有序。这个过程中,大的关键字像石头一样“沉底”,小的关键字像气泡一样逐渐向上“浮动”,起泡排序的名字由此而来。
  2. 算法流程
    在这里插入图片描述
  3. 算法代码
void bubleSort(int arr[]int n)
{
	int i,j,flag;
	int temp;
	/*【第一层循环】指示当前无序序列的范围,即i从n-1到1*/
	for(i=n-1;i>=1;--i)
	{
		flag=0;变量flag用来标记本趟排序是否发生了交换
		/*【第二层循环】用j扫描当前无序序列(当前i所指示范围的无序序列);j=1开始,不从0开始是因为如果j所指当前关键字比其前面的关键字小的话就需要交换,从0开始没有意义,0前面没有关键字*/
		for(j=1;j<=i;++j)
			if(arr[j-1]>arr[j]//如果j所指当前关键字比其前面的关键字比小
			{
				/*交换代码*/
				temp=arr[j];
				arr[j]=arr[j-1];
				arr[j-1]=temp;
				flag=1;//如果没发生交换,则flag的值为0;如果发生交换,则flag的值改为1
			}
		if(flag==0//一趟排序过程中没有发生关键字交换,则证明序列
			return;//flag==0时说明有序,排序结束
	}
}

2.快速排序(划分交换排序)

基本思想
分治法
快速排序(QuickSort)是对起泡排序的一种改进。
它的基本思想是,通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

操作

  1. 初始化标记low为划分部分第一个元素的位置,high为最后一个元素的位置,
  2. 然后不断地移动两标记并交换元素:
    如何交换元素
  1. high向前移动找到第一个比pivot小的元素
  2. low向后移动找到第一个比pivot大的元素
    3)交换当前两个位置的元素
    4)继续移动标记,执行1),2),3)的过程,直到low大于等于high为止。
    在这里插入图片描述在这里插入图片描述

代码:

/*一趟快速排序*/
int Partition (** #, int low, int high) 
//数组ElemType A[]
//链表SqList &L,之后L.A[]
{
	pivot = A[low];//枢轴记录关键字
	while (low<high) //从表的两端交替地向中间扫描
	{
		while (low<high && A[high]>=pivot)			--high ;
					A[low]=A[high] ;           //将比枢轴关键字小的记录移到低端
		while (low<high && A[low]<=pivot)			++low ;
					A[high]=A[low] ;           //将比枢轴记录大的记录移到高端
}
A[low]=pivot;
return low ;
}
/*完整的快速排序算法通过递归函数实现*/
void QuickSort (ElemType A[]int low, int high)
{
	if(low < high) 
	{
	int p = Partition(A, low, high) ;
	QuickSort (A, low, p-1) ;
	QuickSort (A, p+1, high) ;
	}
}
void QuickSort (int R[], int low, int high)
//对从R[1ow]到R[high]的关键字进行排序
{
	int temp;
	int i=low, j=high;
	if (low<high)
	{
		temp=R[low];
/*下面这个循环完成了一趟排序,即将数组中小于temp的关键字放在左边,大于temp的关键字放在右边*/
		while(i<j)
		{
			while(j>i&&R[j]>=temp) --j;//从右往左扫描, 找到一个小于temp的关键字
			if(i<j)
			{	
				R[i]=R[j] ;//放在temp左边
				++i ;//i右移一位
			}
			while(i<j&&R[i]<temp) ++i; //从左往右扫描,找到一//个大于temp的关键字
			if (i<j)
			{
				R[j]=R[i];//放在temp右边
				--j ;//j左移一位
			}
		}
		R[i]=temp ;//将temp放在最终位置
		QuickSort (R, low, i-1) ;//递归地对temp左边的关键字进行排序
		QuickSort (R, i+1,high) ;//递归地对temp右边的关键字进行排序
	}
}

(四)选择类的排序

选择类排序的核心是“选择”,即每一趟排序都选出一个最小(或最大)的关键字,把它和序列中的第一个(或最后一个)关键字交换,这样最小(或最大)的关键字到位。
军训排队,教官说:“你们都站着别动,我看谁个子最小。”然后教官选出个子最小的同学,说“第一个位置是你的了,你和第一个同学换一下,剩下的同学我继续选。”这就是“选择”类的排序。
属于这类排序的有简单选择排序、堆排序。

1.简单选择排序

  1. 算法介绍
    选择类排序的主要动作是“选择”,简单选择排序采用最简单的选择方式,从头至尾顺序扫描序列,找出最小的一个关键字,和第一个关键字交换,接着从剩下的关键字中继续这种选择和交换,最终使序列有序。

  2. 算法代码

void insertSort(int arr[],int n)//待排关键字存储在arr[]中,默认为整型,个数为n
{
	int temp,i,j,k;
	/*【第一层循环】挑选出n个关键字与当前序列第一个关键字交换*/
	for(i=0;i<n;++i)
	{
		k=i; 
		/*【第二层循环】这个循环是算法的关键,它从无序序列中挑出一个最小的关键字;最值代码,用k存储最值中的下标*/
		for(j=i+1;j<n;++j)
			if(arr[k]>arr[j])
				k=j;
		/*下面3句完成最小关键字与无序序列第一个关键字的交换;无序序列第一个关键字arr[i]*/
		temp=arr[i]	;
		arr[i]=arr[k]
		arr[k]=temp;//
		}
	}
}

2.堆排序

  • 堆的定义
    堆是一种数据结构,可以把堆看作一棵完全二叉树且满足:
    任何一个非叶结点的值都不大于(或不小于)其左右孩子结点中的关键字值。

    对堆中任意一个关键字都不小于其孩子节点的关键字,这就导致堆最上面那一个节点的关键字是最大的,即父亲大孩子小,则这样的堆叫作大顶堆,若父亲小孩子大,则这样的堆叫作小顶堆。

  • 应用
    实现优先级队列
    优先级队列应用:如操作系统作业调度

  • 逻辑结构
    完全二叉树
    不仅仅是完全二叉树,它对树中的结点值,这里称之为关键字,还有进一步的约束

    • 完全二叉树中最后一个非叶子结点的编号
      n个结点,从0开始编号,【n/2】-1;其中【向下取整】
      从1开始编号,【n/2】
  • 存储结构
    数组
    完全二叉树最方便的存储结构,顺序存储结构- -数组
    直接按照完全二叉树的结点编号,把它依次地存到一个数组里

在这里插入图片描述若从1开始给结点编号,
父节点位置为 i
左孩子结点位置 2i
右孩子结点位置 2i+1
最后一个结点是 第【n/2】个(向下取整)结点的孩子

操作

0.前提
已经有了一颗完全二叉树,并存储在一位数组中

  • 建堆
    堆排序的关键是构造初始序列建堆,是一个反复筛选过程
    对于大顶堆
    若根节点的关键字小于左右孩子中关键字较大者,则交换
    对于小顶堆
    若根节点的关键字大于左右孩子中关键字较小者,则交换
  1. 首先找出这棵完全二叉树中最后一个非叶子结点的编号

  2. 用p标出了这一个节点的位置,然后按照规则处理观察p所指的节点以及其孩子节点的关键字值。

  3. 规则1
    【p与孩子比较后交换】
    如果p所指节点的关键字值比其孩子节点中的最大关键字的那一个要小,设那一个具有最大关键字的孩子节点叫a,那就让p与a交换

  4. 规则2
    【无交换p前移】
    若没必要交换,p往前移动一个编号的位置【即比当前编号小1位置】

  5. 规则3
    【都可交先交最者,小顶堆交换最小者,大顶堆交换最大者】
    p来到了38,其孩子结点,一个是97,另外一个是76,因为大顶堆所以38和97交换
    交换后p先不迁移,被交换的往下继续交换,变成了叶子节点无需再交换】
    交换后p不要迁移,需要观察被交换到下面的那一个节点值不是叶子结点,看看它是否小于其当前的孩子节点的关键字值,如果小于则继续交换
    若被交换到下面的那一个节点值已经变成了叶子节点,所以不需要再继续进行交换了
    最后一个非业节点

  6. 规则4
    【p为根,建堆结束】p来到了第一个节点,也就是根节点,所以到这里建堆就结束了

  • 插入

  • 删除

代码

建堆排序函数

/*堆排序函数*/
void heapSort (int R[], int n)
{
	int i;
	int temp;
	for (i=n/2; i>=1;--i)//建立初始堆
		Sift (R, i,n) ;
	for (i=n;i>=2;--i)//进行n-1次循环,完成堆排序
	{
	/*以下3句换出了根结点中的关键字,将其放入最终位置*/
		temp=R[1] ;
		R[1]=R[i] ;
		R[i]=temp ;
		Sift (R,1,i-1) ;//在减少了1个关键字的无序序列中进行调整
	}
}

调整函数

/*本函数完成在数组R[1ow]到R[high]的范围内对在位置low上的结点进行调整*/ 
void Sift(int R[],int low,int high) //这里关键字的存储设定为从数组下标1开始
{
	int i=low,j=2*i;          //R[j]是R[i]的左孩子结点
	int temp=R[i] ;
	while (j<=high)
	{
		if(j<high&&R[j]<R[j+1])//若右孩子较大,则把j指向右孩子
			++j ;                //j 变为2*i+1
		if (temp<R[j])
		{
			R[i]=R[j] ;          //将R[j]调整到双亲结点的位置上
			i=j ;                //修改i和j的值,以便继续向下调整
			j=2*i;
		}
		else
			break;							//调整结束
     }
		R[i]=temp;              //被调整结点的值放入最终位置
}

时间复杂度

O(nlog2n)

归并类的排序

所谓归并就是将两个或两个以上的有序序列合并成一个新的有序序列,归并类排序就是基于这种思想。我们继续排队,这次教官想了个特别的方法,他说:“你们每个人,先和旁边的人组成一个二人组,二人组内部先排好。”看到大家排好了,继续说:“二人组和旁边的二人组继续组合成一个四人组,每个四人组内部排好,动作快!”这样不停排下去,最后全部学生都归并到了一个组中,同时也就排好序了。这就是“归并”类的排序。这个例子正是二路归并排序,特点是每次都把两个有序序列归并成一个新的

(八)二路归并排序(mergesort)

(九)基数排序

基数排序(Radix Sorting)是和前面所述各类排序方法完全不相同的一种排序方法。
实现排序主要是通过关键字间的比较和数据移动记录这两种操作,而实现基数排序不需要进行记录关键字间的比较。

基数排序算法实现

数据结构:

/为了有效地存储和重排记录,采用静态链表作为存储结构/

# define MAX_ NUM_ 0F_ KEY 8 //关键字项数的最大值
# define RADIX 10 //关键字基数,此时是十进制整数的基数
# define MAX_ SPACE 10000

typedef struct {
	KeysType keys[MAX_ NUM_ 0F_ KEY]};//关键字
	InfoType otheritems;//其他数据项
	int next ;//静态链表指针域
}SLCe1l;//静态链表的结点类型

typedef struct {
	SLCe11 r[MAX_ SPACE];//静态链表的可利用空间,r[0]为头结点//静态链表表示待排序记录
	int keynum;//记录的当前关键字个数
	int recnum;//静态链表的当前长度
} SLList;//静态链表类型

typedef int ArrType[RADIX]; // 指针数组类型

算法思想:

基数排序是借助“分配”和“收集”两种操作,借助多关键字排序的思想,对单逻辑关键字进行排序的方法。

(十)外部排序

(十一)各种排序算法的比较

(十二)排序算法的应用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值