计数排序如何保证稳定性?

27
假设我的输入是 (a,bc 用于区分相等的键)。
1 6a 8 3 6b 0 6c 4

我的计数排序将保存为(丢弃abc信息!!)

0(1) 1(1) 3(1) 4(1) 6(3) 8(1)

这将为我提供结果

0 1 3 4 6 6 6 8

那么,这个稳定排序是如何实现的呢? 我不确定它是如何“保持相等键值记录的相对顺序”的。

请解释一下。

4个回答

53
为了理解计数排序为什么是稳定的,你需要明白计数排序不仅可以用于对整数列表进行排序,还可以用于对键为整数的元素列表进行排序,这些元素将按照它们的键进行排序,并与每个元素相关联的附加信息一起排序。
一个使用附加信息对元素进行排序的计数排序示例将帮助你理解。例如,我们想按价格对三只股票进行排序:
[(GOOG 3), (CSCO 1), (MSFT 1)]

这里的股票价格是整数键,而股票名称则是它们相关的信息。
排序的预期输出应为:
[(CSCO 1), (MSFT 1), (GOOG 3)] 
(containing both stock price and its name,
and the CSCO stock should appear before MSFT so that it is a stable sort)

为了排序这个数组(假设股票价格只能是0到3),将计算一个计数数组:

counts array: [0, 2, 0, 1] (price "1" appear twice, and price "3" appear once)

如果你只是在排序整数数组,那么你可以遍历计数数组并输出两次“1”和一次“3”,然后就完成了,此后整个计数数组将变成全零数组。
但是这里我们也想在排序输出中包含股票名称。我们如何获取这个额外的信息(似乎计数数组已经丢弃了这个信息)?嗯,相关信息存储在原始未排序的数组中。在未排序的数组[(GOOG 3),(CSCO 1),(MSFT 1)]中,我们有股票名称和其价格。如果我们知道(GOOG 3)在最终排序数组中应该处于哪个位置,我们就可以将这个元素复制到排序数组中的相应位置。
为了获取排序数组中每个元素的最终位置,与排序整数数组不同,你不能直接使用计数数组来输出排序后的元素。相反,计数排序还有一个额外的步骤,从计数数组中计算累加和数组:
counts array: [0, 2, 2, 3] (i from 0 to 3: counts[i] = counts[i] + counts[i - 1]) 

这个累积和数组告诉我们每个值在最终排序数组中的位置。例如,counts [1] == 2 意味着当前值为 1 的项应该放置在排序数组的第 2 个插槽中。直观地说,因为 counts [i] 是从左边开始的累计总和,它显示了前面有多少比 ith 值小的项,这告诉您 ith 值的位置应该在哪里。
如果一个 $1 的股票首次出现,则应将其输出到排序数组的第二个位置;如果一个 $3 的股票首次出现,则应将其输出到排序数组的第三个位置。如果出现一个 $1 股票并且它的元素被复制到排序数组中,我们将减少它在计数数组中的数量。
counts array: [0, 1, 2, 3] 
(so that the second appearance of $1 price stock's position will be 1)

因此,我们可以从后向前迭代未排序的数组(这很重要以确保稳定性),根据计数数组检查其在排序数组中的位置,并将其复制到排序数组中。
sorted array: [null, null, null]
counts array: [0, 2, 2, 3]    

iterate stocks in unsorted stocks from backwards
1. the last stock (MSFT 1)
sorted array: [null, (MSFT 1), null] (copy to the second position because counts[1] == 2)
counts array: [0, 1, 2, 3] (decrease counts[1] by 1)

2. the middle stock (CSCO 1)
sorted array: [(CSCO 1), (MSFT 1), null] (copy to the first position because counts[1] == 1 now)
counts array: [0, 0, 2, 3] (decrease counts[1] by 1)

3. the first stock (GOOG 3)
sorted array: [(CSCO 1), (MSFT 1), (GOOG 3)] (copy to the third position because counts[3] == 3)
counts array: [0, 0, 2, 2] (decrease counts[3] by 1)

如您所见,在数组排序后,计数数组(即[0, 0, 2, 2])并没有像对整数数组进行排序那样成为全零数组。计数数组不用于告诉我们整数在未排序数组中出现的次数,而是用于告诉我们元素在最终排序数组中应该处于哪个位置。由于每次输出一个元素时我们都会减少计数,因此我们实际上是使具有相同键的元素下一次出现的最终位置更小。这就是为什么我们需要从后向前迭代未排序数组以确保其稳定性。

结论:

由于每个元素不仅包含一个整数作为键,还包含一些附加信息,即使它们的键相同,您也可以使用附加信息来判断每个元素是否不同,因此您将能够确定它是否是稳定的排序算法(如果适当实现,则是稳定的排序算法)。

参考资料:

一些解释计数排序及其稳定性的好材料:


5
倒着遍历是我之前理解如何保持排序数据稳定性所缺少的部分。谢谢! - georaldc
@georaldc 你用一行代码概括了整篇文章的要点。自早上以来,我一直在犯同样的错误。 - RBT
稳定排序是否可以原地进行,即不需要额外的空间来存储排序后的数组? - Julian A.
@JulianA。是的,这是可能的。例如,选择排序可以被实现成为一个稳定排序,同时也需要O(1)的空间,https://en.wikipedia.org/wiki/Selection_sort - nybon
@nybon 哦,我明白了。但只有在被排序的条目是键本身 - 例如整数时才会这样;如果排序具有作为键的整数字段的对象,则不会。这正确吗? - Julian A.

20

非常简单:每个“桶”不再使用简单的计数器,而是使用一个链接列表。

也就是说,与其使用

0(1) 1(1) 3(1) 4(1) 6(3) 8(1)

你获得

0(.) 1(.) 3(.) 4(.) 6(a,b,c) 8(.)

(这里我使用 . 来表示桶中的某一项)。

然后将它们倒回一个已排序的列表中:

0 1 3 4 6a 6b 6c 8
那就是,当你找到一个带有关键字x的项目时,知道它可能有其他信息来区分它与具有相同关键字的其他项目,你不仅要为桶x递增计数器(这将丢弃所有这些额外信息)。

相反,你为每个桶创建一个链接列表(或类似的带有常时间摊销附加的数据结构),并在从左到右扫描输入时将该项附加到桶x的列表末尾。

因此,你不是使用k个计数器的O(k)空间,而是使用k个最初为空的列表,其长度总和在算法的“计数”部分结束时将为n。这个变体的计数排序仍然是以前的O(n+k)


10
这不是计数排序,而是桶排序的特例,每个等价类都有一个桶,由比较关系诱导的等价关系产生。计数排序之所以“稳定”,是因为根据定义它对于每个可能的不同值都有一个计数。 - Steve Jessop
4
计数排序是桶排序的一种特殊情况,所以我对你说的话没有问题,也没有问题对我说的话。 - polygenelubricants
1
啊,抱歉,我以为你的回答声称你的算法使用链表是一种计数排序。 - Steve Jessop
2
这也是我理解的方式。实际上,这非常令人困惑。OP询问计数排序,但您的解释是针对简化的桶排序。仅计数就足够了,您不需要任何链接列表... - Karoly Horvath

9

你的解决方案不是完整的计数排序,并且丢弃了相关的值。

这里是完整的计数排序算法。

在计算直方图之后:

0(1) 1(1) 3(1) 4(1) 6(3) 8(1)

您需要计算累加和 - 每个单元格将包含小于或等于该值的元素数量:

0(1) 1(2) 3(3) 4(4) 6(7) 8(8)

现在你要从原始列表的末尾开始,并向遍历。

最后一个元素是4。有4个元素小于或等于4. 所以4将会放在第4个位置。你要减少4的计数器。

0(1) 1(2) 3(3) 4(3) 6(7) 8(8)

下一个元素是6c。小于或等于6的元素有7个。所以6c将进入第7个位置。然后,您会将计数器减去6
0(1) 1(2) 3(3) 4(3) 6(6) 8(8)
                      ^ next 6 will go now to 6th position

正如您所看到的,这个算法是一种稳定排序。具有相同关键字的元素的顺序将被保留。


6
如果您的三个“6”值是可区分的,那么您的计数排序是错误的(它丢失了关于值的信息,而真正的排序不会这样做,因为真正的排序只重新排列值)。
如果您的三个“6”值是不可区分的,则排序是稳定的,因为您在输入中有三个不可区分的“6”,并且在输出中也有三个。谈论它们是否已经“重新排序”是没有意义的:它们是相同的。
非稳定性的概念仅适用于具有某些相关信息但未参与排序的值。例如,如果您对这些整数进行指针排序,则可以通过查看它们的不同地址来“区分”三个6。然后询问任何特定的排序是否稳定是有意义的。基于整数值的计数排序将不会对指针进行排序。基于指针值的计数排序将不按整数值排序,而是按地址排序。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接