学习随记五十二——十种基本的排序算法比较

基本的排序算法

插入排序
希尔排序
堆排序
归并排序
快速排序
计数排序
基数排序
桶排序


前言

  数据排序是指按一定规则对数据进行整理排列, 为数据的进一步处理做好准备。在计算机领域主要使用的数据排序方法根据占用内存的方式不同分为两大类:内部排序方法与外部排序方法。排序算法是为了让无序的数据组合变成有序的数据组合。有序的数据组合最大的优势在于进行数据定位和采用时会非常方便, 因此数据排序算法在计算机处理数据时就变得非常重要, 了解并选择合适的排序算法可以大大提高工作效率。


注:本文所用数据为可比较大小的数据类型,且将数据按升序排列。算法描述可以结合代码一起看。

1、基本的排序算法实现原理和算法描述及其代码

一些说明:我都是将int类型重命名为ElementType 指元素类型,也可以修改成其他可以比较大小的元素类型

typedef int ElementType;
const int N= //自己定义的数组长度

1.1 冒泡排序

实现原理:冒泡排序属于比较排序,每次通过比较元素大小,交换位置使得数据像水中的泡泡一样浮出水面到达序列尾端,直到排序完成。

算法描述:从无序序列头部开始,进行两两比较,根据大小交换位置,直到最后将最大(小)的数据元素交换到了无序队列的队尾,从而成为有序序列的一部分;下一次继续这个过程,直到所有数据元素都排好序。

void BubbleSort(ElementType a[],int length){
	int i,j;
	ElementType temp=0;
	for(i=0;i<length;i++){
		for(j=i;j<length;j++){
			if(a[j]<a[i]){
				temp=a[i];
				a[i]=a[j];
				a[j]=temp;
			}
		}
	}
}

1.2 选择排序

实现原理:基于比较的排序算法,逐轮扫描将最值置顶,与冒泡排序不同的是利用标志变量记录最值下标。

算法描述:每次以无序序列的第一个元素为基准元素用一个标志变量记录下标。然后将基准元素与后面所有元素进行比较,遇到比它小的元素则记录其下标,然后其成为新的基准元素。当遍历完成将无序序列的第一个元素与基准元素进行交换,交换后基准元素从无序序列中删去进入有序序列,然后循环直到排序完成。

void SelectionSort(ElementType a[],int length){
	int i,j,pos;
	ElementType temp;
	for(i=0;i<length;i++){
		pos=i;
		for(j=i;j<length;j++){
			if(a[j]<a[pos]) pos=j;
		}
		temp=a[pos];
		a[pos]=a[i];
		a[i]=temp;
	}
}

1.3 插入排序

实现原理:基于比较相邻元素的排序算法,将每次指向的数组元素在之前已排好序的元素中找到合适的位置插入,然后将其插入位置之后的数组元素后挪。

算法描述:从第二个元素开始一直遍历到最后一个元素,每次令一个中间变量保存当前位置元素,然后开始从当前位置一直向前比较直到到第一个位置元素或前一个位置元素比当前位置元素小为止,每次用前一个位置元素覆盖后一个位置元素,当比较结束时,用中间变量保存的当前位置元素覆盖到达位置元素。这样每次进行插入时有两种情况。情况一,当前位置元素的前一个元素比它小,则内部循环没有进行,此时用中间变量覆盖当前位置相当于没有改变元素。情况二,当前位置元素的前面元素比它大,则进行循环直到找到合适位置进行插入,每次都将循环到的元素后挪一个位置,结束内部循环时则找到合适位置,用中间变量覆盖内循环结束的位置元素。

void Insertion_Sort(ElementType num[],int length){
	int i=0,j=0;
	ElementType temp=0;
	for(i=1;i<length;i++){
		temp=num[i];
		for(j=i;j>0&&num[j-1]>temp;j--) num[j]=num[j-1];
		num[j]=temp;
	}
}

1.4 希尔排序

实现原理:希尔排序属于比较相距一定间隔的排序算法。比较的间隔逐渐减小直到最小间隔为1,只比较相邻元素的最后一趟排序为止。由于这个原因希尔排序有时也被叫做缩小增量排序(diminishing increment sort).

算法描述:首先选择一个最小增量为1的增量序列。每次令当前增量为初始位置开始比较直到数组末尾;每次用一个中间变量保存当前位置元素,然后将中间变量与当前位置的前增量间隔的位置元素进行比较,若前面元素大则将其覆盖当前位置元素,然后将当前位置向前推进增量个单位,若前面元素小或当前位置小于当前增量则结束比较用中间变量覆盖当前位置元素(就是插入排序中为元素寻找到合适的位置进行插入)。然后,进行下一轮增量的比较直到最小增量1的最后一趟比较完成。
希尔增量的希尔排序:

void Shellsort_Shell(ElementType num[],int length){
	int i=0,j=0,increment=0;
	ElementType temp=0;
	for(increment=length/2;increment>0;increment/=2){			//希尔增量 
		for(i=increment;i<length;i++){
			temp=num[i];
			for(j=i;j>increment||j==increment;j-=increment){
				if(temp<num[j-increment]) num[j]=num[j-increment];
				else break;
			}
			num[j]=temp;
		}
	}
}

Hibbard增量的希尔排序:

void Shellsort_Hibbard(ElementType num[],int length){
	int i=length,j=0,increment=0,n=0;						//n是length关于2的最大阶乘数
	ElementType temp=0;
	while(i>1){																	//这个循环获得n 
		i/=2;
		n++;
	}
	for(;n>0;n--){	
		increment=(int)pow(2,n)-1;								//pow(x,y)计算x的y次方,返回结果 
		for(i=increment;i<length;i++){
			temp=num[i];
			for(j=i;j>increment||j==increment;j-=increment){
				if(temp<num[j-increment]) num[j]=num[j-increment];
				else break;
			}
			num[j]=temp;
		}
	}
}

1.5 堆排序

实现原理:不同于比较排序,堆排序主要是建立了一个大根堆,每次都是最大元素成为根节点。

算法描述: 我们可以通过建立一个二叉堆来对元素进行排序,有两种思路,一是先将这些元素建立一个二叉堆,执行N次删除最大元素的操作,用一个数组接收被删除的数据,这样就得到了元素的升序排列。这个算法的主要问题就是使用了一个附加数组,增大了空间开销,第二种实现方法是因为每次删除最大元素后堆的长度减1,所以可以将被删除的元素放在堆中的最后单元,当删除结束后就得到了升序排列。

void PercDown(ElementType a[],int i,int length){
	int Child;
	ElementType temp;
	for(temp=a[i];LeftChild(i)<n;i=Child){
		Child=LeftChild(i);
		if(Child!=n-1&&a[Child+1]>a[Child]) Child++;
		if(temp<a[Child]) a[i]=a[Child];
		else break;
	}
	a[i]=temp;
}
void Heapsort(ElementType a[],int length){
	int i;
	for(i=n/2;i>=0;i--) PercDown(a,i,n);			//Build Heap
	for(i=n-1;i>0;i--){
		Swap(&a[0],&a[i]);											//Delete the max
		PercDown(a,0,i);
	}
}

1.6 归并排序

实现原理:归并排序主要用到了递归思想中的分治,不断地将大数据切割成小数据然后进行排序,再回溯。

算法描述:归并排序算法十分巧妙,很好的体现了递归分而治之的思想。其基本方法就是将未排序的数组进行不断划分到每一段只有一个元素,显然不需要再进行排序,然后进行回溯,每次将左右半边中较小的元素存入中间数组中,一直到其中有一边运行到末尾,然后将已排好序的另一边余下的元素直接接到中间数组后面,然后再将中间数组中的这一段位置的元素覆盖原数组中相应的位置,即将这一段位置的元素排好序了,这就是并,然后不断回溯直至最后只有两段,这就是归。

void MergeSort(ElementType a[],int length){
	ElementType *temp;
	temp=(ElementType*)malloc(length*sizeof(ElementType));
	if(temp){
		MSort(a,temp,0,length-1);
		free(temp);
	}else{
		printf("No space !!!\n");
		exit(0);
	}
}
void MSort(ElementType a[],ElementType temp[],int left,int right){
	int center;
	if(left<right){
		center=(left+right)/2;				//中间位置等于左右边界相加除以2 
		MSort(a,temp,left,center);			//将左半部分进行递归地划分 
		MSort(a,temp,center+1,right);				//将右半部分进行递归地划分 
		Merge(a,temp,left,center+1,right);	//合并左右部分 
	}
}
void Merge(ElementType a[],ElementType temp[],int L_pos,int R_pos,int R_end){
	int L_end=R_pos-1;						//左边数组的终点就是中间位置的前一位
	int temp_pos=L_pos;						//中间数组的起点位置就是左边数组的起点位置
	int num=R_end-L_pos+1;					//数组的长度为尾端减去首端加一,因为考虑到数组从0开始计数
	//关键点来了!!!
	while(L_pos<=L_end&&R_pos<=R_end){//将两个数组中较小的那一个元素放入中间数组 
		if(a[L_pos]<=a[R_pos]) temp[temp_pos++]=a[L_pos++];
		else temp[temp_pos++]=a[R_pos++];
	} 
	while(L_pos<=L_end) temp[temp_pos++]=a[L_pos++];	//如果左边数组中还有剩余则将其复制到中间数组中 
	while(R_pos<=R_end) temp[temp_pos++]=a[R_pos++];	//如果右边数组中还有剩余则将其复制到中间数组中 
	for(int i=0;i<num;i++,R_end--) a[R_end]=temp[R_end];	//因为递归调用,在a中的绝对位置不好确定 
}

1.7 快速排序

实现原理: 快速排序和归并排序一样也是利用了分治地思想,将大问题不断转化成小问题解决,快排也是属于比较排序,不同的是快排比较的不是单个元素而是数据段。

算法描述:我先选取一个枢纽元作为中间元素,不大于它的元素放左边,不小于它的元素放右边,因为我进行排序的是一个随机数数组,所以我选取了每次进行排序的第一个元素为枢纽元,当排序的元素不随机时可以使用三中值分法选取枢纽元。当元素分割完成后,将枢纽元放入左右数组段的中间位置,再在左右数组段中递归进行排序,当待排序元素只有一个时即完成排序。

void QuickSort(ElementType a[],int begin,int end){
	if(begin>end) return ;				//只有一个元素无需再排序
	int i=begin,j=end;
	ElementType temp=a[begin],t;  /*因为本次排序的是随机数数组,数据随机,
所以可以直接用第一个元素作枢纽元,否则可以用三中值分法确定枢纽元*/ 
	while(i!=j){		//i等于j时说明两边元素已分好 ,结束循环 
/*两个循环顺序不能反,第一个循环每次寻找左边中大于枢纽元的元素下标,
第二个循环每次寻找右边中小于枢纽元的元素的下标*/		
		while(a[j]>=temp&&j>i) j--;	 
		while(a[i]<=temp&&j>i) i++;	
		if(i<j){		//显式地写出避免调用交换函数,提高排序速度 
			t=a[i];
			a[i]=a[j];
			a[j]=t;
		}
	}	/*以下两步是使枢纽元移动到合适位置,因为循环完成后左边的元素
小于等于枢纽元,右边的元素大于等于枢纽元,枢纽元即可不用再进入排序*/ 
	a[begin]=a[i];
	a[i]=temp;
	QuickSort(a,begin,i-1);	//对枢纽元左侧元素排序 
	QuickSort(a,i+1,end);				//对枢纽元右侧元素排序 
}

1.8 计数排序

实现原理:不同于比较比较排序每次都是通过元素间的比较来进行排序,简单来说计数排序是“算”出来的。

算法描述:为了避免空间的过多浪费,先遍历一遍待排序数组,找出其中的最大值和最小值,则计数数组的长度等于最大值减最小值加1(数组从0开始计数),将计数数组置0。然后遍历待排序数组,待排序数组的各个位置上的元素与最小值的差值即为该元素在计数数组中的对应位置,每遇到待排序数组的一个元素则在计数数组中其对应位置单元上加1,对应单元的数值对应待排序数组中这个数出现了几次。待排序数组遍历结束再遍历计数数组,将计数数组各个单元上代表的元素依次覆盖待排序数组,排序即完成。

void CountSort(ElementType a[],int length){
	int i,max=a[0],min=a[0];
	for(i=0;i<length;i++){
		if(a[i]>max)max=a[i];
		if(a[i]<min)min=a[i];
	}
	int tmp_length=max-min+1;
	//ElementType b[tmp_length];
	memset(b,0,sizeof(b));	//快速置0 
	for(i=0;i<length;i++) b[a[i]-min]++;	//a[i]与min元素的相对位置 
	for(i=0;i<tmp_length;i++) for(;b[i]>0;b[i]--) a[i-1+b[i]]=min+i;
}

1.9 基数排序

实现原理:计数排序也时算出来的,其利用了“桶子”来储存元素,最后将桶内的元素倒出来。

算法描述:基数排序总共执行最高位轮,每次从最低位开始比较一直到最高位,元素不足的用0补齐,然后用一个数组记录各个桶子中的元素个数,每次按顺序放入各个桶子中,再按从低到高的顺序,将各个桶子中的元素按照先入先出的顺序依次覆盖原数组,因为当最高位相同时其后面的各个位都是按照从低到高的顺序放入桶中的,桶中的元素先入先出,所以当执行完最高位轮后,排序即完成。

void RadixSort(ElementType a[],int length){
	int n,times=MaxDigit(a,length),order[10]; /*times 表示最大值有几
	位,总循环多少次,order数组用来记录每一行上的元素个数	*/ 
	//ElementType bucket[10][length];		//临时数组 
	memset(bucket,0,sizeof(bucket));
	int i,j,k,digit=1,lsd;		/*digit表示当前比较位,lsd表示当前元素
	在比较位上的值,lsd表示从最低位开始比较*/ 
	for(n=0;n<times;n++,digit*=10){
		memset(order,0,sizeof(order));	//每次循环都要将order数组置0 
		for(i=0;i<length;i++){
			lsd=(a[i]/digit)%10;		//获取当前元素在当前比较位上的值 
			bucket[lsd][order[lsd]++]=a[i]; /*行是当前比较位上的值从0-9,
			order[lsd]的值则是比较位为lsd的元素的个数*/ 
		}
		for(i=0,k=0;i<10;i++){		//将每次排序后的数组覆盖原数组 
			if(order[i]){						//如果当前位上有元素 
			//按照从低位到高位,从前到后依次覆盖原数组 
				for(j=0;j<order[i];j++,k++) a[k]=bucket[i][j];	
			}
		}
	}
}
int MaxDigit(ElementType a[],int length){
	int i;
	ElementType max=a[0];
	for(i=0;i<length;i++) if(a[i]>max) max=a[i];
	for(i=0;max>0;max/=10,i++);
	return i;
}

1.10 桶排序

实现原理:桶排序利用了“桶子”先对数据进行一次划分,然后对桶内元素进行排序,最后将元素倒出来。

算法描述: 桶排序其实就是将元素按一定范围分割,然后装到一个个桶子中,桶子是有序的,再将桶子中的元素进行排序(对桶子中的元素可以使用其他排序方法,如快排,归并,插入,冒泡等,也可以递归地把元素再分割到一个个更小地桶子中直到每个桶子中最多只有一个元素,然后回溯即可),最后将桶子按照从低到高的顺序将元素倒出来排序即完成。

基于插入排序的桶排序:

void BucketSort(ElementType* a,int length){
	/*ElementType temp[length/100][100];	 当数组太大时在栈区开辟会导致栈溢出
故在全局变量区声明数组*/ 
	ElementType min=a[0];
	int i,j,pos;
	int n[length/100];			//统计每个桶子中地元素个数 
	memset(n,0,sizeof(n)),memset(temp,0,sizeof(temp));	//将数组快速置0 
	for(i=0;i<length;i++) if(a[i]<min) min=a[i];	//得到最小元素 
	//将元素放到对应的桶子中去 
	for(i=0;i<length;i++) temp[(a[i]-min)/100][n[(a[i]-min)/100]++]=a[i];
	for(i=0,pos=0;i<length/100;i++){
		if(n[i]){
			InsertionSort(temp[i],n[i]);	//对每个桶子中的元素排序 
			for(j=0;j<n[i];j++) a[pos++]=temp[i][j];	//按顺序将桶子中的元素倒出来 
		}
	}
}
void InsertionSort(ElementType *a,int length){
	int i,j;
	ElementType temp=0;
	for(i=1;i<length;i++){
		temp=a[i];
		for(j=i;j>0&&a[j-1]>temp;j--) a[j]=a[j-1];
		a[j]=temp;
	}
}

2、 基本排序算法的时间、空间复杂度分析

注:稳定性是指相同的元素排序后其相对位置有没有改变,若不变则排序算法为稳定的。

在这里插入图片描述

3、各种排序算法的运行实例对比

  以下测试均在本人电脑下进行,电脑配置为,cpu AMD锐龙 54600U 6核,运行内存 16G,运行时间均为10次取平均值。数据样本的选取,我选取了1-N的随机数进行排序,N的取值为10000,100000,1000000,10000000,随机数数组在函数外创建。
生成随机数组的函数:

void Randomize(ElementType a[],int length){
	memset(a,0,sizeof(a));		//快速置0
	srand((ElementType)time(NULL));
	for(int i=0;i<length;i++) a[i]=i+1;
	for(int i=length-1;i>0;i--) Swap(&a[i],&a[rand()%i]);
}
void Swap(ElementType* x,ElementType* y){
	ElementType temp=*x;
	*x=*y;
	*y=temp;
}

运行时间对比表:

在这里插入图片描述


总结

  排序算法大致可以划分为内部排序和外部排序,比较排序和非比较排序,每种算法又有各自的特点。
  冒泡排序,选择排序,插入排序,希尔排序都属于基于比较的内部排序算法,其实现思想也比较相似,冒泡排序使得数据像泡泡浮出水面一样每次达到有序队列末尾;选择排序则是在冒泡排序的基础上使用了标志变量记录每趟中的最值下标避免了昂贵的交换开销;插入排序与冒泡排序不同的是每次向前比较为当前元素找到合适的位置进行插入,插入排序执行的操作次数相对于冒泡排序较少;希尔排序是率先突破平方级平均时间复杂度的排序算法之一,排序的本质就是消除逆序数,希尔排序的比较间隔一直缩小到1为止,每一趟可以消除更多的逆序数,这也是其能突破平方界限的原因。
  堆排序也是基于比较的内部排序算法,其主要用到了大根堆的性质,根节点元素为最大元素,堆排序是时间和空间复杂度都比较好的一种排序算法。快排也是基于比较的内部排序算法,归并排序则是基于比较的外部排序算法,它们都是使用了递归中的分治思想,不断将数据段划分成更小的数据段,直到其中只有一个元素为止,不同的是快排是每次都确定了枢纽元的位置,然后在其左右数据段中递归进行排序,无需回溯。归并排序则是先一直划分到数据段中只有一个元素为止,然后开始回溯,不断将两个数据段进行合并一直到最开始的两个数据段排序完成为止。
  计数排序,基数排序,桶排序都是非比较的外部排序算法,其都用到了“桶子”的思想,都是以空间换时间的排序算法。它们的区别则是,计数排序使用了最多的桶子,将这些元素装到对应的桶子中去,每个桶子只装一个元素;基数排序则用到了进制个数的桶子,如10进制用到10个桶子,16进制使用16个桶子,从最低位一直比较到最高位,每次将元素按各个位上的数字放入对应的桶中;桶排序则是根据需求划分桶子个数,桶排序要求的数据最好是均匀分布在各个区间,将数据放入其对应的桶子中,然后在桶子内部使用其他排序算法完成内部排序;它们最后都是把各个桶子按从小到大的顺序讲其内的元素全部倒出来即可完成排序。
总的来说,排序算法之间有相对优劣之分,但无绝对优劣之分。只能说根据需求,在不同的使用场景下使用合适的排序算法,或结合使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值