我们什么时候应该使用Radix排序?

时间:2010-11-10 16:52:08

标签: performance algorithm sorting quicksort radix-sort

Radix sort似乎具有非常好的平均案例性能,即 O(kN)http://en.wikipedia.org/wiki/Radix_sort

但似乎大多数人仍在使用快速排序,不是吗?

12 个答案:

答案 0 :(得分:27)

Radix排序比大多数其他排序算法更难推广。它需要固定大小的键,以及将键分成几块的标准方法。因此它永远不会进入图书馆。

答案 1 :(得分:17)

根据您的评论编辑:

  • 基数排序仅适用于整数,固定大小的字符串,浮点和“小于”,“大于”或“词典顺序”比较谓词,而比较排序可以适应不同的顺序。
  • k可以大于log N。
  • 可以在适当的位置进行快速排序,基数排序效率会降低。

答案 2 :(得分:15)

这里的其他答案非常糟糕,他们没有举例说明实时使用基数排序

一个例子是使用倾斜DC3算法(Kärkkäinen-Sanders-Burkhardt)创建“后缀数组”。如果排序算法是线性时间的话,算法只是线性时间,并且基数排序在这里是必要和有用的,因为键是短的构造(3元组的整数)。

答案 3 :(得分:9)

除非你有一个巨大的列表或非常小的密钥,log(N)通常小于k,它很少高。因此,选择具有O(N log N)平均案例性能的通用排序算法并不比使用基数排序更糟糕。

更正:正如@Mehrdad在评论中指出的那样,上面的论点不合理:密钥大小是常量,那么基数排序是O(N),或密钥大小是k,然后快速排序是O(k N log N)。所以理论上,基数排序确实有更好的渐近运行时间。

在实践中,运行时将由以下术语主导:

  • 基数排序:c1 k N

  • 快速排序:c2 k N log(N)

其中c1>> c2,因为从较长的密钥中“提取”位通常是涉及位移和逻辑操作(或至少未对齐的存储器访问)的昂贵操作,而现代CPU可以在一次操作中将密钥与64位,128位或甚至256位进行比较。因此对于许多常见情况,除非N是巨大的,否则c1将大于c2 log(N)

答案 4 :(得分:8)

基数排序需要O(k * n)时间。但你必须问什么是K. K是“数字位数”(有点简单,但基本上就是这样)。

那么,你有多少位数?答案很多,比log(n)(使用“数字大小”作为基数的日志)更多,这使得基数算法为O(n log n)。

为什么?如果您的数字小于log(n),那么您的数字可能少于n个。因此,您可以简单地使用“计数排序”,这需要花费O(n)时间(只计算您拥有的每个数字的数量)。所以我假设你有超过k> log(n)数字......

这就是为什么人们不使用Radix排序那么多。虽然有些情况下值得使用它,但在大多数情况下,快速排序要好得多。

答案 5 :(得分:8)

当n> 128,我们应该使用RadixSort

当排序int32s时,我选择基数256,所以k = log(256,2 ^ 32)= 4,这比log(2,n)小得多

在我的测试中,在最佳情况下,基数排序比快速排序快7倍。

public class RadixSort {
    private static final int radix=256, shifts[]={8,16,24}, mask=radix-1;
    private final int bar[]=new int[radix];
    private int s[] = new int[65536];//不使用额外的数组t,提高cpu的cache命中率

    public void ensureSort(int len){
        if(s.length < len)
            s = new int[len];
    }   

    public void sort(int[] a){
        int n=a.length;
        ensureSort(n);
        for(int i=0;i<radix;i++)bar[i]=0;
        for(int i=0;i<n;i++)bar[a[i]&mask]++;//bar存放了桶内元素数量
        for(int i=1;i<radix;i++)bar[i]+=bar[i-1];//bar存放了桶内的各个元素在排序结果中的最大下标+1
        for(int i=0;i<n;i++)s[--bar[a[i]&mask]]=a[i];//对桶内元素,在bar中找到下标x=bar[slot]-1, 另s[x]=a[i](同时--bar[slot]将下标前移,供桶内其它元素使用)

        for(int i=0;i<radix;i++)bar[i]=0;
        for(int i=0;i<n;i++)bar[(s[i]>>8)&mask]++;
        for(int i=1;i<radix;i++)bar[i]+=bar[i-1];
        for(int i=n-1;i>=0;i--)a[--bar[(s[i]>>8)&mask]]=s[i];//同一个桶内的元素,低位已排序,而放入t中时是从t的大下标向小下标放入的,所以应该逆序遍历s[i]来保证原有的顺序不变

        for(int i=0;i<radix;i++)bar[i]=0;
        for(int i=0;i<n;i++)bar[(a[i]>>16)&mask]++;
        for(int i=1;i<radix;i++)bar[i]+=bar[i-1];
        for(int i=n-1;i>=0;i--)s[--bar[(a[i]>>16)&mask]]=a[i];//同一个桶内的元素,低位已排序,而放入t中时是从t的大下标向小下标放入的,所以应该逆序遍历s[i]来保证原有的顺序不变

        for(int i=0;i<radix;i++)bar[i]=0;
        for(int i=0;i<n;i++)bar[(s[i]>>24)&mask]++;
        for(int i=129;i<radix;i++)bar[i]+=bar[i-1];//bar[128~255]是负数,比正数小
        bar[0] += bar[255];
        for(int i=1;i<128;i++)bar[i]+=bar[i-1];     
        for(int i=n-1;i>=0;i--)a[--bar[(s[i]>>24)&mask]]=s[i];//同一个桶内的元素,低位已排序,而放入t中时是从t的大下标向小下标放入的,所以应该逆序遍历s[i]来保证原有的顺序不变      
    }
}

答案 6 :(得分:3)

k =“要排序的数组中最长值的长度”

n =“数组的长度”

O(k * n)=“最坏情况下运行”

k * n = n ^ 2(如果k = n)

所以当使用Radix排序时,请确保“最长的整数比数组大小短”,反之亦然。然后你会击败Quicksort!

缺点是:大多数时候你无法保证大整数会变成多大,但如果你有一个固定的数字范围,那么基数排序应该是最佳选择。

答案 7 :(得分:2)

这是一个比较quicksort和radixsort的链接:

Is radix sort faster than quicksort for integer arrays?(是的,是2-3倍)

这是另一个分析几种算法运行时间的链接:

A Question of Sorts

相同数据哪个更快; O(n)排序还是O(nLog(n))排序?

答案:这取决于。这取决于要排序的数据量。它取决于运行的硬件,它取决于算法的实现。

答案 8 :(得分:2)

基数排序不是基于比较的排序,只能对整数(包括指针地址)和浮点等数字类型进行排序,并且它很难移植支持浮点数。

这可能是因为它具有如此狭窄的适用范围,许多标准库选择忽略它。它甚至不能让你提供自己的比较器,因为有些人甚至可能不想直接对整数进行排序,就像使用整数作为其他东西的索引来用作排序的关键,例如:基于比较的排序允许所有的灵活性,因此它可能只是偏好一种通用解决方案,满足99%的人们的日常需求,而不是为了满足1%的需求。

尽管适用范围狭窄,但在我的域名中,我发现更多用于基数排序,而不是内省或快速排序。我在那1%中并且几乎没有使用过字符串键,但经常会找到可以从排序中获益的数字用例。这是因为我的代码库围绕着实体和组件(实体 - 组件系统)的索引以及索引网格之类的东西,以及大量的数字数据。

因此,在我的情况下,基数排序对各种事物都很有用。我的一个常见例子是消除重复索引。在这种情况下,我并不需要对结果进行排序,但通常基数排序可以比替代方案更快地消除重复。

另一个是找到沿着给定维度的kd树的中值分割。对给定维度的点的浮点值进行基数排序,可以在线性时间内快速得到中值位置,以分割树节点。

如果我们不打算在frag着色器中进行,那么另一个是z的高级基元对半正确的alpha透明度进行深度排序。这也适用于GUI和矢量图形软件到z次序元素。

另一种是使用索引列表进行缓存友好的顺序访问。如果索引被遍历多次,它通常会提高性能,如果我事先对它们进行排序,以便遍历按顺序而不是随机顺序完成。后者可以在内存中来回摆动,从缓存行中驱逐数据只是为了在同一循环中重复加载相同的内存区域。当我在重复访问索引之前对索引进行基本排序时,这种情况不再发生,我可以大大减少缓存未命中数。这实际上是我对radix排序的最常见用法,当系统想要访问具有两个或更多组件的实体时,它是我的ECS缓存友好的关键。

在我的情况下,我有一个经常使用的多线程基数排序。一些基准:

--------------------------------------------
- test_mt_sort
--------------------------------------------
Sorting 1,000,000 elements 32 times...

mt_radix_sort: {0.234000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]

std::sort: {1.778000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]

qsort: {2.730000 secs}
-- small result: [ 22 48 59 77 79 80 84 84 93 98 ]

我可以平均6-7毫秒的时间在我的硬件硬件上一次排序一百万个数字,这个数字并不像我想的那么快,因为用户有时会在交互式环境中注意到6-7毫秒,但仍然比55-85毫秒更好,就像C ++的std::sort或C&#39; qsort一样,这肯定会导致非常明显的打嗝帧率。我甚至听说有人使用SIMD实现基数排序,但我不知道他们是如何管理的。我没有足够的智慧来提出这样的解决方案,尽管与标准库相比,即使我天真的小基数排序也相当不错。

答案 9 :(得分:0)

一个例子是当你对一个非常大的整数或整数数组进行排序时。基数排序和任何其他类型的分布排序非常快,因为数据元素主要被排入队列数组(最多10个队列用于LSD基数排序)并重新映射到要排序的相同输入数据的不同索引位置。没有嵌套循环,因此随着要排序的数据输入整数的数量变得非常大,算法往往表现得更线性。与其他排序方法不同,例如极低效的bubbleSort方法,基数排序不会实现比较操作以进行排序。它只是将整数重新映射到不同索引位置的简单过程,直到输入最终排序。如果你想为自己测试一个LSD基数排序,我已经写了一个并存储在github上,可以在一个在线js ide上轻松测试,例如eloquent javascript的编码沙箱。随意玩它并观察它的行为与不同数量的n。我已经使用运行时&lt;测试了多达900,000个未排序的整数300毫秒。如果您想玩它,可以使用以下链接。

https://gist.github.com/StBean/4af58d09021899f14dfa585df6c86df6

答案 10 :(得分:0)

在 Integer 32bit Sort 中,它会进行 7-10 次快速排序,但在 1b 元素上会占用明显的内存,比如几 gb 。因此,仅当您的数据很大但数据中的原始值很小时,您才可以首先使用基数或计数器排序,或者当您可以用内存换取速度时,您可以在任何巨大的整数列表排序中使用

答案 11 :(得分:-8)

快速排序的平均值为O(N logN),但它的最坏情况也是O(N ^ 2),所以即使在大多数实际情况下也不会达到N ^ 2,总是存在风险你的输入将是“糟糕的顺序”。基数排序中不存在此风险。我认为这给基数排序带来了很大的好处。