基数排序的O(NL)算法

Posted on 2011-10-19 21:59 Mato_No1 阅读(599) 评论(1)  编辑 收藏 引用 所属分类: 排序算法及其应用
(神犇看到这个不要鄙视啊啊……饶了本沙茶啊啊……)

刚才本沙茶在看后缀数组(还木有看完)的时候,里面的那个基数排序写法各种看不懂……

后来到网上一看才发现这是一种极其神犇的基数排序算法,它不需要任何链式或类链式结构,也不需要在每次排序后都把所有元素按照本次排序的结果重新换位置(其实这样是相当耗时间的,尤其是字符串排序的时候,因为复制一个字符串的时间复杂度取决于它的长度),只需要存储每个元素在每次排序之后的“相对下标”即可。

【1】先考虑这样一个问题:对一个含N个元素,每个元素值的范围为[0..SZ-1]的整数序列进行计数排序(第一关键字为字符,第二关键字为下标),并且在排序后求出ord[0..N-1]数组:ord[i]表示排在第i位的元素在排序前的下标。要求这个算法中不能使用任何链式或类链式结构(链表、Dancing Links、vector等)。

设立数组S,S[i]表示值为i的元素的个数。求S[i]可在O(N)时间内求出(一般地,还要用O(SZ)的时间进行初始化)。再设立数组S0(在实现的时候,可以直接用S来存储S0的值),S0[i]=∑S[0..i](也就是值不大于i的元素的个数),求S0只需要在S的基础上用O(SZ)时间即可。然后可以得到一个重要的性质:对于任意i(0<=i<SZ),原序列中的所有值为i的元素,按照其下标从小到大,其名次(或者说是序号,排序前下标为i的元素的名次记为rank[i],下同)依次为S0[i-1]..S0[i]-1(i=0时,为0..S0[i]-1,这里认为名次从0开始),这样,只要在求出S0以后用O(N)时间扫描一遍原序列,每扫描到一个值为i的元素,立刻可以从S0中获得其名次,同时将S0[i]的值加1,扫描完后所有的元素以后即得出了rank[0..N-1]。然后,ord与rank其实是互逆的(因为ord[i]=j等价于rank[j]=i),因此求出了rank以后可以在O(N)时间内求出ord(当然,根据ord[rank[i]]=i这一性质,可以在扫描过程中不求rank而直接求ord)。不过,在扫描这一步,更好的方法是倒序扫描,这样在扫描到一个值为i的元素之后,可以不动用S[i-1](这个当i=0时还要特判,比较麻烦),直接调用S[i]的值(当然要先将S[i]减1),就是该元素的名次了。
很明显,该算法的时间复杂度为O(2SZ+2N)(如果不求rank直接求ord的话)=O(N),且唯一用到的辅助空间就是S(前面已经说过了,直接在S上存储S0,不单独设S0),故空间复杂度也是线性的,没有用到链式或类链式结构。

【2】然后来解决基数排序的问题。假设这里是对字符串进行基数排序(注意,各元素的长度可能不相等,此时排序的总位数L应取所有元素长度的最大值,且排序过程中遇到不足位数的元素要特判:对该元素的该位记为@,@是一个比字符集中的所有字符都小的字符,其在字符集中名次为0,所有实际在字符集中的元素的名次为1..SZ,这样总的字符个数就是SZ+1,在写代码的时候要注意),从最后一位(第L-1位)开始一位一位排序,直至第0位,中间每次排序的过程实质上就是像【1】这样的计数排序。

按照本沙茶以前的方法,每进行一位的排序后就要将所有元素重新调换位置(也就是构造一个新的序列代替原来的序列,原序列的下标为i的元素在新序列中下标为rank[i]),但这样的时间开销很大(前面已经说过了)。更好的方法是在整个排序过程中,序列的各个元素的下标都不变,但每位排序后,序列有一个“相对下标”,相对下标为i的元素就是此次排序中排在第i位的元素(即ord[i]),这样在每次排序时,只要操纵各元素的上一位相对下标即可(在求S的时候不用相对下标,因为S的值是由个数决定的,与顺序无关,但是在求本位的ord的时候,倒序扫描序列其实是倒序扫描相对下标的序列,即扫描到下标为i,实际指的是相对下标为i,即ord[i]),注意一开始所有元素的相对下标与下标相等,即ord[i]=i。这样就不需要调换位置了,只需要ord就行了。

最后总时间复杂度显然是O(NL)的。

代码(对于字符串进行基数排序,这里假设字符串只含小写字母,注意这里的SZ是27而不是26,因为有@的存在):
#include <iostream>
#include 
<stdio.h>
#include 
<string.h>
using namespace std;
#define re(i, n) for (int i=0; i<n; i++)
#define re2(i, l, r) for (int i=l; i<r; i++)
#define rre(i, n) for (int i=n-1; i>=0; i--)
const int MAXN = 100000, MAXLEN = 101, SZ = 27;
int n, L0[MAXN], ord[MAXN], tmp[MAXN], S[SZ];
char A[MAXN][MAXLEN];
void init()
{
    scanf(
"%d"&n);
    re(i, n) scanf(
"%s", A[i]);
}
void solve()
{
    
int L = 0;
    re(i, n) {
        L0[i] 
= strlen(A[i]); ord[i] = i;
        
if (L0[i] > L) L = L0[i];
    }
    rre(j, L) {
        re(i, SZ) S[i] 
= 0;
        re(i, n) S[L0[i] 
> j ? A[i][j] - 96 : 0]++;
        re2(i, 
1, SZ) S[i] += S[i - 1];
        rre(i, n) tmp[
--S[L0[ord[i]] > j ? A[ord[i]][j] - 96 : 0]] = ord[i];
        re(i, n) ord[i] 
= tmp[i];
    }
}
void pri()
{
    re(i, n) puts(A[ord[i]]);
}
int main()
{
    init();
    solve();
    pri();
    
return 0;
}
最后,Orz一下这个无比神犇的算法!!!

Feedback

# re: 基数排序的O(NL)算法  回复  更多评论   

2012-01-29 19:31 by zyn.cpp
这个好像叫做计数排序吧。算法导论上讲的三种O(N)的排序方法之一。

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理