另外一些排序算法:希尔排序、基数排序、桶排序

本文详细介绍了三种排序算法:希尔排序、基数排序和桶排序。希尔排序是插入排序的改进版,通过增量序列逐步排序;基数排序通过数位分配和收集实现稳定排序;桶排序则利用映射到桶内并排序,适合数据分布均匀的情况。三种算法各有优缺点,适用于不同的场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、希尔排序

希尔排序,其实属于一种特殊的插入排序,是简单排序算法里面的直接插入排序的改进版。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。希尔排序是基于插入排序的以下两点性质而提出改进方法的:插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率;插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。希尔排序先取一个小于n的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量为1,即所有记录放在同一组中进行直接插入排序为止一般的初次取序列的一半为增量,以后每次减半,直到增量为1。

1、代码

import java.util.Arrays;

public class Main{//希尔排序
	public static void main(String[] args){
		int[] a=new int[]{1,3,1,2,0,34,5,2,6,2,0,-3,-1,22,44,-11,0,-1,43,222,-2,-56,33,90,22};
		int n=a.length;
		int d=n;//增量
		while(true){
			d=d/2;//第一个增量为元素数量一半
			for(int x=0;x<d;x++)//对各个分组进行插入排序
				for(int i=x+d;i<n;i=i+d){//对其中某个分组进行插入排序
					int temp=a[i];
					int j;
					for(j=i-d;j>=0&&a[j]>temp;j=j-d)
						a[j+d]=a[j];
					a[j+d]=temp;
				}
			if(d==1)
				break;
		}
		System.out.println(Arrays.toString(a));
	}
}

2、时间和空间复杂度分析

希尔排序的增量选择对其性能有很大的影响。希尔排序的时间性能比插入排序有较大的改进。这是因为在希尔排序开始时增量较大,分组较多,每组的记录数目少,故各组内直接插入较快,后来增量di逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多,但由于已经按di-1作为距离排过序,使文件较接近于有序状态,所以新的一趟排序过程也较快。希尔排序的时间复杂度分析很难从数学上分析,但是平均来说当n较大时候,通过大量实验可以得到时间复杂度处于O(n^1.25)到O(n^1.6)之间。空间复杂度当然由于不需要用到额外的空间,因此是O(1)。希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法。

3、稳定性

希尔排序是非稳定的。因为在分组插入的过程中,相同的数有可能会被移动相对位置。

二、基数排序

1、算法

基数也就是一个数字系统的基,如十进制的基数就是10,二进制的基数就是2。基数排序也就是把关键字拆分成数字位,然后按数字位来给关键字排序。在这里,就以十进制作为例子。在这里,我们要先明确两个概念。最高位优先(Most significant Digit first)法,即MSD法和最低位优先(Least Significant Digit first)法,即LSD法有很大的区别。假设一个数字是十进制,有m位,从高位到低位分别为km,km-1,...,k1。对于MSD,先按照km排序分组,同一组中的记录,再按照km-1分组,然后继续这样迭代,最后再将各组连接起来,得到一个有序序列。对于LSD,先从k1开始分组,分完组后连接起来,再对k2分组,依次重复,最后得到一个有序序列。具体过程:
(1)假设原来有一串数值所示:73, 22, 93, 43, 55, 14, 28, 65, 39, 81
首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:

0

1 81

2 22

3 73 93 43

4 14

5 55 65

6

7

8 28

9 39

注意,分配桶子的时候如果该位上的值相同,其前后顺序不能打乱。

(2)接下来将这些桶子中的数值重新串接起来,成为数列:81, 22, 73, 93, 43, 14, 55, 65, 28, 39

接着再进行一次分配,这次是根据十位数来分配:

0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
(3)接下来将这些桶子中的数值重新串接起来,成为数列:14, 22, 28, 39, 43, 55, 65, 73, 81, 93
这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。

2、代码
public class Main{//基数排序
	public static void main(String[] args){
		int[] a=new int[]{1,3,1,2,0,34,5,2,6,2,0,3,1,22,44,11,0,1,43,222,2,56,33,90,22};
		int n=a.length;
		int d=maxbit(a);//d是最大位数
		//基数排序的一个注意的点是如何用简洁和尽量小的数据结构来作为桶。这里采用一个大小为n的数组tmp作为0-9一共10个桶的结合。而桶的分界线采用一个10个元素的计数器count数组来记录
		int[] count=new int[10];
		int[] tmp=new int[n];
		int radix=1;//用来记录处理的数位
		for(int i=1;i<=d;i++){
			for(int j=0;j<10;j++)
				count[j]=0;//每次分配前把计数器清零
			for(int j=0;j<n;j++){
				int k=(a[j]/radix)%10;
				count[k]++;
			}			
			for(int j=1;j<10;j++)//计数器位置记录
				count[j]=count[j-1]+count[j];
			for(int j=n-1;j>=0;j--){//放到桶里去
				int k=(a[j]/radix)%10;
				tmp[count[k]-1]=a[j];
				count[k]--;
			}
			for(int j=0;j<n;j++)//把桶里数据复制回原数组
				a[j]=tmp[j];
			radix=radix*10;
		}
		System.out.println(Arrays.toString(a));
	}
	public static int maxbit(int[] a){//求数组a中的数据的最大位数
		int n=a.length;
		int d=1;
		int p=10;
		for(int i=0;i<n;i++){
			while(a[i]>=p){
				p=p*10;
				d++;
			}
		}
		return d;
	}
}

3、时间和空间复杂度

(1)时间复杂度

可以发现,对于基数排序来说,他的效率和初始序列是否有序是没有关联的,也就是没有最好和最坏时间复杂度之说。仅对于LSD来说,假设有n个待排数字,最大位数是d,基数为r。外层的大循环要进行d次,而每次都对n个数字进行分配,同时基数的桶也要进行处理r次,因此很容易得到时间复杂度是O(d(n+r))。

(2)空间复杂度

很明显,需要一个额外的O(n+r)的空间。这个n是额外存储的数据,而r是用于划分桶边界的计数器。

4、稳定性

无论MSD和LSD都是可以实现稳定排序的,因为在算法中每次对某一位进行排序都没有改变其原来相同的数相对位置变化。但是,基数排序通常都是用LSD。这是因为用MSD的话,需要在每个分好的桶内再继续分桶。假设数字有d位,对于十进制来说,那意味着需要10^d个桶,这是极大的空间开销。而且MSD一般需要依靠递归来实现。而LSD的话,每次分完一位,直接组合起来再继续分桶,这就意味着总是有10个桶就能完成排序了。

5、适用范围(参考:https://www.onmpw.com/tm/xwzj/algorithm_116.html)

由于基数排序的时间复杂度是O(d(n+r)),因此他在一定程度上比那三个基于比较的排序算法的时间复杂度O(nlogn)要优秀。这是因为在某些情况下,d,r是常数,并不会有太大的变动,因此他的效率会很高。但是在一般情况下,d,r不能认为是个常数。我们以十进制r=10为例,假设整个序列中最大的数为N,则位数d=lgN(lg是以十为底的对数)。因此,在一般情况下,基数排序的时间复杂度为O(nlgN)。在N非常大,甚至大于n的情况下,基数排序的效率就比那三个最优的比较型排序算法要低了。这个时候,假设N<=n^c,这里c是一个常数。这种时候基数排序的时间复杂度就变为了O(nlgn)了。但是即使这样,也并不比比较型的排序算法更快。那这个时候,我们就需要考虑,能否令基数变大呢?如果基数r不是10,而是n,这样基数排序的时间复杂度就变为线性的O(n)了。这里就涉及到具体使用基数排序的时候的灵活优化了。

三、桶排序(参考http://blog.sina.com.cn/s/blog_667739ba0100veth.html)

1、思路

其实基数排序就是桶排序的一种。桶排序的思路是,假设有一组长度为n的待排关键字序列K[1,2,...,n]。首先将这个序列划分成m个的子区间(桶) 。然后基于某种映射函数 ,将待排序列的关键字k映射到第i个桶中(即桶数组B的下标 i),那么该关键字k就作为B[i]中的元素(每个桶B[i]都是一组大小为n/m的序列)。接着对每个桶B[i]中的所有元素进行比较排序(可以使用快排)。然后依次枚举输出B[0]....B[M]中的全部内容即是一个有序序列。基数排序,其实就是f(k)=k/10的映射函数,并且对每个桶不进行额外的排序操作。

2、性能分析

对n个关键字进行桶排序的时间复杂度分为两个部分:

(1) 循环计算每个关键字的桶映射函数,这个时间复杂度是O(n)。

(2) 利用先进的比较排序算法对每个桶内的所有数据进行排序,其时间复杂度为 ∑ O(ni*logni) 。其中Ni 为第i个桶的数据量。

很显然,第(2)部分是桶排序性能好坏的决定因素。尽量减少桶内数据的数量是提高效率的唯一办法(因为基于比较排序的最好平均时间复杂度只能达到O(n*logn)了)。所以,应该尽量做到:

(1) 映射函数f(k)能够将n个数据平均的分配到m个桶中,这样每个桶就有n/m个数据量。尽量做到平均分配,这样桶的数量就能尽量少。

(2) 尽量的增大桶的数量。极限情况下每个桶只能得到一个数据,这样就完全避开了桶内数据的“比较”排序操作。 当然,做到这一点很不容易,数据量巨大的情况下,f(k)函数会使得桶集合的数量巨大,空间浪费严重。这就是一个时间代价和空间代价的权衡问题了。当n=m时,即极限情况下每个桶只有一个数据时。桶排序的最好效率能够达到O(n)。

我们可以计算一下平均时间复杂度,对于n个数据,m个桶,平均每个桶有n/m个数据,而由于每个桶有n/m个数据,因此每个桶内进行排序的平均时间复杂度为(n/m)*log(n/m),因此平均时间复杂度为:

O(n)+O(m*(n/m)*log(n/m))=O(n+nlog(n/m)。

当然桶排序的空间复杂度是O(n+m)。如果输入数据非常庞大,而桶的数量也非常多,则空间代价无疑是昂贵的。此外,桶排序是稳定的。

3、桶排序和基数排序的区别

一般来说,基数排序的性能比桶排序要略差。但是,对比桶排序,基数排序每次需要的桶的数量并不多。而且基数排序几乎不需要任何“比较”操作,而桶排序在桶相对较少的情况下,桶内多个数据必须进行基于比较操作的排序。因此,在实际应用中,基数排序的应用范围更加广泛。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值