十大排序②:桶、计数与基数排序

十大排序①:插入、选择和交换排序

桶排序

Bucket sort适用于待排序数据值域较大但分布比较均匀的情况。

桶排序的关键在于桶:

  1. 设置一些数组作为空桶
  2. 数据源按照一定规律放到对应桶里
  3. 对桶内元素进行排序(显然在桶空时是不需要的)
  4. 按一定顺序将每个桶的桶内元素拿出放回原先的数据序列里

关于4要说的是,由于按照一定规律(具有某一共同性质)放桶里,这样桶与桶之间是有某些规律的,而具有某些性质的元素要放在序列的前面。


比如说你有60个数,这些数均匀地分布在1~100之间,你就可以使用10个桶。

显然每个桶装的数据范围分别是 [ 1 , 10 ] [1,10] [1,10] [ 11 , 20 ] [11,20] [11,20] [ 21 , 30 ] [21,30] [21,30] [ 91 , 100 ] [91,100] [91,100]

然后就开始分别在每个桶内排序,让它们变得有序,之后再从 [ 1 , 10 ] [1,10] [1,10] [ 11 , 20 ] [11,20] [11,20] [ 21 , 30 ] [21,30] [21,30] [ 91 , 100 ] [91,100] [91,100]将这些桶元素取出排列好。


所以先有个桶,这里实现是vector,用链表也不是不行,甚至数组也行。只不过变长的可能更加灵活。定长的话可能有某个桶满的情况。

#include<vector>
vector<int> bucket[10];

然后是给桶内元素排序的功能,把之前的直接插入排序代码稍微改改适配vector<int>可以了,这里注意参数是引用的:

void insertSort(vector<int>& arr){
	// 直接插入排序 
	int i, j, temp;
	for(i = 1; i < arr.size(); i++){
		temp = arr[i];
		j = i - 1;
		for(; j >= 0 && temp < arr[j]; --j){
			// 待排序元素左侧大于它的右移
			arr[j + 1] = arr[j]; 
		}
		arr[j + 1] = temp;
	}
}

当然,也可以直接用

#include <algorithm>
//以对bucket[0]排序为例
sort(bucket[0].begin(),bucket[0].end());

接下来就是对桶内元素的分配了,对于N个元素均匀地(什么?怎么有种散列的感觉? )分到M个桶里,显然每个桶装N/M个元素。

一般对于 N N N个元素有桶的个数 M M M符合 M 2 = N M^2=N M2=N时最好的。我们只需要在获取到该序列最大和最小元素后,对于待排序序列的元素 α \alpha α ( α − m i n ) / M (\alpha - min) / M (αmin)/M将之分配到具体的桶。

为什么要减 m i n min min呢?因为我们希望最小的元素会被分配到第一个桶里。这是一种偏移策略。

每个桶装的元素范围,定义了gap来表示,显然第一个桶(下标为0的)装的是 [ 0 , g a p ) [0,gap) [0,gap)范围内的。当然,这是偏移之后的零,偏移前的显然是 [ m i n , m i n + g a p ) [min, min+gap) [min,min+gap)

代码

#include<iostream>
#include<vector>
#include<math.h>
using namespace std;


void insertSort(vector<int>& arr){
	// 直接插入排序 
	int i, j, temp;
	for(i = 1; i < arr.size(); i++){
		temp = arr[i];
		j = i - 1;
		for(; j >= 0 && temp < arr[j]; --j){
			// 待排序元素左侧大于它的右移
			arr[j + 1] = arr[j]; 
		}
		arr[j + 1] = temp;
	}
}

void bucketSort(int* arr, int n){
	
	int i, min, max;
	min = max = arr[0];
	for(i = 1; i < n; ++i){
		// 寻找最小和最大 
		if(min > arr[i]){
			min = arr[i];
		}
		if(max < arr[i]){
			max = arr[i];
		}
	}
	// 整m个桶 
	int m = ceil(sqrt(max - min));	
	vector< vector<int> > bucket(m);
	// 每个桶之间的间距 
	int gap = ceil((max - min) *1.0 / m);
	// 装桶操作 
	for(i = 0; i < n; ++i){
		bucket[(arr[i]-min) / gap].push_back(arr[i]);
	}
	// 对每个桶依次排序 
	for(i = 0; i < m; ++i){
		insertSort(bucket[i]);
	}
	// 依次取出每个桶内元素在原数组排好 
	i = 0;
	while(i < n){
		for(int j = 0; j < m; ++j){
			for(int k = 0; k < bucket[j].size(); ++k){
				arr[i] = bucket[j][k];
				++i;
			}
		}
	}
}
// 主程序,测试代码
int main(){
	int array[17] = {3, 39, 6, 32, 61, 20, 8, 13, 67, 11, -1, -6, 77, 39, 55, 90, 67};
	bucketSort(array, sizeof(array)/sizeof(int)); 
	for(int i = 0; i < sizeof(array)/sizeof(int); ++i){
		cout<<array[i]<<" ";
	}
}

当然,为了使用ceil()sqrt(),需要包含math.h头文件。

计数排序

void countingSort(int* arr, int n){
	// 计数排序
	int i, max, min;
	max = min =  arr[0];
	for(i = 1; i < n; ++i){
		// 寻找最小和最大值 
		if(min > arr[i]){
			min = arr[i];
		}
		if(max < arr[i]){
			max = arr[i];
		}
	}
	// 待开辟数组的长度size 
	int size = max - min + 1;
	int* temp = new int[size];
	// 数组元素全部置零 
	for(i = 0; i < size; ++i){
		temp[i] = 0;
	}
	// 存入统计 
	for(i = 0; i < n; ++i){
		++temp[arr[i] - min];
	}
	// 取出填入原数组 
	i = 0;
	for(int j = 0; j < size; ++j){
		while(temp[j]>0){
			arr[i++] = j + min;
			--temp[j];
		}
	}
	// 释放申请的空间 
	delete[] temp;
	temp = NULL;
}

计数排序中,temp数组的下标index与待排序的关键字key存在着关系index=f(key),而temp数组的元素temp[index]则代表key出现的次数。

其中f(key)是一个映射函数,将一般是将key的最小值映射到下标0上。也就是在遍历一遍待排序序列取得最小值min后对任意key应当存于temp数组的index位置有如右侧关系:index = key - min

显然,index是非负整数,因此多数情况下key亦应当是整数,不过凡事也不绝对,也不是说就不存在某个f(key)可以将key映射到非负整数上,这个应当据实际需要分析。

由于用到了空间换时间的思想,这种算法另行开辟的数组size = max - min若特别大,其空间复杂度也不小,这是应当注意的。

时间复杂度和空间复杂度都是 O ( N + s i z e ) O(N+size) O(N+size),其中size=max-min N N N是输入数据规模

基数排序

#include<vector>
using namespace std;

vector<int> bucket[10];

int getNthElement(int num, int n){
	// 获取右数第n位的数字,若没有则返回0 
	while(n > 1){
		num /= 10;
		--n;
	}
	return num % 10;
}

void radixSort(int* arr, int n){
	int i, max, loop = 0;
	max = arr[0];
	for(i = 0; i < n; ++i){
		arr[i]>max?max=arr[i]:max;
	}
	i = max;
	// 确定要进行loop轮操作,即待排序最大元素有loop位
	while(i > 0){
		i /= 10;
		++loop;
	}
	// 第a(a>=1)轮入+出桶(假如有)
	for(int a = 1; a <= loop; ++a){
		for(int j = 0; j < n; ++j){
			bucket[getNthElement(arr[j], a)].push_back(arr[j]);
		}
		i = 0;
		while(i < n){
			for(int j = 0; j < 10; ++j){
				for(int k = 0; k < bucket[j].size(); ++k){
					arr[i] = bucket[j][k];
					++i;
				}
				bucket[j].clear();
			}
		}
	}
}

基数排序,取index=1为右数第一位,并预先有10个桶bucket[10]

  1. i = 1
  2. 左到右扫描待排序序列
  3. 以右数第i位数字为分类依据,将数字分配到对应桶里,例如第i位数字为0,就放到bucket[0]
  4. 按照先进先出的顺序自左至右取出桶bucket[0]~bucket[9]的内容,将它们按取出顺序从index=0开始填充在原先数组里
  5. i+=1,检查是否满足条件i>所有待排序元素中最大元素的位数,若满足则终止,不满足则跳至1继续重复上述内容

对于上述过程的解释与强调如下:

为什么是10个桶?

  • 因为代表数字0~9,第i位数字始终是10个数字之一

为什么从左到右扫描待排序的序列

  • 结合后面从index=0开始填充在原先数组里,最后序列才是升序的

请务必按照先进先出的顺序取出每个桶的元素

  • 因为这也是一种“局部有序”。例如个位有序后在处理十位时,假设有一些十位均为1的数,由于个位小的在前面,导致带上十位之后,小的还是在前面。

不能使用基数排序处理带有负数的数据

时间复杂度 O ( n ∗ m ) O(n*m) O(nm),其中m是最大元素的位数

空间复杂度 O ( n + k ) O(n+k) O(n+k)

参考

OI百科-桶排序

【算法】排序算法之桶排序

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值