`
喻红叶
  • 浏览: 39554 次
  • 性别: Icon_minigender_1
  • 来自: 哈尔滨
社区版块
存档分类
最新评论

基数排序

 
阅读更多

基数排序

使用例子来讲基数排序是最直观的。假设有10个0~999之间的十进值数,分别是64 8 216 512 27 729 0 1 343 125,对这几个数如何使用基数排序呢?基数排序顾名思义跟基数有关系,这里的数都是十进制的,所以基数Radix = 10。十进制数在某一位上最多有10个不同可能,比如说个位上的数,只能是0~9之间的某一个。假如这些数先按个位上的数的大小排序,那就可以设置10个桶,分别用来装这些可能数。但是每个桶只能装一个数,如果有两个数个位上的数是相同的怎么办?我们可以把这两个数放到表里面,桶保存的是这个表,而不再是某个数了。这样把这10个数都装完之后,按顺序打印桶里的数,就得到了按个位排序的顺序。同理,我们再用十位,百位来排序,最后一趟排序就能得到正确的结果。在这个排序过程中,按地位排序能得到正确的结果,但是如果按高位排序则不能。

描述一下按基数排序的步骤:

(1)首先设置基数大小的桶,桶里的每个元素都是用来保存表的;

(2)先按个位排序,遍历待排序序列,如果它个位上的数是0,则装进第0个桶里的链表里,如果是1,则装进第1个桶里的链表....

(3)按顺序收集各个桶里的数,这样得到的新序列就是按个位排序的结果

(4)在按个位排序的结果上,再分别用十位上的数排序,过程同上

(5)收集按十位排序的结果

(6)接着按百位、千位,直到最高位排序,最后一趟收集的结果就是排序的正确序列。

我们用图来形象的展示基数排序过程,首先是设置10个桶,初始时h[i]和t[i]都指向这些桶,其中,h[i]始终指向着这个桶,t[i]则指向该桶链表的最后一个元素,方便插入元素,为了方便,我使用了头结点,h[i]就是头结点。待排序序列表L,一次排序完成后,L收集各个桶里表,收集顺序是:

(1)L指向第一个非空桶的链表的第一个元素,该链表的表尾是t[i]

(2)t[i]->next是下一个非空桶的链表的第一个元素,然后依次将非空的桶按首尾顺序连接起来。


然后按十位排序,其过程如下图


接着按百位排序,过程省略。给出示意性代码,分别给出了链表L和桶都使用头结点和都不使用头结点的代码,使用头结点要注意头结点的复用问题,不使用同节点在要考虑第一个元素的特殊情况。首先是声明:

#define Radix 10 //基数

/********对链表的定义*********/
typedef int ElementType;

typedef struct Node *PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;

struct Node {
    ElementType element;
    Position next;
};
/********定义结束************/

Position h[Radix];//保存每个表的头元素
Position t[Radix];//保存每个表的尾元素

/*获取某个数(n)的某个位(place)上的值*/
int getPlace(int n,int place) {
    int div = 1;
    int i = 0;
    for(i = 1; i < place; i++)
        div *= Radix;
    return (n / div) % Radix;
}
链表L和桶都使用头结点的情况:
/*初始化保存表头的数组*/
void initH() {
    int i = 0;
    for(; i <Radix; i++) {
          h[i] = malloc(sizeof(struct Node));
          h[i]->next = NULL;
    }
}

/*是一趟基数排序,place是位,L存放待排序数据*/
List sort(int place,List L) {
    int i = 0;
    //每次排序前都要清除桶与前一次排序的联系
    for(i = 0; i < Radix; i++) {
         t[i] = h[i];
         t[i]->next = NULL;
    }
    //为了复用头结点,需要先保存起来,也就是说在整个基数排序过程中,它始终是链表的头结点
    Position head = L;

    Position p = NULL;
    L = L->next;//第一个元素
    while(L != NULL) {
        p = L;
        L = L->next;
        int index = getPlace(p->element,place);
        p->next = NULL;

        t[index]->next = p;
        t[index] = p;
    }


    //复用头结点
    L = head;
    L->next = NULL;
    Position q = L;
    //进行合并
    for(i = 0; i < Radix; i++) {
        if(h[i]->next != NULL) {
           q->next = h[i]->next;
           q = t[i];
        }
    }

    return L;
}
不使用头结点的代码:
/*初始化桶*/
void InitHT() {
    int i = 0;
    for(i = 0; i < Radix; i++) {
        h[i] = t[i] = NULL;
    }
}

List sortNoHead(int place,List L) {
    //每次排序前都需要初始化桶,这个很关键,要清除它与上次的关系
    InitHT();
    Position p = NULL;

    while(L != NULL) {
        //当前正在处理的节点
        p = L;
        //位置很重要,如果放到最后的话,L当前指向的节点已经改变了
        L = L->next;
        //这句话很重要,切断它与原来链表的联系
        p->next = NULL;
        //当前位上的数
        int index = getPlace(p->element,place);
        //如果是该表的第一个数,就将h[index]和t[index]指向它,否则,将它加入链表后面
        if(t[index] == NULL) {
            h[index] = t[index] = p;
        }else {
            t[index]->next = p;
            t[index] = p;
        }
    }

    //合并
    Position tmp = NULL;

    int i = 0;
    for(i = 0; i < Radix; i++) {
        if(h[i] != NULL) {
            //不适用头结点,则要考虑tmp和L第一次指向非空表时的特殊情况
            if(tmp == NULL) {
                tmp = h[i];
                L = h[i];
            }
            else {
               tmp->next = h[i];
               tmp = t[i];
            }

        }
    }

    return L;
}
以上代码是用C语言写的,C的参数传递方式是值传递,我们看到如果把链表L传到函数里面,在函数里面使用的是L的副本L',L'指向的内存发生了变化,所以我们必须要返回L'。

时间复杂度分析

一趟基数排序的过程:把N个元素分别放到不同的桶里需要O(N),之后需要收集各个桶,重新组成链表,总共有Radix个桶,需要时间O(Radix)。所以一趟排序的时间是:O(N + Radix)。如果待排序序列的最高有P位,也就是需要P趟排序,所以总时间是:

O(P(N + Radix)),N待排序元素个数,Radix桶数,P排序的趟数。

快速排序的时间复杂度是O(NlogN),看上去基数排序的趟数要少一下,但是快速排序每一趟执行的时间要长得多,快速排序通常可以比基数排序更好的利用硬件缓存,logN的因子不都那么大)。如果待排序序列中有很大的数,其余的则很小,这样就不太好了,而且基数排序还有保存链表的附加开销。有一点需要注意:基数排序必须是稳定的


分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics