有一个有点玩弄的方法,但它不是那么漂亮(也不是特别快)。但无论如何我都会解释它,因为你问过,这很有趣。
这个想法是,不是将每个位位置的计数保存在整数中,而是将计数器的位保存在整数中。因此,如果您将计数器视为一个布尔矩阵,每一行都是一个计数器,请将 列 存储为整数。这样,输入数字是一种奇怪的加法,而不是“与位一样多的增量”。像这样(未测试)(添加c)
for (int i = 0; i < counter_bits; i++) {
counter[i] ^= c;
c &= ~counter[i]; // counter & c == ~(counter ^ c) & c
}
现在有趣的部分是:中位数是多少?好吧,如果 n(项目数)是 2 减一的幂,而 counter 是一个长度正好足以“垂直”表达 n 的数组,那么中位数就是“高位”计数器(只要看到一个位至少 (n+1)/2 次就会出现 1)。
更有趣的情况是“否则”。它仍然是可修复的,要让它再次正常工作所需要的只是用一个偏差初始化计数器,以便在正确的时刻准确设置最高位。例如,如果n = 5,那么计数器应该初始化为1(垂直1,所以counter[0] = -1,所有其他计数器都是0),所以当添加3个时,它会变为4,即在这种情况下最高位。另一个例子:如果n = 17,那么最高位的权重应该是 16,但是 9 足以在中位数中设置一个位,所以计数器应该初始化为 7(所以,counter[0] = counter[ 1] = 计数器[2] = -1,其余为 0)。
这种方法显然可以以一种简单的方式推广到更宽的位向量,因为对变得更宽的事物的所有操作都是逐点位操作。
示例(以防万一有人对该算法中发生的事情感到困惑)
输入:
1000
1111
1010
3 个项目,大多数需要 2 个,我们数到最多 3 个,所以计数中只有 2 位(权重 1 和 2),不需要偏差。
init: counter = { 0000, 0000 }
put in 1000
counter = { 1000, 0000 }
put in 1111
counter = { 0111, 1000 } (the leftmost bit carried into the high counter)
put in 1010
counter = { 1101, 1010 } (the second bit from the right carried into the high counter)
result: the upper counter, so 1010
现在,在现实世界中,我不会这样做。根据情况,我可能会这样做:
有一个表(或pdep,如果有的话)将字节映射到“展开”版本,例如10010101b -> 0x10010101,然后添加这些(通过正常添加),然后从半字节的高位(pext 如果你有它,否则它会更棘手)。偏见技巧仍然有效。缺点:仅适用于n < 16。尽管存在巨大的劣势,但我仍然在现实生活中使用它(对于二元谜题,修剪搜索空间)。当然,这仍然适用于“更多的传播”,这会给你一个更高的最大值n(例如,将 8 个计数器放在 uin64 中得到 n < 256,将 4 个放在 uint64 中得到 n < 65536 等等在)。使用 LeleDumbo 的答案中的数组,这真的是同构的,除了数组是在一个单一的 int 中(事实上,如果你把传播提高到 11,它就变得完全相同)。这也可以扩展到非常大的向量(只需在数组中使用其中几个计数器)。
或者:使用正确的 SIMD,而不是这个假 SIMD。使提取位更容易(因为可以提取所有最高位的掩码)。它甚至不需要偏置技巧,因为您可以进行 SIMD 比较。它还使传播更容易,因为它不必使用查找表来完成 - 将项目广播到所有通道,屏蔽每个通道中的适当位(这很方便忽略了一些细节),比较然后 减去 这个来自计数(因为现在它是 -1 时为真,而不是 1)。例如(未测试)
vpbroadcastb xmm0, [item] ; put 8bit item in all lanes
vpand xmm0, [lane_bit_mask] ; { 1, 2, 4, 8, ...
vpcmpeqb xmm0, [lane_bit_mask] ; -1 if the bit is set
vpsubb xmm1, xmm0 ; subtract from total
这有点浪费,只使用了 16 条(或 32 条)车道中的 8 条,但它显示了基本思想。使用更多通道时,需要花费更多精力来解包。