| 关于   «  12. 堆排序   ::   目录   ::   14. 基数排序  »

13. 桶排序

13.1. 桶排序

设想过去一年里,你在支付各种账单时,只是把所有单据随手堆在某个角落里。 现在一年结束了,你决定该按账单的用途(电话、电费、房租等)和日期把这些 纸张整理一下。 一个很自然的做法是在地板上腾出一些空间,然后一边翻看那堆纸张,一边把 电话账单放进一堆,把电费账单放进另一堆,以此类推。 一旦完成了账单到各堆的初始分配(一趟即可),你就可以相对快速地按日期对 每一堆分别排序,因为每一堆都相当小。 这就是 桶排序 背后的基本思想。

我们从一种特别简单的情形开始。 考虑下面这段代码片段,用于对数字 0 到 \(n-1\) 的一个排列进行排序。

  for (i=0; i<A.length; i++)
    B[A[i]] = A[i];
  for (i=0; i<A.length; i++)
    B[A[i]] = A[i];
Settings

Proficient Saving... Error Saving
Server Error
Resubmit

这里用键值来确定记录在最终有序数组中的位置。 这是 桶排序 最基本的例子,其中用键值把记录分配到 各个桶中。 这个算法极其高效,无论键的初始次序如何,总是花费 \(\Theta(n)\) 时间。 这远好于我们目前见过的任何排序算法的性能。 问题在于这个算法的用途有限,因为它只适用于 0 到 \(n-1\) 这些数字的 一个排列。

我们可以扩展这个简单版本的桶排序算法,使其更加有用。 由于桶排序必须对键值进行直接计算(而不像我们之前的排序算法那样, 只是询问两条记录中哪一条在前),我们将假定记录使用整数键类型。

最简单的扩展是允许键中出现重复值。 这可以通过把数组槽位变成任意长度的桶来实现,办法是把数组 B 变成 一个由链表构成的数组。 这样,所有键值为 \(i\) 的记录都可以放入桶 B[i] 中。 第二个扩展允许键范围大于 \(n\) 。 例如,一组 \(n\) 条记录的键可能落在 1 到 \(2n\) 的范围内。 唯一的要求是每个可能的键值在 B 中都有一个对应的桶。 我们假定已知可能的键范围在 0 到 MaxKeyValue 之间。 下面是扩展后的桶排序算法。

void binsort(int[] A) {
  List[] B = new LinkedList[MaxKeyValue+1];
  int item;
  for (int i=0; i<=MaxKeyValue; i++)
    B[i] = new LinkedList();
  for (int i=0; i<A.length; i++) B[A[i]].append(A[i]);
  int pos = 0;
  for (int i=0; i<=MaxKeyValue; i++)
    for (B[i].moveToStart(); (item = B[i].getValue()) != -1; B[i].next())
      A[pos++] = item;
}
void binsort(Integer[] A) {
  List[] B = new LinkedList[MaxKeyValue+1];
  Object item;
  for (int i=0; i<=MaxKeyValue; i++)
    B[i] = new LinkedList();
  for (int i=0; i<A.length; i++) B[A[i]].append(new Integer(A[i]));
  int pos = 0;
  for (int i=0; i<=MaxKeyValue; i++)
    for (B[i].moveToStart(); (item = B[i].getValue()) != null; B[i].next())
      A[pos++] = (Integer)item;
}

这个版本的桶排序可以排序任何键值落在 0 到 MaxKeyValue 范围内的 记录集合。

Settings

Proficient Saving... Error Saving
Server Error
Resubmit

所需的总工作量,就是把每条记录放入合适的桶、再把所有记录从桶中取出来 所需的工作量。 因此,我们需要对每条记录处理两次,总工作量为 \(\Theta(n)\) 。

这样的代价分析真的合理吗? 实际上,最后那句话是 错的 ,因为它忽略了一个关键观察。 把所有记录从桶中取出来,要求桶排序检查每一个桶,看它是否包含记录。 因此,无论实际上有多少桶装有记录,算法都必须处理 MaxKeyValue 个桶。 如果 MaxKeyValue 相对于 \(n\) 很小,那么这倒不算很大的开销。 假设 MaxKeyValue \(= n^2\) 。 在这种情况下,完成的总工作量为 \(\Theta(n + n^2) = \Theta(n^2)\) 。 这导致一个糟糕的排序算法。 而且随着 \(n\) 与 MaxKeyValue 之间差距的增大,这个算法会变得更糟。 此外,较大的键范围需要一个大到无法接受的数组 B 。 因此,即使是扩展后的桶排序,也只适用于有限的键范围。

对桶排序作进一步推广,会得到 桶排序 。 在这里,每个桶(现在称为 bucket)关联的不只是一个键,而是一个键 值范围。 桶排序把记录分配到各个桶中,然后依靠某种其他排序技术对每个桶内的记录 进行排序。 希望在于,相对廉价的装桶过程只会把少量记录放进每个桶,这样对每个桶做 一次"清理排序"就相对便宜。 这在精神上类似于基数排序,后者以一种实用的方式扩展了桶排序的概念。

   «  12. 堆排序   ::   目录   ::   14. 基数排序  »

关闭窗口