数据结构(C语言描述)排序与选择算法

1 简单排序算法

1.1 冒泡排序算法

思想:键值较小的记录比较轻,要往上浮。
处理一遍,就是自底向上检查一遍这个序列,注意两个相邻的记录的顺序是否正确。如果发现两个相邻记录的顺序不对,即“轻”的记录在下面,就变换它们的位置。
处理一遍之后,“最轻”的记录就浮到了最高位置;处理两遍之后,“次轻”的记录就浮到了次高位置。
一般地,第i遍处理时,不必检查i高位置以上的记录。
设待排序的数组段是a[l]~a[r],则冒泡排序算法实现如下。

typedef int Item;//待排序元素类型
#define less(A,B) (key(A)<key(B))
#define swap(A,B) {Item t=A;A=B;B=t;}
#define compswap(A,B) if(less(B,A)) swap(A,B)
void sort(Item a[],int l,int r)
{
    
    //冒泡排序
	for(int i=l+1;i<=r;i++)
		for(int j=i;j>1;j--)
		compswap(a[j-1],a[j]);
}
void ItemShow(Item x)
{
    
    
	printf("%d \n",x);
}

待排序元素类型是Item;
less(A,B)比较A和B的键值,等价于key(A)<key(B);
swap(A,B)交换A和B的值;
compswap(A,B)等价于语句if(less(B,A)) swap(A,B),即当key(B)<key(A)时,交换A和B的值。

1.2 插入排序算法

原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
类似拿到17张纸牌,从小到大排序整理纸牌。
元素插入过程由算法insert算法来完成。

void insert(Item a[],int l,int i)
{
    
    //元素a[i]插入数组a[l:i]
	Item v=a[i];
	while(i>1 &&less(v,a[i-1])) {
    
    a[i]=a[i-1];i--;}
	a[i]=v;
void sort(Item a[],int l,int r)
{
    
    
	for(int i=l+1;i<=r;i++) insert(a,l,i);
}
}

插入排序算法通过反复调用insert来完成排序任务。

1.3 选择排序算法

选择排序
例题:用一个函数实现用选择法对10个整数按升序排序。
思路:进行n-1轮排序,每一轮将最小的数与a[n-1]对换。每一轮对换两个数的位置。例如:
用选择法对5个数排序的步骤。
在这里插入图片描述
确定要排序数组中最小元素下标的算法mini如下。

int mini(Item a[],int i,int r)
{
    
    //确定要排序数组中最小元素下标
	int min=i;
	for(int j=i+1;j<=r;j++) if(less(a[j],a[min])) min=j;
	return min;
}
void sort(Item a[],int l,int r)
{
    
    //选择排序算法
	for(int i=l;i<r;i++) {
    
    int j=mini(a,i,r);swap(a[i],a[j]);}
}

1.4 简单排序算法的复杂性

冒泡排序和选择排序算法至少需要O(n^2)计算时间。
插入排序在最坏情况下,需要O(n^2)计算时间。

2 快速排序算法

快速排序算法,平均情况下需要O(nlogn)时间。

2.1 算法基本思想及实现

基于分治策略的排序算法。
基本思想:对输入的数组,按3个步骤进行排序。
(1)分解。以基准(a[r])将输入的数组(a[l:r])分为3段前段的数(a[l:i-1]),基准(a[r])和后段的数(a[i+1:r]),使得前段的数小于基准,后段中任何一个元素大于等于基准。
(2)递归分解。通过递归调用快速排序算法分别对前段的数(a[l:i-1])和后段的数(a[i+1:r])进行排序。
(3)合并。由于对段的数(a[l:i-1])和后段的数的排序时就地进行的,所以在前段的数(a[l:i-1])和后段的数都已排好序后不需要执行任何计算,数组(a[l:r])就已排序好了。
实现快速排序算法如下。

void sort(Item a[].int l,int r)
{
    
    //快速排序算法
	if(r<=l) return;
	int i=partition(a,l,r);
	sort(a,l,i-1);//对左半段排序
	sort(a,i+1,r);//对右半段排序
}
int partition(Item a[],int l,int r)
{
    
    //元素划分算法
	int i=l-1,j=r;Item v=a[r];
	//将大于等于v的元素交换到右边区域
	//将小于等于v的元素交换到左边区域
	for(;;){
    
    
	while(less(a[++i],v));
	while(less(v,a[--j])) if(j==l) break;
	if(i>=j) break;
	swqp(a[i],a[r]);
	}
	swap(a[i],a[r]);
	return i;
}

函数partition,以一个确定的基准元素a[r]对数组a[l:r]进行划分,它是快速排序算法的关键。
函数partition对数组a[l:r]进行划分时,以元素v=a[r]作为划分的基准,分别从左、右两端开始,扩展两区域a[l:i]和a[j:r],使得数组a[l:r]中的元素小于或等于v,而a[j:r]中元素大于等于v。i=l-1且j=r。

2.2 算法的性能

快速排序算法的性能取决于划分的对称性。

2.3 随机快速排序算法(改快速排序算法的基准)

通过修改快速排序算法的函数partition,可以设计出采用随机选择策略的快速排序算法。在快速排序算法的每一步,当数组还没有被划分时,可以在a[l:r]中随机选出一个元素划分基准,这样可以使划分基准的选择是随机的。

int randompartition(Item a[],int l,int r)
{
    
    //随机划分算法
	int i=randomi(l,r);
	swap(a[i],a[l]);
	return partition(a,l,r);
}
int randomi(int l,int r)
{
    
    //随机选取划分基准
	return 1+(r-l)*(1.0*rand/RAND_MAX);
}
void sort(Item a[].int l,int r)
{
    
    //快速排序算法
	if(r<=l) return;
	int i=partition(a,l,r);
	sort(a,l,i-1);//对左半段排序
	sort(a,i+1,r);//对右半段排序
}

函数randomi(l,r)产生l和r之间的一个随机整数。
随机化的快速排序算法通过调用函数randompartition产生随机划分。

2.4 非递归快速排序算法(改快速排序算法的递归)

设待排序数组大小为n时,快速排序算法所需栈空间为s(n),若采用小者优先递归的策略,则s(n)满足
在这里插入图片描述
s(n)<=logn。
对快速排序算法的改进如下。

void sort(Item a[].int l,int r)
{
    
    //快速排序算法
	if(r<=l) return;
	int i=partition(a,l,r);
	if(i-l>r-i){
    
    sort(a,i+1,r);sort(a,l,i-1);}
	else {
    
    sort(a,l,i-1);sort(a,i+1,r);}

采用模拟递归技术可以消去算法的递归调用。
在这里插入图片描述
push2(A,B,s)定义为连续两次进栈运算Push(B,s)和Push(A,s)。
非递归快速排序算法,有效改进递归算法的性能。

2.5 三数取中划分算法

思想:基于划分基准的选取。
对于待排序数组a[l:r],算法选取a[l],a[r],a[(l+r)/2]这3个数的中位数作为划分基准,从而改进划分的对称性。
三数取中快速排序算法如下。
在这里插入图片描述

2. 6 三划分快速排序算法

思想:
(1)在划分阶段以v=a[r]为划分基准,将待排序数组a[l:r]划分为左、中、右三段a[l:j],a[j+1:i-1],a[i:r]。
(2)左段数组a[l:j]中元素键值小于v,中段数组a[j+1:i-1]中元素键值等于v,右段数组a[i:r]中元素键值大于v。
(3)算法对左右两段数组递归排序。
(4)实现三划分快速排序算法时,首先将键值与v相同的元素分别交换到左右两段数组的左右两端。在搜索游标i和j交叉后,再将这些元素交换到中段数组中。
实现三划分快速排序算法描述如下。

void sort(Item a[],int l,int r)
{
    
    //三划分快速排序算法
	int i=l-i,j=r,p=l-1,q=r;
	Item v=a[r];
	if(r<=1) return;
	for(;;){
    
    
	while(less(a[++i],v))
	while(less(v,a[--j])) if(j==1) break;
	if(i>=j) break;
	swap(a[i],a[j]);
	if(eq(a[i],v)){
    
    p++;swap(a[p],a[i]);}
	if(eq(v,a[j])){
    
    q--;swap(a[p],a[j]);}
}
swap(a[i],a[r]);j=i-1;i=i+1;
for(int k=l;k<p;k++,j--) swap(a[k],a[j]);
for(int k=r-1;k<q;k--,i++) swap(a[k],a[i]);
sort(a,l,j);
sort(a,i,r);
}

3 合并排序算法

3.1 算法基本思想及实现

合并排序是将两个(或两个以上)有序表合成一个新的有序表。
合并排序算法是用分治策略实现对n个元素排序的算法。
思想:当n=1时终止排序,否则将待排序元素分成大小大致相同的两个子集,分别对两个子集进行排序,最终将排好序的子集合并为所求的集合。
例如:将8,4,5,7,1,3,6,2按照从小到大合并排序。
在这里插入图片描述
一共7次排序,下面举例最后一次排序的过程
1<4,将2填入temp临时数组,右移J
在这里插入图片描述
2<4,将2填入temp临时数组,右移J
在这里插入图片描述
3<4,将3填入temp临时数组,右移J
在这里插入图片描述
6>4,将4填入temp临时数组,右移i

在这里插入图片描述
5<6,将5填入temp临时数组,右移i
在这里插入图片描述
7>6,将6填入temp临时数组,J到头
在这里插入图片描述
直接将7,8填入temp临时数组
在这里插入图片描述
最后将temp中的内容全部复制到原数组中去,排序完成。
在这里插入图片描述

合并排序算法可递归地描述如下。

void msort(Item a[],int l,int r)
{
    
    //合并排序算法
	int m=(r+l)/2;//取中点
	if(r<=l) return;
	msort(a,l,m);//对左边半段排序
	msort(a,m+1,r);//对右边半段排序
	mergeab(a,b,l,m,r);//合并到数组b
	copy(b,a,l,r);//复制回数组a
}
void mergeab(Item a[],Item b[],int l,int m,int r)
{
    
    //合并左半段a[1:m]和右半段a[m+1:r]到b[l:r]
	int i=l,j=m+1,k=l;
	//取两段中较小元素到数组b中
	while((i<=m)&&(j<=r))
		if(less(a[i],a[j])) b[k++]=a[i++]
		else b[k++]=a[j++];
	//处理剩余元素
	if(i>m) for(i=j;i<=r;i++) b[k++]=a[i];
	else for(;i<=m;i++) n[k++]=a[i];
}
void copy(Item b[],Item a[],int l,int r)
{
    
    
	for(int i=l;i<=r;i++) a[i]=b[i];
}

合并排序算法是渐近最优算法。

3.2 对合并排序算法的改进

3.3 自底向上合并排序算法

事实上, 算法mergesort的递归过程只是将待排序集合一分为二, 直至待排序集合只剩下1个元素,然后不断合并两个排好序的数组段。
按此机制,首先将数组a中相邻元素两两配对,用合并算法将它们排序,构成n/2组长度为2的排好序的子数组段。然后将它们排成长度为4的排好序的子数组段。如此继续下去,直至整个数组排好序。
按此思想, 消去递归后的自底向上合并排序算法mergesortBU可描述如下。
在这里插入图片描述

3.4 自然合并排序算法

自然合并排序算法是自底向上合并排序算法mergesortBU的一个变型。
例如:
在这里插入图片描述
自然合并排序算法需要O(n)时间。

3.5 链表结构的合并排序算法

3.5.1 结点结构定义

存放待排序元素的结点结构定义如下。

typedef struct node *link;//结点指针类型
typedef struct node{
    
    
	Item element;//待排序元素
	link next;//下一结点指针
}Node;

3.5.2 合并新的有序链表

算法merge(a,b)将两个有序链表a和b合并为一个新的有序链表。

link merge(link a,link b)
{
    
    //链表合并
	Node head;
	link c=&head;
	while(a&&b)
		if(less(a->element,b->element)) {
    
    c->next=a;c=a;a=a->next;}
		else{
    
    c->next=b;c=b;b=b->next;}
	c->next=(!a)?b:a;
	return head.next;
}

3.5.3 分成两个子链表,并递归

将一个链表分为大小相同的两个子链表,然后递归对两个子链表排序。最后用算法merge将排好序的子链表合并为整个排好序的链表。

link mergesort(link c)
{
    
    //链表合并排序算法
link a,b;
if(!c->next)return c;
a=c;b=c->next;
while(b&&b->next) {
    
    c=c->next;b=b->next->next;}
b=c->next;c->next=0;
return merge(mergesort(a),mergesort(b));
}

4 线性时间排序算法

线性时间排序算法在线性时间内完成排序任务。

4.1 计数排序算法

思想:对每一个输入元素x,确定输入序列中键值小于x的元素个数。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。
例如:若输入序列中有17个元素的键值小于x,则x就应存放在第18个输出位置上。
程序设计思路:
假设输入的n个元素存放在数组a[0:n-1]中,输出的排序结构存放在数组b[0:n-1]中。数组a和b中的每个元素均为0~m之间的一个整数,算法中还用到一个辅助数组cnt[0:m]用于对输入元素进行计数。
编写程序:

void countsort(int a[],int l,int r)
{
    
    
int *b=(int *)malloc(size(int)*(r+1));
int cnt[m]={
    
    0};//对数组cnt初始化
for(int i=l;i<r;i++) cnt[a[i]]++;//某个输入元素的值为i,则cnt[i]增1
for(int i=1;i<m;i++) cnt[i]+=cnt[i-1];//统计值小于或等于i的输入元素个数。
for(int i=l;i<=r;i++) a[i]=b[i-l];
free(b);
}

计数排序算法计算时间复杂性为O(n)。
在输入和输出序列中,具有相同值元素的相对次序不变。
计数排序算法是一个稳定的排序算法。

4.2 桶排序算法

与计数排序类似的一个线性时间排序算法是桶排序算法。
桶排序算法的基本思想是:设置若干个桶,将键值等于i的元素全部装入第i个桶中。然后,按桶的顺序将桶中元素顺序连接起来。由于每个桶中元素键值相同,可以将第i个桶看成键值为i的元素组成的一个表。用数组bottom表示桶底,bottom[i]指向第i个桶中第一个元素。用数组top表示桶顶,top[i] 指向第i个桶中最后一个元素。这样很容易按桶的顺序将桶中元素顺序连接在一起。
思路:桶排序算法假定输入以单链表形式给出,通排序算法返回排序后的单链表。元素键值上界为m。
编写程序:

define m 10000
link binsort(link first)
{
    
    //通排序算法
int b;//桶下标
link bottom[m+1],top[m+1];
link p=0;
for(b=0;b<=m;b++) bottom[b]=0;//桶初始化
for(;first;first=first->first->next){
    
    //将元素装入桶中
	b=first->element;
	if(bottom[b]){
    
    //桶非空
		top[b]->next=first;
		top[b]=first;}
	else nottom[b]=top[b]=first;//桶为空
}
//按桶的顺序将桶中元素顺序连接在一起
for(b=0;b<=m;b++)
	if(bottom[b]){
    
    
		if(p)p->next=bottom[b];//不是第一个非空桶
		else first=bottom[b];//第一个非空桶
		p=top[b];
}
	if(p)p->next=0;
	return first;
}

桶排序算法只需要O(n)计算时间。

4.3 基数排序算法

基数排序算法是一个与计数排序和桶排序算法十分类似的线性时间排序算法。
其基本思想是将输入数据看成具有相同长度的正整数,长度较短的数在高位用0补齐。然后,从最低位开始,按照从低位到高位的次序,依次对上一轮排序后数据的高一位数值做一次排序,直至最高位后完成排序。下面用一个具体例子来说明基数排序算法。
例题:用基数排序对下列数从小到大排序
39,457,657,39,436,720,355
思路:
(1)将长度较短的数39在高位用0补齐成039。这样一来,待排序的7个正整数成为长度均为3的正整数039,457,657,039,436,720,355。
(2)按照从低位到高位的次序,先对7个正整数个位上的数值9,7,7,9,6,0,5进行排序。
排序后得到0,5,6,7,7,9,9。
按此序原来的7个数相应地排列成720,355,436,457,657,039,039。
(3)对第1轮排序后的7个正整数十位上的数值2,5,3,5,5,3,3进行排序。
排序后得到2,3,3,3,5,5,5。
原数相应地排列成720,436,039,039,355,457,657。
(4)算法对百位上的数值7,4,0,0,3,4,6进行排序。
排序后得到0,0,3,4,4,6,7。
原数相应地排列成039,039,355,436,457,657,720。经过3轮排序后完成了基数排序算法。
排序过程如下图
在这里插入图片描述
进制10,也称为基数。一般的基数R将正整数表示成R进制数,基数排序算法的思想依然不变。
编写程序:

void countsort(int a[],int b[],int l,int r,int p)
{
    
    
	int cnt[RADIX]={
    
    0};//清空计数器
	for(inti=l;i<=r;i++) cnt[a[i]/p%RADIX]++;//计数
	for(int i=1;i<RADIX;i++) cnt[i]+=cnt[i-1];
	for(int i=r;i>=l;i--) b[--cnt[a[i]/p%RADIX]]=a[i];
	for(int i=l;i<=r;i++) a[i]=b[i-1];
}
void RadixSort(int a[],int l,int r)
{
    
    
	int maxv=0,pow=1;
	int *b=(int *)malloc(sizeof(int)*(r+1));
	for(int i=l;i<r;i++) if(a[i])maxv=a[i];
	while(amxv/pow>0){
    
    
		countsort(a,b,l,pow);
		pow*=RADIX;
}
	free(b);
}

基数排序算法RadixSort是稳定排序算法。

5 中位数与第k小元素

5.1 平均情况下的线性时间选择算法

选择问题的一个算法randomselect,该算法实际上是模仿快速排序算法设计出来的。其思想也是对输入数组进行递归划分。
算法randomselect是一个随机化的算法。

5.1.1 randomselect算法

Item randselect(Item,a[],int l,int r,int k)
{
    
    
	if(r<=l) return a[r];
	int i=randompartition(a,l,r);
	int j=i-l+1;
	if(j==k) return a[i];
	if(j>k) return randomselect(a,l,i-1,k);
	else return randomselect(a,i+1,r,k-j);
}

在最坏情况下,算法randomselect需要O(n^2)计算时间。
平均时间为O(n)。

5.1.2 改进randomselect算法

消除尾递归选择算法

Item randomselect(Item a[],int l,int r,int k)
{
    
    
	int i,j;
	while(r>l){
    
    
		i=randompartition(a,l,r);
		j=i-l+1;
		if(j==k) return a[i];
		if(j>k) r=i-1;
		else {
    
    l=i+1;k-=j;}
}
	return ((r<i)?a[i]:a[r]);

5.2 最坏情况下的线性时间选择算法

6 小结

(1)简单排序算法有:冒泡排序算法、插入排序算法和选择排序算法。
冒泡排序和选择排序算法至少需要O(n^2)计算时间。
插入排序在最坏情况下,需要O(n^2)计算时间。
(2)快速排序算法有:快速排序算法、随机快速排序算法、非递归快速排序算法、三数取中划分算法和三划分快速排序算法。
快速排序算法,平均情况下需要O(nlogn)时间。
(3)合并排序算法可以改进为自底向上合并排序算法、自然合并排序算法和链表结构的合并排序算法。
(4)线性时间排序算法有:计数排序算法、基数排序算法和桶排序算法。

猜你喜欢

转载自blog.csdn.net/qq_45059457/article/details/114953513