1、情景导入
当谈到“桶”的时候,你第一时间想到的是什么?是“木桶”、“水桶”、“酒桶”、还是“饭桶”?
但总之不管是什么桶,通过我们的常识就可轻易得知:“桶”是一个容器,一个有一定容量的容器,理论上来说它可以容纳容量比它小的物体。而这个物体,它可以是固体,也可以是液体,甚至连固液混合体也没有问题。
因而,桶的作用我们是母庸质疑的,就是用来装东西的一个物体。但让我们不妨假设一下当桶里装的为各种各样不同种类液体的时候,桶里会发生什么奇效,桶的作用会有什么改变~
2、算法思想
2.1、问题描述:
现有密度大小不同且互不相溶的5种液体l1、l2、l3、l4、l5,它们的密度大小均未知,但容量均为200ml,现有一个容量为1.5L并以200ml为刻度的水桶(假设水桶内壁与5种液体均不发生化学反应)与5个容量为250ml的烧杯b1、b2、b3、b4、b5,试设计一个方案将这5种密度大小不同的液体按密度依次从小到大的顺序依次分离烧杯b1、b2、b3、b4、b5。
2.2、问题解析:
对于这道题目,想必学过初中化学的同学应该都知道怎么做吧?把5种液体l1、l2、l3、l4、l5依次倒入水桶中,密度小的液体自然就浮在密度大的液体上面了,因此再将水桶中的液体依次以200ml为单位取出至烧杯b1、b2、b3、b4、b5,烧杯中的液体密度大小就从小到大排列了~
如下图所示,我们可以观察到,将任意五种密度大小不同且互不相溶(但体积相同)的液体倒入桶中,密度最小的液体总是处于区域1内,而密度次小的液体处于区域2内,密度中等的液体处于区域3内,密度次大的液体处于区域4内,密度最大的液体处于区域5内。
通过这个图是不是不禁让我们联想到了与计算机息息相关的数组呢?我们的数组也不正是如此吗?我们编程时一次申请了一块连续的数组空间,不就相同于取了一个桶出来吗?(如果有同学觉得桶的各个区域不可以实现随机取,那其实我也可以在桶的每个区域中钻个可以实现开关的“洞”)。
假设有一组数据待排序,我们假设用一个数组来解决。这个数组初始时我们假设每个内存单位都没有存数据,置为0,而当存了数据的内存单元,我们就将其置为一个正整数。我们利用数组的下标的有序性,将数据依次存入与数组下标相等的位置,并将该下标位置的内容加1。当我们将数据存储完毕之后,我们再从依次遍历数组,将下标位置上内容不为0的下标输出(若为2则输出下标2次),这样我们就能够从原先数据中得到一个有序序列。
比如有一组数据{2,1,9,7,5,6,3,7},考虑到最大的数为9,因而我们用一个容量为10的整型数组int a[10]来处理,根据上述步骤,
依次置 a[2] = 1, a[1] = 1, a[9] = 1,a[7] = 1,a[5] = 1, a[6] = 1,
a[3] = 1, a[7] = 2
再依次遍历数组,将下标位置上内容不为0的下标输出:
1,2,3,5,6,7,7,9 即可得到一个有序序列。
如果通过上面的例子你还觉得有一些模糊的地方,下面再让我们来看一道2018年计算机综合408的真题。
3、例题解析
3.1、问题描述:
41.(13分)给定一个含n(n>=1)个整数的数组,请设计一个在时间上尽可能高效的算法,找出数组中未出现的最小正整数。例如,数组{-5,3,2,3}中未出现的最小正整数为1;数组{1,2,3}中未出现的最小正整数时4.要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
(3)说明你所设计算法的时间复杂度和空间复杂度。
3.2、问题解析:
若未学习桶排序之前,我们使用普通的方法,我们可能会先想到其中的一个方法就是先把数组排个序,然后用动态规划的思想,设置一个变量minInteger,初值为1,依次与排序后的数组的每个值进行比较,当遇到相等的值时,则minInteger加1。
实现的源码如下:
#include<iostream>
#include<algorithm>
using namespace std;
int main(){
int *A;
int n;
cin >> n;
A = (int *)malloc(sizeof(int) * n);
for(int i = 0;i < n; i++){
cin >> A[i];
}
sort(A,A + n);
int minInteger = 1;
for(int i = 0;i < n; i++){
if(A[i] == minInteger) minInteger++;
}
cout << "\n未出现的最小正整数:" << minInteger << endl;
}
这个算法用到了C++中的sort()函数来对数组排序,时间复杂度为O(nlogn),遍历数组寻找最终minInteger的值,时间复杂度为O(n),所以时间复杂度总共为O(nlogn + n ) = O(nlogn),而未使用到辅助数组,因而空间复杂度为O(1)。
这样做看似没有什么大问题,但实则忽略了题目条件中时间上尽可能高效的条件,所以我们不妨试试以下的思路:
题目中要求算法时间上尽可能高效,因此可以采用空间换时间的方法。我们可以分配一个用于标记的桶数组B[n],用来记录A中是否出现了1~n中的正整数,B[0]对应正整数1,B[n-1]对应正整数n,初始化B中全部为0,由于A中含有n个整数,因此可能返回的值为1 ~ n + 1。当数组A中出现了小于等于0或者大于n的值时,会导致1 ~ n中出现空余位置,返回结果必然在1 ~ n中,因此对于A中出现了小于等于0或大于n的值可以不采取任何操作。
具体实现的源码如下:
#include<iostream>
using namespace std;
int main(){
int *A,*B;
int n;
cin >> n;
A = (int *)malloc(sizeof(int) * n);
B = (int *)malloc(sizeof(int) * n);
int i;
for(i = 0;i < n; i++){
cin >> A[i];
B[A[i] - 1] = 1;
}
for( i = 0;i < n; i++){
if(B[i] == 0) break;
}
int minInteger = i + 1;
cout << "\n未出现的最小正整数:" << minInteger << endl;
}
这个算法将数组A与数组B各遍历了一遍,且两次循环内操作步骤均为O(1)量级,因此时间复杂度为O(n),又因为额外使用了辅助数组B[n],则空间复杂度为O(n)
算法总结
a)、因为用桶排序来排序数量级为n的数据需要用到的辅助数组规模为n,因而空间复杂度为O(n)。
b)、排序过程中只需要将数量级为n的数据存入辅助数组中并遍历一遍辅助数组,因而时间复杂度为O(n + n) = O(n)。
综合上述两点,可以得知,桶排序只适用于对数据规模较小的数据排序,且为一种速度较快稳定性较好的排序方式。