7.箱子排序
说明一点,箱排序实用价值不大,在此仅适用于作为基数排序的一个中间步骤,所以有必要介绍一下。
我们先来看一个场景需求:
数据库中存储了学生的姓名年龄和成绩,要求将学生按照成绩排序。如果是前面的几种简单排序,所需要花费的时间均为N方,所以介绍一种更快的排序算法:箱子排序。
简单点说,箱子排序就是把待排序元素分成几类,然后设置若干个箱子,依次扫描待排序的记录,把关键字等于k的记录全都装入到第k个箱子里(分配),然后按序号依次将各非空的箱子首尾连接起来(收集)。
对于实现箱子排序,需要能够:
1.从欲排序链表的首部开始,逐个删除每个节点,并把删除的节点放入合适的箱子中。
2.收集并链接每个箱子里的节点,产生一个排序的链表。
如果输入的链表是Chain类型,那么可以:
1.连续的删除链表的首元素,并将其插入到相应箱子链表首部。
2.逐个删除每个箱子中的元素(从最后一个箱子开始)并将其插入到一个初始为空的链表首部。
下面来总结一下箱子排序:原理:将元素分装到各个有序的箱子中,并且将箱子按照顺序生成新的序列。
要点:先分类,再排序
8.基数排序:
基数排序可以说是对上面箱子排序的扩充。箱子排序存在一个非常大的问题:如果箱子跨度range为次方级,那么箱子排序的箱子数目将会非常多。为了解决这个弊端,我们将箱子排序进行扩充。
基数排序的原理是不直接对元素进行排序,而是先将其分解,然后对每一部分进行排序。
比如对十进制三位数排序。123拆解为1,2,3;456拆解为4,5,6。先比较个位数,排好序后再比较十位数,最后再比较百位数排序,这样便可以得到最后的排序结果。
简而言之就是每次按照每个数字的一位进行排序,先按照最个位,再按照十位,依次类推至最高位。其中每一位的排序必须是稳定排序才能保证算法的正确性。
基数排序的源码:
- #include
- using
namespace std; -
- template
<</span>class T> - void
show(T arr,int n){ -
for(int i =0;i -
cout<<arr[i]<<","; -
} -
cout<<arr[n-1]<<endl; - }
-
-
- int
RadixCountSort(int* npIndex, int nMax, int* npData, int nLen){ -
//这里就不用说了,计数的排序。不过这里为了是排序稳定 -
//在标准的方法上做了小修改。 -
int* pnCount = (int*)malloc(sizeof(int)* nMax); //保存计数的个数 -
int i = 0; -
for (i = 0; i < nMax; ++i){ -
pnCount[i] = 0; -
} -
for (i = 0; i < nLen; ++i){ //初始化计数个数 -
++pnCount[npIndex[i]]; -
} -
-
for (i = 1; i < 10; ++i){ //确定不大于该位置的个数。 -
pnCount[i] += pnCount[i - 1]; -
} -
-
int * pnSort = (int*)malloc(sizeof(int) * nLen); //存放零时的排序结果。 -
//注意:这里i是从nLen-1到0的顺序排序的,是为了使排序稳定。 -
for (i = nLen - 1; i >= 0; --i){ -
--pnCount[npIndex[i]]; -
pnSort[pnCount[npIndex[i]]] = npData[i]; -
} -
for (i = 0; i < nLen; ++i){ //把排序结构输入到返回的数据中。 -
npData[i] = pnSort[i]; -
} -
free(pnSort); //记得释放资源。 -
free(pnCount); -
return 1; - }
-
- //基数排序
- int
RadixSort(int* nPData, int nLen){ -
//申请存放基数的空间 -
int* nDataRadix=(int*)malloc(sizeof(int) * nLen); -
int nRadixBase = 1; //初始化倍数基数为1 -
int nIsOk = 0; //设置完成排序为0 -
-
//循环,知道排序完成 -
while (!nIsOk){ -
nIsOk = 1; -
nRadixBase *= 10; -
int i = 0; -
for (i = 0; i < nLen; ++i){ -
nDataRadix[i] = nPData[i] % nRadixBase; -
nDataRadix[i] /= nRadixBase / 10; -
if (nDataRadix[i] > 0){ -
nIsOk = 0; -
} -
show(nPData,nLen); -
} -
if (nIsOk){ //如果所有的基数都为0,认为排序完成,就是已经判断到最高位了。 -
break; -
} -
RadixCountSort(nDataRadix, 10, nPData, nLen); -
-
} -
free(nDataRadix); -
return 1; - }
-
-
- void
main() - {
-
int inputNumber[]={212,127,425,159,681,914,326,153,558}; -
int count = 9; -
cout<<"原始数组:"<<endl; -
show(inputNumber,count); -
cout<<"排序过程:"<<endl; -
RadixSort(inputNumber,count); -
cout<<"排序结果:"<<endl; -
show(inputNumber,count); - }
基数排序的过程:
下面来分析一下基数排序的复杂度:
时间效率:设待排序列为n个记录,d个关键码,关键码的取值范围为radix,则进行链式基数排序的时间复杂度为O(d(n+radix))。
其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(radix),共进行d趟分配和收集。
接下来是稳定性的讨论:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序,最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。基数排序基于分别排序,分别收集,所以其是稳定的排序算法。
原理:将数字按位数划分出n个关键字,每次针对一个关键字进行排序,然后针对排序后的序列进行下一个关键字的排序,循环至所有关键字都使用过则排序完成。
要点:对关键字的选取,元素分配收集。
下面对前面讨论过的和未讨论的各种排序算法进行一下总结(注释:以下表格引用自明哥的博客):
| 时间复杂度 | 原理 | 稳定性 | 举例(3,1,4,5,2) |
名次排序 | O(n2) | | 稳定 | 名次是(3,1,4,5,2) 排序是(1,2,3,4,5) |
冒泡排序 | O(n2) | | 稳定 | 一次冒泡:1,3,4,2,5 二次冒泡:1,3,2,4,5 三次冒泡:1,2,3,4,5 四次冒泡:1,2,3,4,5 五次冒泡:1,2,3,4,5 |
插入排序 | O(n2) | | 稳定 | 插入第二个:1,3,4,5,2 插入第三个:1,3,4,5,2 插入第四个:1,3,4,5,2 插入第五个:1,2,3,4,5 |
选择排序 | O(n2) | | 稳定 | 第一次:3,1,4,2,5 第二次:3,1,2,4,5 第三次:1,2,3,4,5 第四次:1,2,3,4,5 |
基数排序 | O(n) | | 稳定 | 一次:1,2,3,4,5 |
堆排序 | O(nlogn) | | 不稳定 | 删除5,4,3,2,1 依次放入12345 |
拓扑排序 | O(n2) | | 稳定 | 无 |
快速排序 | 平均O(nlog2n) 最坏O(n2) | | 稳定 | 第一次;1,3,4,5,2 第二次:1,3,4,2,5 第三次:1,2,3,4,5 |
归并排序 | 平均O(nlog2n) 最坏O(nlog2n) | | 稳定 | 3,1 1,3 1, 2, 3 ,4, 5 |