【数据结构】排序

发布于:2024-04-30 ⋅ 阅读:(21) ⋅ 点赞:(0)

基本概念

排序:将一组无序的数据元素按其关键字的非递减(或非递增)次序排列成有序序列。
稳定:相同的元素在排序前后的相对位置不发生改变。
不稳定:相同的元素在排序前后的相对位置发生改变。
内部排序:将排序记录序列全部读入内存中处理。
外部排序:仅有部分记录在内存中,还要对外村中的记录进行访问。

评价算法的效率主要有两条:
(1)时间开销,主要消耗在元素之间的比较和记录的移动上。
(2)存储空间

插入排序

基本思想:将第一个记录看作一个有序序列,然后每次将下一个待排序的记录有序插入已排好的有序序列中,使有序序列逐渐扩大,直至所有记录都加到有序序列中。

本文主要介绍三种:直接插入排序、折半插入、希尔排序

直接插入排序

记录序列为R[0…n-1],首先将第一个记录R[0]看作一个有序子序列,然后依次将记录R[i]插入有序子序列R[0…i-1]中,使记录的有序子序列从R[0…i-1]变为R[0…i]

例:8个待排序记录,{36,80,45,66,22,9,16,36}
在这里插入图片描述

template <class Type>
void straightInsertSort(Type R[], int size){
	int pos,j;		//pos为待插入记录位置
	Type tmp;
	
	for(pos=1;pos<size;pos++){	//取初始序列的元素
		tmp=R[pos];		//将待插入记录放进临时变量中
		for(j=pos-1; tmp<R[j]&&j>=0; j--)	//在有序序列中,从后向前查找插入位置
			R[j+1]=R[j];		//将大于待插入记录向后移动
		R[j+1]=tmp;			//待插入记录放到合适位置
	}
}

直接插入是一种稳定排序。
基本操作有两种:比较关键字移动记录
平均时间复杂度O(n2), 空间复杂度O(1)

折半插入排序

对直接插入排序算法进行改进,可以从减少比较和移动次数这两个方面入手。

当前面i-1个有序记录序列时,要插入第i个记录,可以利用折半查找方式确定插入位置。

template <class Type>
void binaryInsertSort(Type R[],int size){
	int pos, j, low, high, mid;
	Type tmp;
	
	for(pos=1;pos<size;high=pos-1;){	//假定第一个记录有序
		tmp=R[pos];			//将待排序记录R[pos]暂存到tmp中
		low=0;
		high=pos-1;			//设置折半查找的范围
		while(low<=high){		//在R[low...high]中折半查找有序插入位置
			mid=(low+high)/2;		//计算中间位置
			if(tmp<R[mid])	high=mid-1;		//插入点在低半区
			else low=mid+1;				//插入区在高半区
		}
		for(j=pos-1;j>=low;j==)
			R[j+1]=R[j];		//记录后移
		R[low]=tmp;			//插入待排序记录
	}
}

折半插入排序减少了关键字间的比较次数,但记录移动的次数不变时间复杂度为O(n2)

希尔排序

又称为缩小增量排序。

对“减少记录个数”和“基本有序”两方面对直接插入排序进行了排序。

基本思想:将待排序的记录划分成几组,间距相同的记录分在一个组中,对各组分别实施直接插入排序。当经过几次分组排序后,记录的排序已经基本有序,再对所有的记录试试最后的直接插入排序。
在这里插入图片描述

template <class Type>
void shellSort(Type R[], int size){
	int gap, pos, j;
	Type tmp;
	
	for(gap=size/2;gap>0;gap/=2){	//gap为希尔增量,即步长
		for(pos=gap;pos<size;pos++){		//pos为待插入记录位置
			tmp=R[pos];
			for(j=pos-gap;j>=0&&R[j]>tmp;j-=gap)	
				R[j+gap]=R[j];		//记录后移
			R[j+gap]=tmp;			//将待插入记录放到合适位置
		}
	}
}

希尔排序适用于待排序的记录数量较大的情况,是一种不稳定的排序方法。
其时间性能与选定的增量序列有关,时间复杂度约O(n1.3),空间复杂度O(1)。

交换排序

冒泡排序

基本思想:对所有相邻的关键字进行比较,若发现记录逆序,则交换。
在这里插入图片描述

template <class Type>
void bubbleSort(Type R[], int size){
	int i,j ;
	bool flag=true;		//记录一趟排序后是否发生过交换
	for(i=1;i<size&&flag;++i){
		flag=false;		//假定本次排序没有交换
		for(j=0;j<size-i;++j)
			if(R[j+1]<R[j])	{		//逆序
				swap(R[j],R[j+1])		//调用STL中的swap进行交换
				flag=true;
			}
	}
}

冒泡排序是一种稳定的排序方法,关键字的比较次数和记录的交换次数的初始顺序有关。
平均时间复杂度为O(n2),空间复杂度为O(1).

快速排序

又称为分区交换排序,是对,冒泡排序的改进

基本思想:
(1)在序列中选取一个记录作为枢轴,并以该记录的关键字为基准
(2)凡是关键字小于枢轴记录的均移动值枢轴之前,凡是关键字大于枢轴记录的均移动值枢轴之后。一趟排序后,记录序列分为两个子序列L和R,使得L中所有记录的关键字都小于等于key,R中均大于key,枢轴位于二者之间,且刚好在最终位置上。
(3)对子序列L和R进行快速排序

一趟快速排序的具体做法
(1)设置两个指针low和high分别用来指示将要与枢轴进行比较的左侧记录和右侧记录。
(2)先从high所指位置开始向前查找关键字小于枢轴关键字的记录,将其与之交换,再从low所指位置开始向后查找关键字大于枢轴关键字的记录,将其与之交换。重复上述步骤直到low与high相等。

一次划分后,枢轴两侧记录数量越接近,排序速度将越块。

在这里插入图片描述
在这里插入图片描述

以最左侧记录作为枢轴的快速排序:

template <class Type>
int partition(Type S[], int low, int high){
	Type tmp=S[low];	//暂存枢轴
	while(low!=high){		//开始分割
		while(low<high&&S[high]>=tmp)	high--;	//大下标端小于枢轴的记录
		if(low<high)	{S[low]=S[high];	low++;}		//记录移动到小下标端
		while(low<high&&S[low]<=tmp)	low++;	//小下标端大于枢轴的记录
		if(low<high)	{S[high]=S[low];	high--;}	//记录移动到大下标端
	}
	S[low]=tmp;		//把枢轴回填到分界位置上
	return low;		//返回枢轴位置
}

递归快速排序

template <class Type>
void quickSort(Type S[], int low, int high){
	int pivot;
	if(low>=high)	return;
	pivot=partition(S,low,high);	//一次划分,返回枢轴位置
	quickSort(S,low,pivot-1);		//枢轴左边一半快速排序
	quickSort(S,pivot+1,high);		//枢轴右边一半快速排序
}

快速排序的接口函数

template <class Type>
void quickSort(Type S[], int size){	//待排序序列,序列大小
	quickSort(S,0,size-1);
}

快速排序的平均时间复杂度为O(nlogn),就平均时间而言,是被认为目前最好的内部排序算法
是一种不稳定的算法,适用于较多且基本无序的情况。

选择排序

基本思想
依次将待排序记录中选出关键字最小(或最大)的记录、关键字次之的记录…并分别将它们定位到序列左侧(或右侧)的第1个位置、第2个位置…。直至序列中只剩下一个最小(或最大)的记录位置。

本文介绍两种选择排序方法:直接选择排序、堆排序

直接选择排序

基本思想:第1趟从n个记录中选取关键字最小的记录与第1个记录互换;第2趟从剩余的n-1个记录中选取关键字最小的记录与第2个记录互换…第i趟从剩余的n-i+1个记录中选取关键字最小的记录与第i个记录互换.
在这里插入图片描述

template <class Type>
void straightSelectSort(Type R[],int size){
	int pos, min, j;		//min为一趟排序中最小记录的下标
	for(pos=0;pos<size-1;pos++){		//pos为待存放当前最小记录的位置
		min=pos;
		for(j=pos+1;j<size;++j)
			if(R[j]<R[min])		//查找最小记录
				min=j;
			if(pos!=min)		//调用STL中的swap,头文件algorithm
				swap(R[pos],R[min]);	//把无序序列中的最小值换到前面有序序列中
	}
}

直接排序是一种不稳定的排序方法。
关键字的比较次数和记录的顺序无关
平均时间复杂度O(n2),空间复杂度O(1)

堆排序

基本思想
(1)由存储于数组的序列,建成一个大根堆(或小根堆)
(2)将堆顶的最大(或最小)记录与序列中最后一个记录交换,最大(或最小)记录存放在数组末尾的有序段中。
(3)将序列中除末尾的有序段之外的剩余记录重新调整为堆
(4)重复步骤三四共n-1次,若初始建立的是大根堆,则得到一个非递减序列;若建立的是小根堆,则得到一个非递增序列。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
向下调整堆:

template <class Type>
void siftDown(Type R[], int pos, int size){	//待排序序列,要调整的结点编号,序列大小
	int child;
	Type tmp=R[pos];		//暂存“根”记录
	
	for(;pos*2+1<size;pos=child){
		child=pos*2+1;
		if(child!=size-1 && R[child+1]>R[child])
			child++;		//选取两个孩子的大者
		if(R[child]>tmp)		//较大的孩子比双亲大
			R[pos]=R[child];
		else 
			break;
	}
	R[pos]=tmp;		//被调整结点放入正确位置
}

堆排序:

template <class Type>
void headSort(Type R[], int size){
	int i;
	for(i=size/2-1; i>=0; i--)	//初始建堆,从最后一个非叶结点开始调整堆
		siftDown(R,i,size);
	for(i=size-1; i>0; i--){	//共size-1趟排序(删除堆顶元素后反复调整堆)
		swap(R[0],R[i]);		//堆顶元素与子序列中最后一个元素交换
		siftDown(R,0,i);		//将R[0..i]重新调整为大根堆
	}	
}

堆排序过程中存在大幅度的数据移动,是一种不稳定的排序方法
时间复杂度为O(nlogn),空间复杂度O(1)

快速排序和堆排序经常用于排在前面的若干最大(或最小)记录

归并排序

归并是指将两个或两个以上的有序表合并成一个新的有序表。

在内部排序中,采用的是2-路归并排序

2-路归并排序的基本思想
将一个具有n个待排序记录的序列看成n个长度为1的有序序列,然后将相邻的子序列进行两两归并,得到[n/2]个长度为2的有序子序列,继续对相邻子序列进行两两合并。得到[n/4]个长度为4的有序子序列.重复此过程,直到得到一个长度为n的有序序列为止。
在这里插入图片描述

归并相邻两个有序子序列。将有序序列R[low…mid-1]和R[mid…high]归并为有序序列R[low…high]:

template <class Type>
void merge(Type R[], Type tmp[], int low,int mid,int high){
	int i=low, j=mid,k=0;
	while(i<mid && j<=high)		//R中记录由小到大复制到tmp中
		if(R[i]<R[j])	tmp[k++]=R[i++];   //将R[i]和R[j]中的小者复制到tmp[k]中
		else tmp[k++]=R[j++];
	while(i<mid)	tmp[k++]=R[i++];	//将剩余的R[i...mid-1]复制到tmp中
	while(j<=high)	tmp[k++]=R[j++];	//将剩余的R[j...high]复制到tmp中
	for(i=0,k=low;k<=high;)
		R[k++]=tmp[i++];	//排好序的记录由tmp复制回R中
}

递归2-路归并排序。
通过递归调用实现对子序列R[low…high]的排序过程,将其归并为有序段。

template <class Type>
void mergeSort(Type R[],Type tmp[],int low, int high){
	if(low==high)	return;
	int mid=(low+high)/2;
	mergeSort(R,tmp,low,mid);
	mergeSort(R,tmp,mid+1,high);	//递归地对子序列R[mid+1...high]进行归并排序
	merge(R,tmp,low,mid+1,high);	//归并两个子序列
}

2-路归并排序的接口函数

template <class Type>
void mergeSort(Type R[], int size){
	Type *tmp=new Type[size];		//辅助数组
	mergeSort(R,tmp,0,size-1);
	delete [] tmp;
}

归并排序是一种稳定的排序方法。
时间复杂度O(nlogn),空间复杂度O(n)

各内部排序方法的比较

在这里插入图片描述

习题(含408真题)

1.【408】下列选项中,不可能是快速排序第2趟排序结果是()
A.2,3,5,4,6,7,9
B.2,7,5,6,4,3,9
C.3,2,5,4,7,6,9
D.4,2,3,5,7,6,9

每趟快速排序后,至少一个记录找到最终位置。选C

2.【408】数据元素序列11,12,13,7,8,9,12,4,5,是采用下列排序方法之一得到的第2趟排序后的结果,该算法只能是()
A.冒泡排序
B.插入排序
C.选择排序
D.2-路归并排序

冒泡排序在第2趟至少排好了前两个最大值
选择排序在第2趟至少排好了前两个最小值
2-路归并排序的子序列应该是有序的,找不到这种有序的子序列。
选B.

3.下面4种排序方法中,排序过程中的比较次数与排序方法无关的是()
A.选择排序法
B.插入排序法
C.快速排序法
D.堆排序法

选A

4.分别采用堆排序、快速排序、冒泡排序和归并排序,对初态为有序的表,则最省时间的是(),最费时间的是()

冒泡排序;快速排序


网站公告

今日签到

点亮在社区的每一天
去签到