第1关:基数排序的实现
任务描述
本关任务:编写代码实现基数排序。
相关知识
为了完成本关任务,你需要掌握: 1.如何实现基数排序; 2.基数排序的算法分析。
基数排序
大多数的排序算法都是通过比较数据大小的方式对列表进行排序,而基数排序与此不同,它不需要进行数据的比较与交换,而是通过“分配”和“收集”两个过程来实现排序。基数排序是桶排序的扩展,它的主要思想是多关键字排序,例如扑克牌有数字和花色两类关键字,可以先按数字将牌分配到 13 个桶中,然后从第一个桶开始依次收集。再将收集好的牌按花色分配到 4 个桶中,然后还是从第一个桶开始依次收集。经过两次“分配”和“收集”操作,最终使牌有序。
对于数字型的数据项,可以将该整数按位数切割成不同的数字,然后按每个位数分别排序。具体做法是:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
下面的一个例子为对多位整数进行基数排序的过程,列表初始状态如图 1 所示。由于列表中的整数最多位数为 3 位,因此需要进行三趟排序。
图1 初始列表
该列表中每个数据项的每一位都是由数字组成的,数字的范围是 0 到 9,所以准备 10 个桶用来放数据项,如图 2 所示。
图2 初始桶
在算法实现中,这里的桶可以通过先进先出的队列来实现。以下为队列 Queue 类的定义,其中还定义了判断队列是否为空、入队、出队等方法。
class Queue:
def __init__(self):
self.items = []
def isEmpty(self): # 判空
return self.items == []
def enqueue(self, item): # 入队
self.items.insert(0,item)
def dequeue(self): # 出队
return self.items.pop()
def size(self): # 求队列元素个数
return len(self.items)
- 首先按照最后一位来进行第 1 趟分配和收集。
(1)分配过程(从桶上方进入):
- 278 最低位是 8,放到桶 8 中;
- 109 最低位是 9,放到桶 9 中;
- 063 最低位是 3,放到桶 3 中;
- 930 最低位是 0,放到桶 0 中;
- 589 最低位是 9,放到桶 9 中;
- 184 最低位是 4,放到桶 4 中;
- 505 最低位是 5,放到桶 5 中;
- 269 最低位是 9,放到桶 9 中;
- 008 最低位是 8,放到桶 8 中;
- 083 最低位是 3,放到桶 3 中。
第 1 趟分配过程完成,结果如图 3 所示。
图3 第 1 趟分配的结果
(2)收集过程按桶 0 到桶 9 的顺序收集(数据项从桶下方出):
- 桶 0:930
- 桶 1:没数据项,不收集
- 桶 2:没数据项,不收集
- 桶 3:063,083
- 桶 4:184
- 桶 5:505
- 桶 6:没数据项,不收集
- 桶 7:没数据项,不收集
- 桶 8:278,008
- 桶 9:109,589,269
第 1 趟收集的结果如图 4 所示,可以看到,整个列表最低位有序了。
图4 第 1 趟收集的结果
- 然后在第 1 趟排序结果的基础上,按中间位来进行第 2 趟分配和收集。
(1)分配过程(从桶上方进入):
- 930 中间位是 3,放到桶 3 中;
- 063 中间位是 6,放到桶 6 中;
- 083 中间位是 8,放到桶 8 中;
- 184 中间位是 8,放到桶 8 中;
- 505 中间位是 0,放到桶 0 中;
- 278 中间位是 7,放到桶 7 中;
- 008 中间位是 0,放到桶 0 中;
- 109 中间位是 0,放到桶 0 中;
- 589 中间位是 8,放到桶 8 中;
- 269 中间位是 6,放到桶 6 中。
第 2 趟分配过程完成,结果如图 5 所示。
图5 第 2 趟分配的结果
(2)收集过程按桶 0 到桶 9 的顺序收集(数据项从桶下方出):
- 桶 0:505,008,109
- 桶 1:没数据项,不收集
- 桶 2:没数据项,不收集
- 桶 3:930
- 桶 4:没数据项,不收集
- 桶 5:没数据项,不收集
- 桶 6:063,269
- 桶 7:278
- 桶 8:083,184,589
- 桶 9:没数据项,不收集
第 2 趟收集的结果如图 6 所示,可以看到,此时中间位有序了,并且中间位相同的那些数据项,其最低位也是有序的。
图6 第 2 趟收集的结果
- 最后在第 2 趟排序结果的基础上,按最高位来进行第 3 趟分配和收集。
(1)分配过程(从桶上方进入):
- 505 最高位是 5,放到桶 5 中;
- 008 最高位是 0,放到桶 0 中;
- 109 最高位是 1,放到桶 1 中;
- 930 最高位是 9,放到桶 9 中;
- 063 最高位是 0,放到桶 0 中;
- 269 最高位是 2,放到桶 2 中;
- 278 最高位是 2,放到桶 2 中;
- 083 最高位是 0,放到桶 0 中;
- 184 最高位是 1,放到桶 1 中;
- 589 最高位是 5,放到桶 5 中。
第 3 趟分配过程完成,结果如图 7 所示。
图7 第 3 趟分配的结果
(2)收集过程按桶 0 到桶 9 的顺序收集(数据项从桶下方出):
- 桶 0:008,063,083
- 桶 1:109,184
- 桶 2:269,278
- 桶 3:没数据项,不收集
- 桶 4:没数据项,不收集
- 桶 5:505,589
- 桶 6:没数据项,不收集
- 桶 7:没数据项,不收集
- 桶 8:没数据项,不收集
- 桶 9:930
第 3 趟收集的结果如图 8 所示,可以看到,此时最高位有序了,并且最高位相同的数据项按中间位有序,中间位相同的数据项按最低位有序。于是整个列表有序,基数排序过程结束。
图8 第 3 趟收集的结果
基数排序的算法分析
基数排序平均和最坏情况下的时间复杂度都是O(d(n+r))
,其中:
- d 为数据项的关键字位数,即分配和收集的趟数,如 930 是由 3 位数组成,所以 d 为 3;
- n 为列表中数据项的个数,即每一趟分配需要放入桶中的元素个数;
- r 为关键字的取值范围,即每一趟需要收集的桶数,如 930 的每一位都是数字,取值范围是 0 到 9,所以 r 为 10。
编程要求
在右侧编辑器中的 Begin-End 区间补充代码,根据基数排序的算法思想和队列的基本操作完成radix_sort
方法,从而实现对无序表的排序。
测试说明
平台会对你编写的代码进行测试,比对你输出的数值与实际正确的数值,只有所有数据全部计算正确才能通过测试:
测试输入:
278,109,63,930,589,184,505,269,8,83
输入说明:输入为需要对其进行排序的无序表。
预期输出:
[8, 63, 83, 109, 184, 269, 278, 505, 589, 930]
输出说明:输出的是对无序表进行基数排序后的结果,以列表的形式展现。
测试输入:
54,26,93,17,77,31,44,55,20
预期输出:
[17, 20, 26, 31, 44, 54, 55, 77, 93]
开始你的任务吧,祝你成功!
'''请在Begin-End之间补充代码, 完成radix_sort函数'''
# 队列
class Queue:
def __init__(self):
self.items = []
def isEmpty(self): # 判空
return self.items == []
def enqueue(self, item): # 入队
self.items.insert(0,item)
def dequeue(self): # 出队
return self.items.pop()
def size(self):
return len(self.items)
# 基数排序
def radix_sort(s):
main = Queue()
for n in s:
main.enqueue(n) # 所有数据项入队
d = len(str(max(s))) # 求出最大数据项的位数
dstr = "%%0%dd" % d # 前导零的模板,把数据项补齐为d位,位数不够的前面置0,如"%05d"表示补齐成五位数
nums = [Queue() for _ in range(10)] # 准备10个队列
for i in range(-1, -d-1, -1): # i的取值为-1到-d,代表从个位到最高位
while not main.isEmpty(): # 进行分配
n = main.dequeue()
dn = (dstr % n)[i] # 得到出队数据的第i位,转成类似"00345"[-2],即得到倒数第二位的4
nums[int(dn)].enqueue(n) # 放到对应的队列中
# 从10个队列中收集到main
# ********** Begin ********** #
for i in range(10):
while not nums[i].isEmpty():
main.enqueue(nums[i].dequeue())
# ********** End ********** #
# 从main导出为列表
result = []
while not main.isEmpty():
result.append(main.dequeue())
return result