线性时间排序--计数排序、基数排序、桶排序


之前总结的都是通过比较方法进行排序的算法,我们知道,通过比较排序算法平均时间复杂度最多为O(lgN)。

这篇文章来分析一下非比较的线性时间排序方法,计数排序,基数排序,桶排序。


一:计数排序

正如它的名字,计数排序是通过计算待排序元素小于等于该元素的次数这个属性,然后利用该属性将元素以次数为下标挪入另外的数组,不能原地挪动,就得到了排序后的结果。

代码如下:
void counting_sort(int* arr, int* result, const int size, const int k)
{
    int i = 0;  
    vector<int> count(k+1, 0); 
    
    for(i=0; i<size; ++i)
        ++count[arr[i]];
    for(i=1; i<=k; ++i)
        count[i] += count[i-1];
    for(i=size-1; i>=0; --i){
        result[count[arr[i]]-1] = arr[i];   //注意下标-1
        --count[arr[i]];
    }   
}

时间复杂度为O(n),空间复杂度,O(n+k)
注意:
>>>1.计数排序一共使用了两个额外数组。一个用来存放结果即result数组,一个用来计数count数组。result数组是用来存放结果的,自然它的大小和原数组arr的大小是完全一致的。而count数组使用来对每个元素计数的,它的下标就是原数组中的元素,所以它的大小由原数组中最大值决定,大小是最大值max+1,这里我们通过外部参数传入,它就是k。这么做是因为它需要支持最大值的计数。
>>>2.每次计数前需初始化,这是所有counter共同遵循的特征。上面代码中vector已经初始化过了。
>>>3.对每一个原数组元素,它现在是count数组的下标,所以count[arr[i]]表示它出现的次数,可能是重复出现的。
>>>4.我们需要将count数组中内容一个一个累加起来,每一个当前坐标对应的值,表示小于等于它的坐标(原数组值)的元素数目。
>>>5.倒着for循环是为了 保证排序的稳定性。因为我们倒着回去,并且会将count[arr[i]减一,正好是后面arr[i]对应未减一前的count值,所以后面的元素由于count值大会被放在后面。
>>>6. 千万要注意赋给result时下标要减1,因为count数组中元素的最小值是1,不可能某个arr[i]元素在count中出现了0次。result的下标最小值为0,所以需要减1。否则会越界。

测试文件:
int main()
{
    int array[] = {3, 9, 12, 7, 6, 15, 4, 3, 2, 1}; 
    int len = sizeof(array) / sizeof(len);
    int result[len];
    std::fill(result, result+len, 0); 
    counting_sort(array, result, len, 15);
    for(auto i : result)
        cout<<i<<' ';
    cout<<endl;
    return 0;
}


二:基数排序

基本思想:
将待排序数组中的每组关键字依次进行桶分配。比如对三位数排序,我们先分配十个桶,分别装数字0~9。我们先按个位吧所有数放入相应的桶中,然后按桶序0~9在吧它们输出。然后按十位把他们再次放入桶中,按桶序再次输出。重复多次,得到就是有序数组。

我们可以利用队列数组来轻易实现基数排序的桶集合。每次push进去,然后按桶下标在pop出来,重复操作就完成排序。

看一道题:

对于一个int数组,请编写一个基数排序算法,对数组元素排序。

给定一个int数组A及数组的大小n,请返回排序后的数组。保证元素均小于等于2000。

测试样例:
[1,2,3,5,2,3],6
[1,2,2,3,3,5]
代码如下:
class RadixSort {
public:
    int* radixSort(int* A, int n) {
    	queue<int> bucket[10];
        for(int k=1; k<=1000; k*=10){  //d限制
            for(int i=0; i<n; ++i)   //分配
                bucket[A[i]/k%10].push(A[i]);
            
            int index = 0; 
            for(int i=0; i<10; ++i){  //收集
                while(!bucket[i].empty()){   
                	A[index++] = bucket[i].front();
                	bucket[i].pop();
                }
            }
        }
        return A;
    }
};
它的时间复杂度为O(d(n+radix)),radix是基数,其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。 空间效率:O(rd+n)约等于O(n)。
稳定性:稳定。

三:桶排序

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

假如待排序列K= {49、 38 、 35、 97 、 76、 73 、 27、 49 }。这些数据全部在1—100之间。因此我们定制10个桶,然后确定映射函数f(k)=k/10。则第一个关键字49将定位到第4个桶中(49/10=4)。依次将所有关键字全部堆入桶中,并在每个非空的桶中进行快速排序。

有一种说法桶排序不是一种实现,而是一种思想,比如基数排序就是用了bucket,我认为它也可以算作一种桶排序。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值