散列小整数的无序序列

Hashing an unordered sequence of small integers

本文关键字:无序 整数      更新时间:2023-10-16

背景

我有一个大的整数序列集合(约数千个)。每个序列具有以下属性:

  1. 长度为12
  2. 序列元素的顺序无关紧要
  3. 没有元素在同一序列中出现两次
  4. 所有元件都小于约300

请注意,属性2。和3。这意味着序列实际上是集合,但为了最大限度地提高访问速度,它们被存储为C数组。

我正在寻找一个好的C++算法来检查集合中是否已经存在新序列。如果没有,则会将新序列添加到集合中。我考虑过使用哈希表(但请注意,我不能使用任何C++11构造或外部库,例如Boost)。散列序列并将值存储在std::set中也是一种选择,因为如果冲突足够罕见,则可以忽略冲突。任何其他建议也欢迎。

问题

我需要一个交换散列函数,即一个不依赖于序列中元素顺序的函数。我曾考虑过先将序列简化为某种规范形式(例如排序),然后使用标准哈希函数(参见下面的参考文献),但我更希望避免与复制(我无法修改原始序列)和排序相关的开销。据我所知,下面引用的函数都不是可交换的。理想情况下,散列函数还应该利用元素从不重复的事实。速度至关重要。

有什么建议吗?

  • http://partow.net/programming/hashfunctions/index.html
  • http://code.google.com/p/smhasher/

以下是一个基本想法;可以随意修改。

  1. 对一个整数进行哈希只是一个恒等式。

  2. 我们使用来自boost::hash_combine的公式来获得组合散列。

  3. 我们对数组进行排序以获得唯一的代表。

代码:

#include <algorithm>
std::size_t array_hash(int (&array)[12])
{
int a[12];
std::copy(array, array + 12, a);
std::sort(a, a + 12);
std::size_t result = 0;
for (int * p = a; p != a + 12; ++p)
{
std::size_t const h = *p; // the "identity hash"
result ^= h + 0x9e3779b9 + (result << 6) + (result >> 2);
}
return result;
}

更新:删除它。你刚刚把这个问题编辑成了完全不同的东西。

如果每个数字最多为300,则可以将排序后的数组压缩为9位,即108位。"无序"属性只会为您节省额外的12!,这大约是29位,所以它并没有真正的区别。

您可以查找128位无符号整数类型,并将排序的压缩整数集直接存储在其中。或者,您可以将该范围拆分为两个64位整数,并按照上面的方式计算哈希:

uint64_t hash = lower_part + 0x9e3779b9 + (upper_part << 6) + (upper_part >> 2);

(或者可以使用0x9E3779B97F4A7C15作为幻数,这是64位版本。)

对序列的元素进行数字排序,然后将序列存储在trie中。trie的每个级别都是一个数据结构,您可以在其中搜索该级别的元素。。。根据数据结构中的元素数量,可以使用不同的数据结构。。。例如,链表、二进制搜索树或排序向量。

如果您想使用哈希表而不是trie,那么您仍然可以对元素进行数字排序,然后应用其中一个非交换哈希函数。为了比较序列,您需要对元素进行排序,这是必须的,因为您会遇到哈希表冲突。如果你不需要排序,那么你可以用一个常数因子乘以每个元素,这个常数因子会把它们涂抹在int的比特上(有理论可以找到这样的因子,但你可以通过实验找到),然后对结果进行XOR。或者,你可以在一个表中查找大约300个值,将它们映射到通过XOR混合良好的唯一值(每个值都可以是一个随机值,这样它就有相等数量的0和1位——每个XOR翻转随机的一半位,这是最优的)。

我只需要使用sum函数作为散列,看看你能做到什么。这既没有利用数据的不重复特性,也没有利用它们都<另一方面,速度非常快。

std::size_t hash(int (&arr)[12]) {
return std::accumulate(arr, arr + 12, 0);
}

由于函数需要不知道排序,我认为没有一种聪明的方法可以在不首先对输入值进行排序的情况下利用有限的输入值范围。如果这是绝对需要的,在冲突方面,我会硬编码一个排序网络(即许多ifelse语句)来对12个值进行排序(但我不知道12个值的排序网络会是什么样子,甚至不知道它是否实用)。

EDIT经过评论中的讨论,这里有一个减少冲突的非常好的方法:在求和之前,将数组中的每个值都提高到某个整数幂。最简单的方法是通过transform。这确实会生成一个副本,但可能仍然很快:

struct pow2 {
int operator ()(int n) const { return n * n; }
};
std::size_t hash(int (&arr)[12]) {
int raised[12];
std::transform(arr, arr + 12, raised, pow2());
return std::accumulate(raised, raised + 12, 0);
}

您可以在大小为300的位集中切换与12个整数中的每一个对应的位。然后使用boost::hash_combine中的公式组合十个32位整数,实现这个位集。

这提供了交换散列函数,不使用排序,并利用了元素从不重复的事实。


如果我们选择任意的比特集大小,并且如果我们为12个整数中的每一个设置或切换任意数量的比特(通过散列函数或使用预先计算的查找表来确定为300个值中的每个值设置/切换哪些比特),则该方法可以被推广。这导致Bloom过滤器或相关结构。

我们可以选择大小为32或64位的Bloom滤波器。在这种情况下,不需要将大块的比特向量组合成单个散列值。在大小为32的Bloom滤波器的经典实现的情况下,散列函数(或查找表的每个值的非零比特)的最佳数量为2。

如果我们选择"xor",而不是经典Bloom滤波器的"或"运算,并对查找表的每个值使用半个非零位,我们得到了Jim Balter提到的解决方案。

如果我们选择"+"而不是"或"运算,并为查找表的每个值使用大约一半的非零位,我们就会得到一个类似于Konrad Rudolph建议的解决方案。

我接受了Jim Balter的答案,因为他是最接近我最终编码的人,但所有答案都得到了我的+1,因为它们很有用。

这是我最终得到的算法。我写了一个小Python脚本,它生成300个64位整数,使它们的二进制表示恰好包含32个真位和32个假位。真实比特的位置是随机分布的。

import itertools
import random
import sys
def random_combination(iterable, r):
"Random selection from itertools.combinations(iterable, r)"
pool = tuple(iterable)
n = len(pool)
indices = sorted(random.sample(xrange(n), r))
return tuple(pool[i] for i in indices)
mask_size = 64
mask_size_over_2 = mask_size/2
nmasks = 300
suffix='UL'
print 'HashType mask[' + str(nmasks) + '] = {'
for i in range(nmasks):
combo = random_combination(xrange(mask_size),mask_size_over_2)
mask = 0;
for j in combo:
mask |= (1<<j);
if(i<nmasks-1):
print 't' + str(mask) + suffix + ','
else:
print 't' + str(mask) + suffix + ' };'

脚本生成的C++数组如下所示:

typedef int_least64_t HashType;
const int maxTableSize = 300;
HashType mask[maxTableSize] = {
// generated array goes here
};
inline HashType xorrer(HashType const &l, HashType const &r) {
return l^mask[r];
}
HashType hashConfig(HashType *sequence, int n) {
return std::accumulate(sequence, sequence+n, (HashType)0, xorrer);
}

这个算法是迄今为止我尝试过的最快的算法(这个,这个是立方体,这个是300大小的比特集)。对于我的"典型"整数序列,碰撞率小于1E-7,这对我来说是完全可以接受的。