我有几个巨大的数组(数百万++成员)。所有这些都是数字数组,它们没有排序(我不能这样做)。有些是uint8_t
,有些是uint16_t/32/64
。我想估计这些数组中不同值的计数。条件如下:
完美输出的例子:
ArrayA [uint32_t, 3M members]: ~128 distinct values
ArrayB [uint32_t, 9M members]: 100000+ distinct values
ArrayC [uint8_t, 50K members]: 2-5 distinct values
ArrayD [uint8_t, 700K members]: 64+ distinct values
我知道有些限制可能看起来不合逻辑,但就是这样。作为旁注,我也想要最常用的X(3或10)和最少使用的值,但这样做要容易得多,我可以自己做。但是,如果有人也有这样的想法,请随时分享!
编辑:关于STL的一些澄清。如果您有使用它的解决方案,请发布它。不使用STL对我们来说只是一个奖励,我们不太喜欢它。但是,如果它是一个很好的解决方案,它将被使用!
对于8位和16位值,您只需创建每个值的计数表;每次写入之前为零的表条目时,都会找到不同的值。
对于较大的值,如果您对100000以上的计数不感兴趣,std::map
是合适的,如果它足够快。如果这对你来说太慢了,你可以编写自己的B树。
我很确定你能做到:
假设您同时计算前10个最常见的值,那么如果少于10个不同的值,您将确切地知道它们是什么,并且您不需要估计。
我认为“最常用”的问题很难(很好,耗费内存)。假设您只需要最常用的前1个值。进一步假设您在阵列中有1000万个条目,并且在第一个990万个条目之后,到目前为止您看到的数字都没有出现超过100k次。那么你到目前为止看到的任何值都可能是最常用的值,因为它们中的任何一个都可以在最后运行100k值。更糟糕的是,他们中的任何两个最终都会有50k的运行,在这种情况下,前990万条目的计数是它们之间的平局。因此,为了在最常用的单次通过中进行计算,我认为您需要知道出现在990万中的每个值的确切计数。你必须为过去的10万个中两个值之间的近似关系做准备,因为如果发生这种情况,你不能再倒带并再次检查两个相关值。最终你可以开始剔除值 - 如果有一个数量为5000的值并且只剩下4000个条目要检查,那么你可以剔除任何数量为1000或更少的数据。但这并没有多大帮助。
所以我可能错过了一些东西,但我认为在最坏的情况下,“最常用”的问题要求你为你看到的每个值保持一个计数,直到几乎结束阵列。因此,您可以使用该计数集合来计算出有多少不同的值。
即使对于大值,一种可以工作的方法是将它们分散到延迟分配的桶中。
假设你正在使用32
位整数,创建一个2**32
位数组是相对不切实际的(2**29
bytes,hum)。但是,我们可以假设2**16
指针仍然合理(2**19
字节:500kB),因此我们创建2**16
桶(空指针)。
因此,最重要的想法是采用“稀疏”的方法进行计数,并希望整数不会分散,因此许多桶指针将保持null
。
typedef std::pair<int32_t, int32_t> Pair;
typedef std::vector<Pair> Bucket;
typedef std::vector<Bucket*> Vector;
struct Comparator {
bool operator()(Pair const& left, Pair const& right) const {
return left.first < right.first;
}
};
void add(Bucket& v, int32_t value) {
Pair const pair(value, 1);
Vector::iterator it = std::lower_bound(v.begin(), v.end(), pair, Compare());
if (it == v.end() or it->first > value) {
v.insert(it, pair);
return;
}
it->second += 1;
}
void gather(Vector& v, int32_t const* begin, int32_t const* end) {
for (; begin != end; ++begin) {
uint16_t const index = *begin >> 16;
Bucket*& bucket = v[index];
if (bucket == 0) { bucket = new Bucket(); }
add(*bucket, *begin);
}
}
收集数据后,您可以计算不同值的数量或轻松找到顶部或底部。
几点说明:
如果您设法收集这些统计信息,那么您的工作就会被删除。
对于8位和16位非常明显,您可以在每次迭代时跟踪每种可能性。
当你得到32位和64位整数时,你真的没有记忆来追踪每一种可能性。
这里有一些可能超出约束范围的自然建议。
我真的不明白你为什么不能对数组进行排序。 RadixSort是O(n),一旦排序,将获得准确的独特性和前X个信息。实际上,如果使用1字节基数(1个通道用于计数+ 1 * 4通道用于每个字节+ 1个通道用于获取值),那么对于32位将共有6个通道。
在与上面相同的尖点中,为什么不使用SQL。您可以创建一个存储过程,该数组将数组作为表值参数,并一次性返回不同值的数量和前面的x值。也可以并行调用此存储过程。
-- number of distinct
SELECT COUNT(DISTINCT(n)) FROM @tmp
-- top x
SELECT TOP 10 n, COUNT(n) FROM @tmp GROUP BY n ORDER BY COUNT(n) DESC
我刚才想到了一个有趣的解决方案。它基于布尔代数定律,称为乘法幂乘,它指出:
X * X = X.
从它,并使用布尔乘法的可交换属性,我们可以推导出:
X * Y * X = X * X * Y = X * Y.
现在,你看到我要去哪里?这就是算法的工作方式(我对伪代码很糟糕):
在第一次迭代中,我们使(element1 * element2)* element3。我们可以将其表示为:
(X * Y)* Z.
如果Z(element3)等于X(element1),那么:
(X * Y)* Z = X * Y * X = X * Y.
如果Z等于Y(element2),那么:
(X * Y)* Z = X * Y * Y = X * Y.
因此,如果Z与X或Y不同,那么当我们将它乘以Z时,X * Y不会改变
这对于大表达式仍然有效,例如:
(X * A * Z * G * T * P * S)* S = X * A * Z * G * T * P * S
如果我们收到的值是我们的大被乘数的因子(这意味着它已经被计算过),那么当我们将它乘以收到的输入时,大被乘数不会改变,因此没有新的不同值。
这就是它将如何发展。每次计算出不同的值时,我们的大被乘数和该不同值的乘法将与大操作数不同。所以,使用b = c & element[i]
,如果b!= c
我们只是增加不同的值计数器。
我想我不够清楚。如果是这种情况,请告诉我。