用少量重复键对巨大的数组进行排序

sort huge array with small number of repeating keys

我想对一个巨大的数组进行排序,比如 10^8 个 X 类型的条目,最多 N 个不同的键,其中 N 是 ~10^2。因为我不知道元素的范围或间距,所以计数排序不是一个选项。所以到目前为止我最好的猜测是使用哈希映射来计算这样的计数

std::unordered_map< X, unsigned > counts;
for (auto x : input)
    counts[x]++;

这工作正常,比 3 向快速排序快 4 倍,但我是一个紧张的人,它仍然不够快。

我在想:我是不是漏掉了什么?我可以更好地利用预先知道 N 的事实吗?或者是否可以根据我的需要调整哈希映射?


EDIT另外一个前提条件是输入序列排序不好,key出现频率差不多

我希望将项目存储在排序的向量中,因为大约 100 个键意味着插入到向量中只会在 10^6 个条目中出现 1 个。查找将是处理器高效的 bsearch in vector

STL 实现在性能方面通常并不完美(请不要大打出手)。

如果您知道唯一元素数量 (N) 的有保证且合理的上限,那么您可以轻松实现自己的散列 table,大小为 2^s >> N。以下是我通常自己做的:

int size = 1;
while (size < 3 * N) size <<= 1;
//Note: at least 3X size factor, size = power of two
//count = -1 means empty entry
std::vector<std::pair<X, int>> table(size, make_pair(X(), -1));
auto GetHash = [size](X val) -> int { return std::hash<X>()(val) & (size-1); };

for (auto x : input) {
  int cell = GetHash(x);
  bool ok = false;
  for (; table[cell].second >= 0; cell = (cell + 1) & (size-1)) {
    if (table[cell].first == x) { //match found -> stop
      ok = true;
      break;
    }
  }
  if (!ok) {             //match not found -> add entry on free place
    table[cell].first = x;
    table[cell].second = 0;
  }
  table[cell].second++;  //increment counter
}

在 MSVC2013 上,与您的代码相比,它将时间从 0.62 秒缩短到 0.52 秒,因为 int 用作类型 X .

另外,我们可以选择更快的散列函数。但是请注意,哈希函数的选择在很大程度上取决于输入的属性。我们取 Knuth's multiplicative hash:

auto GetHash = [size](X val) -> int { return (val*2654435761) & (size-1); };

它进一步将时间缩短到 0.34 秒。

作为结论:您真的想重新实现标准数据结构以实现 2 倍速度提升吗?

注意:另一个 compiler/machine 上的加速可能完全不同。如果您的类型 X 不是 POD,您可能需要做一些修改。

计数排序确实最好,但由于未知范围或间距而不适用。

似乎很容易与 fork-join 并行化,例如boost::thread.

您也可以尝试更高效的手动哈希图。 Unorded_map 通常使用链表来对抗潜在的坏散列函数。如果哈希表不适合 L1 缓存,链表的内存开销可能会影响性能。 Closed Hashing 可能会占用更少的内存。一些优化提示:

  • 具有线性探测且不支持删除的封闭哈希
  • 用于移位而不是模运算的两个大小哈希表的幂(除法需要多个周期并且每个内核只有一个硬件除法器)
  • 低 LoadFactor(通过大小的条目)以最大程度地减少冲突。这是内存使用和冲突次数之间的权衡。应避免超过 0.5 的 LoadFactor。哈希表大小为 256 似乎适合 100 个条目。
  • 便宜的哈希函数。您还没有显示 X 的类型,因此也许更便宜的散列函数可以胜过更多的冲突。