代码改变世界

算法与数据结构——排序(十)基数排序

2012-11-23 07:55  左眼微笑右眼泪  阅读(650)  评论(0编辑  收藏  举报

     我们现在来学习几个线性相关的排序算法,首先是基数排序。

     我们平时在打扑克的时候,如果我们要对我们手里的牌进行排序,那么一般我们先会对花色排个序,然后在每个花色里面,再来对数字进行排序。当给一系列数字给我们的时候,我们有排序的时候,也会先按照百位数(假如最高是百位数)排序,如果对百位数相同的再用十位数来排序,再对十位数相同的用个位数来排序,最终整个序列就是有序的了。

     基数排序分为MSD和LSD两种方法,MSD就是上面描述的那样,是从左到右的排序(从百位数到十位数到个位数),LSD则是为从右到左的排序(从个位数到十位数到百位数)。

     下面以LSD为列,假如待排的序列为{345,563,136,873,74,456,132,9,583}.

     那么第一步我们把建立十个桶,把个位数相同的放到一个桶内(第i个桶内装的是个位数为i的数)。

image

      第二步,从上到下把每个桶的数遍历一遍,成为一个新的数组,新数组是{132,563,873,583,74,345,136,456,9},然后再次建立十个桶,把十位数相同的放到一个桶内(第i个桶内装的是十位数为i的数)。

image

      第三步,从上到下把每个桶的数遍历一遍,成为一个新的数组,新数组是{9,132,136,345,456,563,873,74,583},然后再次建立十个桶,把百位数相同的放到一个桶内(第i个桶内装的是百位数为i的数)。

image

      第四步,从上到下把每个桶的数遍历一遍,成为一个新的数组,新数组是{9,74,132,136,345,456,563,583,873},此时序列已经是有序的了。

从上面的过程我们可以看到,基数排序是先把数分配到桶内,然后重新组成一个数组,然后再次分配这样的一个过程,所以有人也有基数排序叫做“分配—收集”排序。上面演示的LSD排序的过程,MSD的过程恰好与这个相反,它是先把百位数相同的放入到一个桶内,放完后,它并不马上进行收集,而是在每个子桶内,再建立子桶,按照十位数相同的进行分配,一直这样递归,直到按照个位数相同的进行分配了,这时候,从最小的桶里面开始收集数据,最后整个序列就是有序的了。LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好。

      我们平时在排序的时候,一般是从高位往低位排,而LSD是从低位往高位排,最后为什么也能得到正确的结果呢?其实这可以用数学里面的归纳法来证明。假如我们先按照个位数来进行排序,个位数排序完成后,个位数是有序的,这个时候再按照十位数来进行排序,这时候分两种情况,一种情况是十位数是递增的,那么这个时候不管个位数的大小,按照十位数的大小的顺序就是整个序列的顺序;如果十位数是相等的,那么就取决于个位数的情况,而个位数也是有序的,那么就能证明整个序列是有序的,这样依此类推,最后的结果一定就是有序的。

     下面是具体的代码:

public void Sort(List<int> sortList, int nCount)
{
 
    for (int i = 0; i < nCount; i++)
    {
        int[] tempList = new int[10];//记录余数的数组,temp[i]记录的是余数为i的数的个数
        int[] tempResult = new int[sortList.Count];//记录排序后的结果
 
        //1.分配到桶内
        for (int j = 0; j < sortList.Count; j++)
        {
            //求每个数的个位数,十位数以及百位数.....
            int m =Convert.ToInt32(sortList[j] % Math.Pow(10, i + 1) / Math.Pow(10, i)+0.5)-1;
            tempList[m] += 1;//记录个(十,百,...)位数为m的数的个数
        }
 
 
        //2.收集
        for (int j = 0; j < sortList.Count; j++)
        {
            int m = Convert.ToInt32(sortList[j] % Math.Pow(10, i + 1) / Math.Pow(10, i) + 0.5) - 1;
            int sumCount = 0;
            //求出个(十,百,...)位数比m小的数的个数,这个数也就是即将插入数的下标
            for (int k = 0; k < m; k++)
            {
                sumCount += tempList[k];
            }
 
            //如果说这个下标已经有值了,那么就往后移
           while(tempResult[sumCount] != 0)
            {
                sumCount++;
            }
 
            //把当前值插入到数组中下标为sumCount的元素中
           tempResult[sumCount] = sortList[j];
        }
 
 
        //另一种收集的方法
        //for (int m = 1; m < 10; m++)
        //{
        //    tempList[m] += tempList[m - 1];//记录个(十,百,...)位数等于或小于m的数的个数,与上面的记录的有区别
        //}
 
        //for (int n = sortList.Length - 1; n >= 0; n--)
        //{
        //    int m = Convert.ToInt32(sortList[j] % Math.Pow(10, i + 1) / Math.Pow(10, i) + 0.5) - 1;
        //    tempResult[tempList[m] - 1] = sortList[n];
        //    tempList[tmpSplitDigit] -= 1;
        //}
 
        for (int m = 0; m < sortList.Count; m++)
        {
            sortList[m] = tempResult[m];
        }
 
    }
}

     我们来看一下效率的问题,假如待排的序列有n个数,这些数有d个关键码(取关键码最多的那个数的关键码,关键码就可以理解为最高位是多少位,上面的例子中关键码是3),每个关键码的取值范围为m(m为10,一般每个位上的数字取值范围都是0到9),那么基数排序的时候复杂度是O(d(n+m)),具体来说就是一次分配需要时间为O(n),一次收集时间为O(m),共进行d次分配与收集。