计数排序再思考——从2020年1月第16题谈起
出来以后,再次刷新大家对排序算法的认识。有人提出说题目中使用的排序方法应该称为计数排序,而不是桶排序。这种说法有一定道理,但计数排序本身就是一种特殊的桶排序,所以把它称为桶排序也是可以的。
在认真分析了计数排序的原理以后,笔者发现第16题也只是借鉴了计数排序的算法思想,其代码形式与经典的计数排序代码还是有一定区别的。本文总结了计数排序的基本思想和常见代码形式,并尝试把第16题的代码改写成“更常规”(也更简明)的写法,请大家批评指正。
计数排序再思考
计数排序是一种特殊的桶排序,它把待排序数组中值相等的元素放到同一个桶中(为了表述的方便,我们把桶序列称为统计数组)。这样虽然需要分配较多的桶,但无需再对桶里的元素进行排序了,是一种以空间换时间的方法。
一个简单的计数排序如下所示:
例1. 对学生成绩按升序排序。已知数组a存储了n个学生的成绩,成绩为40到100之间的整数。请编写程序对学生成绩按升序排序。
Constn = 20
Const maximum= 100
Dima(1 To n) As Integer
Dimc(1 To maximum) As Integer
PrivateSub Command1_Click()
Dim i As Integer, j As Integer, t AsInteger
For i = 1 To maximum '把所有的桶均清空
c(i) = 0
Next i
For i = 1 To n '把数据装入对应的桶中
c(a(i)) = c(a(i)) + 1 '累计每个桶中装入元素的数量
Next i
t = 0
For i = 1 To maximum '依次把桶里的数据取出来
j = 0
Do While j < c(i) '将桶里的每个元素按顺序存储到原数组a中
t = t + 1
a(t) = i
j = j + 1
Loop
Next i
For i = 1 To n
Text2.Text = Text2.Text + Str(a(i))
Next i
EndSub
PrivateSub Form_Load()
Dim i As Integer
Randomize
For i = 1 To n
a(i) = Int(Rnd * 61 + 40)
Text1.Text = Text1.Text + Str(a(i))
Next i
EndSub
上述代码能够实现题目的要求,但是还存在两个问题。一是没有考虑“成绩为40到100之间的整数”这个重要条件,申请了100个桶的空间,其实只需要申请61个桶空间即可。二是没有注意到“把数据装入对应的桶中”时是顺序遍历待排序数组的,索引值(下标)越大的元素越晚装入桶中,“把桶里的数据取出来”的时候,也是顺序遍历统计数组的,这样最先从桶里取出来的元素事实上是最晚装入桶中的,也就是说每个桶相当于一个“栈”数据结构,具有“先进后出”特征,如果我们直接按顺序存储到原数组a中,就会改变数组a中等值元素的相对位置,造成“不稳定排序”的后果。
我们可以对计数排序做进一步优化,使其成为一种稳定排序。具体的做法是拓展统计数组c的功能,使其元素值c(i)不再表示i号桶中元素的个数,转而表示最后一个入桶的元素在已排序数组中的索引值。
因为桶中的元素具有“先进后出”特征,则越晚入桶的元素,其排序后的索引值越大,所以我们把桶里的数据取出来时,需要先设置一个辅助数组b(称为目标数组),然后逆序遍历待排序数组a,把元素a(i)依次存储到目标数组b中,即b(c(a(i))) = a(i)。
因为此时已经把元素a(i)从桶中取出来了,即桶里的元素少了一个,所以编号为a(i)的桶中存储的索引值需要减一,以便将该桶的下一个元素存储到正确的位置去,即c(a(i))=c(a(i))-1。
经过上述操作后,存储到数组b中的元素已经是有序的了,将其复制到数组a即可。
优化的计数排序代码如下所示:
Const n = 20
Const minimum = 40
Const maximum = 100
Dim a(1 To n) As Integer
Dim b(1 To n) As Integer
Dim c(minimum To maximum) As Integer
Private Sub Command2_Click()
Dim i As Integer
For i = minimum To maximum '把所有的桶均清空
c(i) = 0
Next i
For i = 1 To n '把数据装入对应的桶中
c(a(i)) = c(a(i)) + 1 '累计每个桶中装入元素的数量
Next i
For i = minimum + 1 To maximum '依次求出每个桶的前缀和
c(i) = c(i) + c(i - 1) '此时c(i)表示最后一个入桶的元素在已排序数组中的索引值
Next i
For i = n To 1 Step -1 '逆序把桶里的数据取出来依次存储到目标数组b中
b(c(a(i))) = a(i)
c(a(i)) = c(a(i)) - 1 '桶里元素少了一个,索引值减一
Next i
For i = 1 To n '将数组b的值复制到数组a
a(i) = b(i)
Text2.Text = Text2.Text + Str(a(i))
Next i
End Sub16.某省举办大型活动,面向省内城市招募有志愿者服务工作经历的志愿者,每个志愿者的报名数据包含城市序号、姓名(字母缩写)和参加志愿者服务的次数。现需要整理报名数据,要求是:先按城市序号从小到大排列;然后,同一城市的志愿者按参加志愿服务的次数从多到少排列。
按上述要求,编写一个VB程序,功能如下:在列表框 List1中显示整理前的数据,单击“整理”按钮 Command1,整理结果显示在列表框 List2 中,程序运行界面如图所示。
(1)将数组元素 q(1)到 q(200)分为10段,如果每段恰好包含20个元素,采用选择排序算法分别对每段中的元素进行排序,整个排序过程中,数组元素之间的比较次数是 (单选,填字母:A.200×199/2次 /B.10×20×19/2次 /C.20×10×9/2次)。
(2)请在划线处填入合适的代码。
Const n = 200 '报名总人数
Const nc = 10 '城市数
Dim city(1 To n) As Integer, pname(1 To n) As String, times(1 To n) As Integer
Dim b(1 To nc) As Integer '存储每个城市的报名人数
Dim c(1 To nc) As Integer
Dim q(1 To n) As Integer
Private Sub Form_Load()
'本过程读取城市序号、姓名和参加次数的数据分别存储在数组city,pname 和times 中,并在 List1 中显示,代码略
End Sub
Private Sub Command3_Click()
Dim i As Integer, j As Integer, k As Integer, t As Integer, pos As Integer
For i = 1 To nc
b(i) = 0
Next i
For i = 1 To n
①
b(k) = b(k) + 1
Next i
k = 1
For i = 1 To nc
c(i) = k
k = k + b(i)
Next i
For i = 1 To n
k = city(i)
②
c(k) = c(k) + 1
Next i
pos = 1
For i = 1 To nc '对各城市报名数据按参加志愿服务的次数进行排序
For j = pos To pos + b(i) - 2
k = fp(j, pos + b(i) - 1)
t = q(k): q(k) = q(j): q(j) = t
Next j
pos = pos + b(i)
Next i
For i = 1 To n
List2.AddItem " " & city(q(i)) & " " & pname(q(i)) & " " & times(q(i))
Next i
End Sub
Function fp(head As Integer, tail As Integer) As Integer
Dim i As Integer, k As Integer
k = head
For i= ③
If times(q(i)) > times(q(k)) Then k = i
Next i
fp = k
End Function
由于已经够多了,我这里不再重复,只重点说明一下几个关键数组的含义:数组b存储各城市志愿者的数量,b(k)=t表示城市k的志愿者数量为t。
数组c开始存储的是各城市的排名(即首个志愿者的总排名),c(k)=t表示城市k的排名为t;后来由于要顺序将各志愿者的编号存储到索引数组q中,每存储一个编号,该城市的下一个志愿者排名就要后移一位,所以c(k)的值不断递增,最后c(k)的值变成了城市k最后一名志愿者的排名加一。
索引数组q存储各志愿者的总排名,q(k)=i表示编号为i的志愿者总排名为k,也就是说对于编号为i的志愿者来说,其姓名为pname(i),城市序号为city(i),服务次数为times(i),总排名为k。
题目实现了双关键字排序,其中主要关键字是城市序号,使用计数排序思想把同一城市的志愿者放到同一个桶中,并用统计数组c存储该城市首个志愿者的总排名;次要关键字是服务次数,分别对每一个城市的志愿者按照服务次数进行选择排序。
通过比较我们可以发现,为了增强代码的可读性,题目在经典计数排序的基础上做了一些改变,它引入了数组b,而且统计数组c的含义也发生了变化(经典计数排序算法中c(k)=t表示最后一个装入桶k中的元素的排名为t)。此外,对次要关键字排序采用的是不稳定的选择排序算法,多少算一点瑕疵。
下面我尝试用经典计数排序算法的代码形式重新实现程序功能,并使用冒泡排序代替选择排序,以获得稳定的排序结果。修改后代码如下:
(1)B
(2)① k = city(i)
② q ( c( k ) ) = i
③ head+1To tail 或k+1To tail 或Head to tail
或k to tail 或tail to head step -1或tail to head+1step -1
或tail to k step -1或tail to k+1step -1① 0
② c(a(i)) + 1
③ 1 (说明:分数最高的肯定是第一名——即使没人考到这个分数)
实现程序功能的方法很多,题目代码只是其中一种,题目中只用到了一个统计数组c来存储桶序列,在3段不同的代码中c(i)=t分别表示第i号桶中有t个元素、第i号桶中最后一个入桶的元素在已排序数组中的索引值为t、第i号桶中第一个入桶的元素在已排序数组中的索引值为t,即分数为i的学生排名为t。
题目代码虽然巧妙,但可读性不高,这里再给出一个可读性稍微高点的代码:
Const n = 20
Const minimum = 40
Const maximum = 100
Dim a(1 To n) As Integer
Dim b(minimum To maximum) As Integer
Dim c(minimum To maximum) As Integer
Private Sub Command2_Click()
Dim i As Integer
For i = minimum To maximum '把所有的桶均清空
b(i) = 0
Next i
For i = 1 To n '把数据装入对应的桶中
b(a(i)) = b(a(i)) + 1 '累计每个桶中装入元素的数量
Next i
c(maximum) = 1 '分数最高的肯定是第一名(即使没人考到这个分数)
For i = maximum - 1 To minimum Step -1 '逆序求出每个分数对应的排名
c(i) = c(i + 1) + b(i + 1)
Next i
For i = 1 To n '输出各学生的名次
Text3.Text = Text3.Text + Str(c(a(i)))
Next i
End Sub为了保证解析的原创性和思维的独特性,我都是独立解题后,先不看答案(除非题目不会做),直接把解析写好,再去看答案。
当然,如果发现参考答案有更好的思路,我还是很乐于学习和借鉴的。同时,由于本人水平有限,解析中难免出现疏漏甚至错误之处,敬请谅解。
无论是赞同还是反对我的看法,都请你给我留言。如果你有新的想法,千万不要憋在心里,请发出来大家一起讨论。让我们相互学习,共同进步!
需要本文word版的,可以加入“选考VB算法解析”知识星球参与讨论和下载文件,“选考VB算法解析”知识星球汇集了数量众多的同好,更多有趣的话题在这里讨论,更多有用的资料在这里分享。
我们专注选考VB算法,感兴趣就一起来!
相关优秀文章: