O(n)时间复杂度排序算法(桶、计数、基数排序)总结

概述

桶、计数、基数排序算法中,除了桶排序在桶粒度大于1时要通过比较排序外,其他两种排序都不需要使用比较就能使数列完成排序,这也是为什么时间复杂度可以达到线性O(n)的原因。


桶排序

桶排序的求解思路为找出数列中最小和最大值,按照同等大小的范围从最小到最大值闭区间的数值范围内垂直切分为一定个数的子区间,然后把数列内的值依次取出放到合适的子区间内,各子区间再分别对各自区间内的数值进行排序,最后拼接所有子区间的数值就完成了排序。

假设桶排序内各个子区间内的排序算法使用的是快速排序,快速排序的时间复杂度为O(nlogn),那么桶排序的时间复杂度O(n)又是怎么算出来的呢?

假设n个数据能均匀的拆分到m个桶中分别进行快速排序,也就是说分配到每个桶内的数值个数e = n / m,每个桶内排序使用的时间复杂度公式就是O(e * log(n/m)),总的时间复杂度为O(m * e * log(n/m)),转化一下就是O(nlog(n/m)),如果m越近似n,那么log(n/m)的值就越小,也就越接近O(n)。

 桶排序是稳定排序吗?

如果数据分配到各个子区间后每个桶的粒度都为1,那么肯定是稳定排序;否则,取决于桶排序算法的各个子区间使用的是不是稳定排序,比如结合使用的是快速排序,那么桶排序就不是稳定排序,如果使用的是插入排序,那么桶排序就是稳定排序。

桶排序是原地排序吗? 

桶排序将数列分配到各个子区间的时候,各个子区间内需要开辟额外的空间来临时存储待排序数据,所以不是原地排序,空间复杂度为O(n)。

 桶排序这么优秀,为什么不直接用桶排序代替其他非线性时间复杂度的排序算法?

桶排序的最好情况时间复杂度为O(n),如果数据分配到桶的分布情况极度倾斜,比如所有数据都分配到了一个桶,那么这时的时间复杂度就是最坏情况,至于用O表示为多少,就需要看桶排序结合的排序算法的时间复杂度是什么了,如果是快速排序,那么就是O(nlogn),插入排序则是O(n^2)。

如果用桶排序的数列需要使用的时间复杂度近似最坏情况,那么还不如直接选择相关合适的O(nlogn)的排序算法,比如快速排序,因为桶排序不是原地排序,需要额外消耗内存空间。

桶排序适用于什么场景?

数据在最小和最大值闭区间内分布较为均匀。

 桶排序结合快速排序C语言版本实现如下:

void quickSortFunc(int arr[], int left, int right){
	int pivotValue;
	int i, j;
	int temp;
	if(left >= right){
		return;
	}
	pivotValue = arr[right];
	i = j = left;
	while(j < right){
		if(arr[j] < pivotValue){
			temp = arr[j];
			arr[j] = arr[i];
			arr[i] = temp;
			i++;
		}
		j++;
	}
	arr[right] = arr[i];
	arr[i] = pivotValue;

	quickSortFunc(arr, left, i - 1);
	quickSortFunc(arr, i + 1, right);
}

void quickSort(int arr[], unsigned int size){
	quickSortFunc(arr, 0, size - 1);
}

void bucketSort(int arr[], unsigned int size){
	int max, min;
	int i, j, k, n;
	int num;
	int part;
	int **tmpArr;
	int* indexArr, * tmpSubArr, * lenArr;
	if(size < 2){
		return;
	}
	max = arr[0];
	min = arr[0];
	for(i = 1; i < size; i++){
		if(arr[i] > max){
			max = arr[i];
		}
		if(arr[i] < min){
			min = arr[i];
		}
	}
	if(size > 100){
		num = 100;
	}
	else{
		num = size;
	}
	tmpArr = (int**)malloc(num * sizeof(int*));
	indexArr = (int*)malloc(num * sizeof(int));
	lenArr = (int*)malloc(num * sizeof(int));
	part = ((max - min) + 1) / num;
	for(i = 0; i < num; i++){
		tmpArr[i] = (int*)malloc(part * sizeof(int));
		indexArr[i] = 0;
		lenArr[i] = part;
	}
	
	for( i = 0; i < size; i++){
		j = num - 1;
		while(j > 0){
			if(arr[i] >= min + j * part){
				break;
			}
			j--;
		}
		n = lenArr[j];
		if(indexArr[j] >= n){
			tmpSubArr = tmpArr[j];
			lenArr[j] = n * 2;
			tmpArr[j] = (int*)malloc(sizeof(int) * lenArr[j]);
			for(k = 0; k < n; k++){
				tmpArr[j][k] = tmpSubArr[k];
			}
			free(tmpSubArr);
		}
		tmpArr[j][indexArr[j]] = arr[i];
		indexArr[j]++;
	}
	for(i = 0; i < num; i++){
		if(indexArr[i] > 1){
			quickSort(tmpArr[i], indexArr[i]);
		}
	}
	i = 0;
	k = 0;
	while(i < num){
		if(indexArr[i] > 0){
			for(j = 0; j < indexArr[i]; j++){
				arr[k] = tmpArr[i][j];
				k++;
			}
		}
		free(tmpArr[i]);
		i++;
	}
	free(tmpArr);
	free(indexArr);
	free(lenArr);
}

计数排序

计数排序的求解思路如下,假设要排序数列Q,首先查找数列Q的最小和最大值,创建一个“最大值 - 最小值 + 1”的数组A,初始化数组A内所有元素值为0,然后依次取出数列Q的n个数值,以“数值 - 最小值”的差值作为数组A下标,将数组A对应下标的值加1,遍历完数列Q后,数组A内就统计出了数列Q里的各个不同的数值分别有相同值的个数。然后从数组A的第二个下标开始遍历,依次累加前一个下标的值就能算出小于等于当前下标的元素共有多少个。最后,创建和数列Q大小一样数组B,再进行数列Q倒序遍历,依次取出一个值x,用“x - 最小值”的结果作为下标访问数组A的对应下标的值y,用“y - 1”作为数组B的下标插入值x,然后将y减1,接着数列Q的倒序遍历前进一步重复这个逻辑,最终,数组B内排列的数值就是数列Q的有序版本,且是稳定排序。

求解过程中没有用到比较逻辑,时间复杂度为4n,用O表示法省略系数,即O(n)。

计数排序不是原地排序,最坏情况空间复杂度为O(2n),最好空间复杂度为O(n)。忽略系数,空间复杂度为O(n)。

计数排序适用于什么场景?

数据范围小于等于数据大小的场景,且范围内的数值分布较为均匀,数据范围 = 最大值 - 最小值 + 1。

 计数排序的C语言版本实现如下:

void countingSort(int arr[], unsigned int size){
	int max, min;
	int i;
	int *arrA, *arrB;
	if(size < 2){
		return;
	}
	max = min = arr[0];
	for(i = 1; i < size; i++){
		if(arr[i] < min){
			min = arr[i];
		}
		if(arr[i] > max){
			max = arr[i];
		}
	}
	arrA = (int*)malloc((max - min + 1) * sizeof(int));
	for(i = 0; i < max - min + 1; i++){
		arrA[i] = 0;
	}
	for(i = 0; i < size; i++){
		arrA[arr[i] - min]++;
	}
	for(i = 1; i < max - min + 1; i++){
		arrA[i] += arrA[i - 1];
	}
	arrB = (int*)malloc(size * sizeof(int));
	for(i = size - 1; i >= 0; i--){
		arrB[arrA[arr[i] - min] - 1] = arr[i];
		arrA[arr[i] - min]--;
	}
	for(i = 0; i < size; i++){
		arr[i] = arrB[i];
	}
	free(arrA);
	free(arrB);
}

基数排序

基数排序其实是计数排序的一种特殊场景的升级版应用,它的求解思路是如下,假设把要排序的数列里的每一个数值都看成是一行数值,那么首先将要排序的一行行数值右对齐,然后取所有行数值的右起的第一位的值进行计数排序,接着拿排序的结果取所有行数值的右起的第二位的值进行计数排序,接着再拿最新排序的结果取所有行数值的右起的第三位的值进行计数排序,以此类推,最终要排序的所有行数据变成有序的。某些行数值位数的长度可能不一样,所以较短的数值相比最长位数的数值左边看起来就空缺了,每次取所有行数值右起第n位时如果某些行的右起第n位为空,那么就用0作为本次的取值即可。

基数排序适用于什么场景?

所有数值的相同位的值的数值范围(最小到最大值的差加1)小于等于数据大小,且位数据分布较为均匀。

举个例子,假设要排序的数值如下:134,23,780,12345,如何用比较合适的线性时间复杂度O(n)的排序算法将它们排序呢?

我们分析一下这组数值的特点,最小值为23,最大值为12345。

先看看适不适合桶排序,按照桶排序,数值个数n小于一定数量,直接分配n个桶,第i(i从1开始)个桶涵盖的数值范围依次为[ “最小值 +(最大值 - 最小值 + 1)/ n * (i) - (最大值 - 最小值 + 1)/ n”, “最小值 +(最大值 - 最小值 + 1)/ n * (i)”),所以划分后的桶的数值范围分别为:[23, 3103),[3103, 6183),[6183, 9263),[9263, ∞],然后把这组数值按每个桶的数值范围放置到合适的桶内,根据规则,前三个数值都会放置到第一个桶,最后一个值会放到最后一个桶,中间两个桶为空,数据倾斜得比较严重,如果按照这个比例把数据大小放大几十倍,那么很明显这种特点的数据用桶排序来排序时间复杂度接近于最坏情况时间复杂度,所以这种方案不太好。

那再看看计数排序,计数排序的适用场景是数值范围小于等于数值个数大小,且数值范围内的数值分布较为均匀的情况,数值范围 = 最大值 - 最小值 + 1,这个例子的数值个数为4个,数值范围为12345 - 23,数值范围远大于数值个数,所以也不适合。

那最后再看看基数排序,基数排序是按所有数值的相同位倒序遍历排序的,因为例子的数值是整数,相同位的数值范围取值0 - 9,所以位值范围为10,数据大小为4个,因为排序的数值为整数,即使数据大小放大几十倍,位值的数据范围还是0-9,所以如果在这三种线性排序算法里选择一种来排序例子的数值,基数排序最为合适。

升序排序过程,这是数值的初始排列:

右起第5位右起第4位右起第3位右起第2位右起第1位
  134
   23
  780
12345
根据右起第1位的进行计数排序后:
右起第5位右起第4位右起第3位右起第2位右起第1位
  780
   23
  134
12345
根据右起第2位的进行计数排序后:
右起第5位右起第4位右起第3位右起第2位右起第1位
   23
  134
12345
  780
根据右起第3位的进行计数排序后,位空缺的补零:
右起第5位右起第4位右起第3位右起第2位右起第1位
  023
  134
12345
  780
根据右起第4位的进行计数排序后,位空缺的补零:
右起第5位右起第4位右起第3位右起第2位右起第1位
 0023
 0134
 0780
12345

因为右起第4位排序时就只有一个数值的位值有值,所以排序到此就可以结束了,这时数值就是有序的了,是不是很神奇?

 基数排序是计数排序的升级版应用,因为计数排序是稳定排序,所以基数排序也是稳定排序。

因为计数排序不是原地排序,所以基数排序也不是原地排序,忽略系数,空间复杂度为O(n)。

忽略系数,时间复杂度为O(n)。

不同数值类型的取位方法不同,不具有普适性,所以在此就不贴代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值