TOP

基数排序的可复用实现(C++11/14/17/20)(一)
2019-07-09 18:10:27 】 浏览:173
Tags:基数 排序 复用 实现 11/14/17/20

基数排序,是对整数类型的一种排序方法,有MSD (most significant digit)和LSD (least significant digit)两种。MSD将每个数按照高位分为若干个桶(按照我们常用的十进制,就是0-9,10个桶,这也是“基数”的由来),在每个桶内使用排序算法(如果也是MSD基数排序,就成了递归,出口在最低位),最后按顺序收集每一个桶,收集到的序列就是有序的。如果入桶和收集的过程能保证先入桶的元素先被收集,那么基数排序就是稳定的。 

而LSD则先按照最低位分组,然后按与入桶相同的顺序重新按更高位分组,直到最高位,最后收集到的序列也是有序的,同时也是稳定的。有图比较容易理解,可以参考相关文章。

基数排序的时间复杂度为O(P(N+B)),额外空间复杂度,LSD为O(N+B)(两个临时数组,见下),MSD最坏情况为O(P(N+B))(所有元素都相等,每次递归复制一次),其中P为位数,N为待排元素个数,B为桶的个数。

无论哪种基数排序,自始至终都没有比较过任何两个元素,原理在于整数的离散性。那么,这是不是意味着无法重载 operator< 的类也能用用基数排序呢?当然不能,无法比较的类的对象显然不能表达为整数(否则为什么无法比较),也就不能用基数排序算法。

基数排序的简单实现,可参考中文维基英文维基,以及其他相关文章。

 

但是我觉得吧,这些实现都很逊。不要误会,我不是针对哪个实现,我的意思是网上的各个实现都是垃圾。我们学习数据结构与算法的时候,不能忘记我们学习的目的,这些东西最终都是要用到实际开发中去的,而工程中当然不只有算法。作为一个优化合理就能在O(N)时间复杂度的情况下排完序的算法,基数排序的性能在有些情况下会比 std::sort 还要好(比如OJ不给编译器开优化的时候)。本文就是要实现一个优雅的、可复用的基数排序算法。实际上,算法还是基数排序,本文只是给基数排序做了一个好的接口。

回到基数排序的目的。基数排序什么时候可用呢?当每个待排元素可以分解成相同数量的可取有限个离散量的元素时可用,这些元素可取的离散量的数量可以不同。所以,基数排序可以用在很多类型上,实现起来,无非是每一次求当前“位”上的数的算法不同而已。这里的“位”已经是个抽象概念了,指的就是上述可取有限个离散量的元素。排序的每一轮分类时,只要把这些离散量映射到从0开始的连续整数上,然后插入连续存储的表中对应位置的容器中去(这个操作必须是O(1))。

所以,这个排序的接口应该包含:待排元素范围、迭代深度(对于MSD)或循环次数(对于LSD)、每一轮分类的基数(即基数排序的退化版桶排序中桶的数量),还有一个谓词,它应该接受待排元素和第几轮两个参数,并返回映射结果,范围为[0,基数-1]。理论上每一轮分类的基数可以不同,但是实现起来有些麻烦(实际上是因为我没想到,现在懒得改了),这里简化为所有的基数都相同。

接口长成这样:

1 template <typename It, typename Parser>
2 void radix_sort(It _begin, It _end, int _radix, Parser _parser, int _pass);

对于 It 类型的对象 iter ,[0,_pass)范围内的整数 pass , _parser 必须可以调用 _parser(*iter, pass) 并返回[0,_radix)范围内的整数

MSD因为需要递归,耗费大量空间,就不去实现了。LSD的实现不太复杂。大体上是先创建两个 _radix 长度的数组,每个元素都是带排序类型的 vector 容器,然后将范围内元素按最低位放到一个数组相应位置的 vector 的末端,之后在两个数组之间分组、收集,最后收集回原来的迭代器范围中。算法实现如下:

 1 #include <vector>
 2 #include <utility>
 3 
 4 template <typename It, typename Parser>
 5 void radix_sort(It _begin, It _end, int _radix, Parser _parser, int _pass)
 6 {
 7     auto begin = _begin;
 8     std::vector<std::vector<std::remove_reference_t<decltype(*_begin)>>> temp0(_radix), temp1(_radix);
 9     auto src = &temp0;
10     auto dst = &temp1;
11     for (; begin != _end; ++begin)
12         (*src)[_parser(*begin, 0)].push_back(*begin);
13     int pass = 1;
14     while (1)
15     {
16         for (const auto& v: *src)
17             for (const auto& i : v)
18                 (*dst)[_parser(i, pass)].push_back(i);
19         if (++pass == _pass)
20             break;
21         std::swap(src, dst);
22         for (auto& v : *dst)
23             v.clear();
24     }
25     for (const auto& v : *dst)
26         for (const auto& i : v)
27         {
28             *_begin = i;
29             ++_begin;
30         }
31 }

注意第8行, decltype(*_begin) 返回的是引用类型,需要用 remove_reference_t<T> 去除引用,也相当于 typename remove_reference<T>::type ,者需要C++14,后者需要C++11,都在 <type_traits> 中定义。

接口和实现都好了,这个函数如何使用呢?起始和尾后迭代器没什么好说的,STL中遍地都是,两个整数参数也很常规,关键在于 _parser 所属的类怎么写。最简单的,对于 int 类型,或者稍微广泛一些,对于所有内置整数类型,要怎么获得指定位上的数呢?

这还得先看怎么划分“位”。最容易想到的当然是十进制,但最高位取不到0-9,而且各类型的最高位的取值不统一,同时也不能很方便地获得循环次数,还有负数要考虑,又给最高位的问题引入了新的麻烦。计算机是二进制的,以2为基数,上述问题就不存在了,但效率太低。权衡了一下(一拍脑袋决定),我选择以16为基数,以4个bit为一位。

接下来就是负数的问题。想必你学导论或者学C的时候都学过整数的底层表示。对于带符号整数,最
基数排序的可复用实现(C++11/14/17/20)(一) https://www.cppentry.com/bencandy.php?fid=49&id=227443

首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇C++屌屌的观察者模式-同步回调和.. 下一篇BFS(三):双向广度优先搜索