我有一个1和0的数组随机分布在数组上。
int arr[N] = {1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,1....................N}
现在我想尽可能快地检索数组中的所有1,但条件是我不应该松开数组的确切位置(基于索引),因此排序选项无效。 所以剩下的唯一选择就是线性搜索,即O(n),还有什么比这更好的。
线性扫描背后的主要问题是,我需要运行扫描 为X次。所以我觉得我需要一些其他的数据结构 一旦第一次线性扫描发生,它就会保持这个列表 我不需要一次又一次地运行线性扫描。
Let me be clear about final expectations-
我只需要在一定范围的数组中找到1的数量,我需要在40-100范围内找到数组中的1的数字。所以这可以是随机范围,我需要在该范围内找到1的计数。由于不同的范围要求,我无法一次又一次地迭代数组
答案 0 :(得分:8)
我很惊讶您认为排序是线性搜索的更快替代方案。
如果你不知道它们出现在哪里,那么没有比线性搜索更好的方法了。也许如果您使用位或char
数据类型,您可以进行一些优化,但这取决于您希望如何使用它。
您可以对此进行的最佳优化是克服分支预测。因为每个值都是零或一,您可以使用它来推进用于存储一个索引的数组的索引。
简单方法:
int end = 0;
int indices[N];
for( int i = 0; i < N; i++ )
{
if( arr[i] ) indices[end++] = i; // Slow due to branch prediction
}
没有分支:
int end = 0;
int indices[N];
for( int i = 0; i < N; i++ )
{
indices[end] = i;
end += arr[i];
}
[edit] 我测试了上面的内容,发现没有分支的版本几乎快了3倍(4.36s对11.88s,在随机填充的1亿元素阵列上重复20次)。 / p>
回到这里发布结果,我发现你已经更新了你的要求。使用动态编程方法,你想要的东西真的很容易......
你所要做的就是创建一个大一个元素的新数组,它存储从数组开头到当前索引(但不包括)当前索引的数量。
arr : 1 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 1 1 0 0 0 1
count : 0 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 4 5 6 6 6 6 7
(我已偏移arr
以上,因此排队更好)
现在,您可以在O(1)时间内计算任何范围内的1的数量。要计算索引A
和B
之间的1的数量,您只需:
int num = count[B+1] - count[A];
显然,您仍然可以使用非分支预测版本来初始生成计数。所有这些都应该为你提供一个相当好的加速,而不是每个查询的简单求和方法:
int *count = new int[N+1];
int total = 0;
count[0] = 0;
for( int i = 0; i < N; i++ )
{
total += arr[i];
count[i+1] = total;
}
// to compute the ranged sum:
int range_sum( int *count, int a, int b )
{
if( b < a ) return range_sum(b,a);
return count[b+1] - count[a];
}
答案 1 :(得分:3)
一次线性扫描很好。由于您正在寻找跨阵列范围的多次扫描,我认为可以在恒定时间内完成。你走了:
扫描数组并创建一个位图,其中key = key = sequence(1,2,3,4,5,6 ....)。位图中存储的值为{{1}其中isOne是否有一个在那里,累积的Sum是1的加法,并且你遇到它们
数组= 1 1 0 0 1 0 1 1 1 0 1 0
元组:(1,1)(1,2)(0,2)(0,2)(1,3)(0,3)(1,4)(1,5)(1,6) (0,6)(1,7)(0,7)案例1:当cumulativeSum的下限为0时,1的数量为[6,11] = cumulativeSum在第11位 - 累计第6位= 7 - 3 = 4
案例2:当cumulativeSum的下限为1时,1的数量为[2,11] = cumulativeSum在第11位 - 累积在第2位+ 1 = 7-2 + 1 = 6
步骤1是O(n)
步骤2为0(1)
总的复杂性是线性的毫无疑问但是对于你必须使用范围多次的任务,如果你有足够的内存,上面的算法似乎会更好:)
答案 2 :(得分:1)
它必须是一个简单的线性数组数据结构吗?或者您可以创建自己的数据结构,恰好具有所需的属性,您可以为其提供所需的API,但其实现细节可以隐藏(封装)?
如果您可以实现自己的,并且如果有一些保证稀疏性(对于1或0),那么您可能能够提供比线性性能更好的。我看到你想保留(或能够重新生成)确切的流,所以你必须存储一个数组或位图或行程长度编码。 (如果流实际上是 random 而不是任意的,则RLE将无用,但如果存在明显的稀疏性或具有长串的一个或另一个的模式,则RLE将非常有用。例如,黑色和白色光栅的位图图像通常是RLE的良好候选者。
假设您保证流将是稀疏的 - 例如,不超过10%的比特将是1(或者相反,超过90%将是)。如果是这种情况,那么您可以在RLE上对解决方案进行建模并保持所有1的计数(只需在设置位时递增,在清除时递减)。如果可能需要快速获取这些元素的任意范围的设置位数,则可以为流的分区提供方便大小的计数器阵列,而不是单个计数器。 (在这种情况下,方便大小意味着可以轻松放入内存,缓存或寄存器集中,但在计算总和(完全在该范围内的所有分区)和线性扫描之间提供合理的折衷。任意范围的结果是范围完全包围的所有分区的总和加上未在分区边界上对齐的任何片段的线性扫描结果。
对于一个非常非常大的流,你甚至可以拥有一个分层和的多层“索引” - 从最大(最粗糙)的粒度向下移动到“片段”到任一端(使用下一个)分区总和)并完成线性搜索只有小片段。
显然,这样的结构代表了构建和维护结构的复杂性之间的权衡(插入需要额外的操作,对于RLE,除了附加/前置之外的任何其他内容可能非常昂贵)与执行任意长线性的费用相比搜索/增量扫描。
答案 3 :(得分:0)
如果:
n
值m
次更改数组中找到1的数量, ...你当然可以通过使用缓存策略来检查数组中m
次的每个单元格。
第一次需要1的数量时,你必须检查每个细胞,正如其他人指出的那样。但是,如果然后在变量(例如sum
)中存储1的数量并跟踪对数组的更改(例如,要求通过特定的update()
函数进行所有数组更新),每次使用0
在数组中替换1
时,update()
函数都可以将1
添加到sum
,每次1
在数组中使用0
替换,update()
函数可以从1
中减去sum
。
因此,sum
在第一次计算数组中的1的数量并且不需要进一步计数之后始终是最新的。
(编辑将更新的问题考虑在内)
如果需要在数组的给定范围内返回1的数量,那么可以使用比我刚刚描述的更复杂的缓存策略来完成。
您可以在数组的每个子集中保留1的计数,并在该子集内将0更改为1或反之亦然时更新相关的子集计数。找到阵列中给定范围内的1的总数将是在每个子集中添加完全包含在该范围内的1的数量,然后计算该范围内但不在该范围内的1的数量。已经计数的子集。
根据具体情况,可能值得采用分层排列,其中(比如)整个数组中1的数量位于层次结构的顶部,每个1/q th
中的1的数量array位于层次结构的第二级,数组的每个1/(q^2) th
中的1的数量位于层次结构的第三级,等等。对于q = 4
,你的顶部总数为1,第二级数组每四分之一的数量为1,第三级数组的每十六进制数为1,等等。
答案 4 :(得分:0)
您使用的是C(或派生语言)吗?如果是这样,你能控制数组的编码吗?例如,如果您可以使用位图进行计数。关于位图的好处是你可以使用查找表来计算总和,但是如果你的子范围末端不能被8整除,你将不得不专门处理结束部分字节,但加速将是显着的
如果不是这样,你至少可以将它们编码为单个字节吗?在这种情况下,您可以利用稀疏性(如果存在)(更具体地说,希望通常存在多个零的索引行)。
所以:
u8 input = {1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1,1,0,0,0,1....................N};
您可以编写类似(未经测试)的内容:
uint countBytesBy1FromTo(u8 *input, uint start, uint stop)
{ // function for counting one byte at a time, use with range of less than 4,
// use functions below for longer ranges
// assume it's just one's and zeros, otherwise we have to test/branch
uint sum;
u8 *end = input + stop;
for (u8 *each = input + start; each < end; each++)
sum += *each;
return sum;
}
countBytesBy8FromTo(u8 *input, uint start, uint stop)
{
u64 *chunks = (u64*)(input+start);
u64 *end = chunks + ((start - stop) >> 3);
uint sum = countBytesBy1FromTo((u8*)end, 0, stop - (u8*)end);
for (; chunks < end; chunks++)
{
if (*chunks)
{
sum += countBytesBy1FromTo((u8*)chunks, 0, 8);
}
}
}
基本技巧是利用将目标数组的切片强制转换为语言可以一次查看的单个实体的能力,并通过推理测试它的任何值是否为0,然后跳过整个块。零越多,它就越好。在大型强制转换整数始终至少有一个的情况下,这种方法只会增加开销。您可能会发现使用u32更适合您的数据。或者在1和8之间添加u32测试有帮助。对于零比一般情况更常见的数据集,我使用这种技术非常有利。
答案 5 :(得分:0)
为什么排序无效?您可以克隆原始数组,对克隆进行排序,并根据需要计算和/或标记1
的位置。