用于随机访问和元素循环的最佳数据结构(C++)

Optimal data structure (in C++) for random access and looping through elements

本文关键字:数据结构 最佳 C++ 循环 随机 访问 元素 用于      更新时间:2023-10-16

我有以下问题:我有一组N个元素(N在几百到几千个元素之间,比方说在500到3000个元素之间)。在这些元素中,小百分比会有一些性质"X",但元素以半随机的方式"获得"answers"失去"这个性质;因此,如果我将它们全部存储在一个数组中,并将1分配给属性为X的元素,否则为零,那么这个由N个元素组成的数组将有N个1和N-N个零(N在20-50的范围内很小)。

问题如下:这些元素以半随机的方式变化非常频繁(意味着任何元素都可以从0翻转到1,反之亦然,但控制它的过程有些稳定,所以总数"n"有点波动,但在20-50的范围内相当稳定);我经常需要集合的所有"X"元素(换句话说,数组的索引,其中数组的值为1),来对它们执行一些任务。

实现这一点的一种简单而缓慢的方法是简单地循环遍历数组,如果索引k的值为1,则执行该任务,但这有点慢,因为超过95%的所有元素的值都为1。解决方案是将所有的1放入一个不同的结构(有n个元素),然后在该结构中循环,而不是在所有n个元素中循环。问题是什么是最好的结构?

元素将随机从0翻转到1,反之亦然(从几个不同的线程),因此没有任何顺序(元素从0翻转至1的时间与时间无关,它将翻转回来),当我循环通过它们时(从另一个线程),我不需要按照任何特定的顺序循环(换句话说,我只需要得到所有的,但这与哪个顺序无关)。

有什么建议吗?这方面的最佳结构是什么?我想到了"std::map",但由于std::map的键是排序的(我不需要这个功能),问题是是否有更快的功能?

编辑:为了澄清,数组示例只是解决问题的一种(缓慢的)方法。问题的本质是,在一个有"N"个元素的大集合"S"中,有一个"N"个元素(其中N远小于N)的连续变化的子集"S",我需要循环通过该集合"S"。速度是至关重要的,无论是对"s"添加/删除元素,还是对它们的循环。因此,尽管从迭代的角度来看,像拥有2个数组并在它们之间移动元素这样的建议会很快,但向数组中添加和删除元素的速度会慢得令人望而却步。这听起来像是一些基于哈希的方法,比如std::set,在迭代和添加/删除方面都能相当快地工作,问题是有比这更好的方法吗?阅读关于"unordered_map"answers"unorde雷德_set"的文档并没有真正阐明元素的添加/删除相对于std::map和std::set快多少,也没有真正阐明它们的迭代会慢多少。另一件需要记住的事是,我不需要一个在所有情况下都最有效的通用解决方案,我需要一个当N在500-3000范围内时最有效的解决方案,并且n在20-50的范围内。最后,速度确实至关重要;有很多慢的方法,所以我在寻找最快的方法。

由于顺序似乎并不重要,因此可以使用单个数组,并将属性为X的元素放在前面。您还需要一个索引或迭代器,指向数组中从X集到未集的转换点。

  • 要设置X,请增加索引/迭代器,并将该元素与要更改的元素交换
  • 要取消设置X,请执行相反的操作:减少索引/迭代器,并将该元素与要更改的元素交换

对于多个线程,自然需要某种互斥来保护数组和索引。

编辑:为了保持迭代器通常使用的半开放范围,您应该颠倒上面的操作顺序:交换,然后递增/递减。如果你保留一个索引而不是迭代器,那么索引的作用是X的计数的两倍。

N=3000其实并不多。如果您对它们中的每一个使用一个位,那么您就有一个小于400字节的结构。您可以使用std::bitset。然而,如果您使用unordered_setset,请注意,您将为列表中的每个n元素花费更多的字节:如果您仅为64位体系结构中的每个元素分配一个指针,则您将使用至少8*50=400字节,远远超过位集

@geza:也许我误解了你所说的两个数组的意思;我想你的意思是,有一个std::vector(或类似的东西),我在其中存储所有具有属性X的元素,另一个存储其余元素?事实上,我不在乎别人,所以我真的需要一个数组。如果我可以将元素添加到数组的末尾,那么添加元素显然很简单;现在,如果我错了,请纠正我,但在该数组中找到一个元素是O(n)运算(因为该数组未排序),然后再次将其从数组中删除需要将所有元素移动一个位置,所以这平均需要n/2次运算。如果我使用链表而不是向量,那么删除元素会更快,但找到它仍然需要O(n)。这就是我说它会慢得令人望而却步的意思;如果我误解了你,请澄清。

听起来std::unordered_set或std::unrdered_map在添加/删除元素方面最快,因为找到一个元素是O(1),但我不清楚一个循环通过所有键的速度有多快;文档清楚地指出,通过std::unordereded_map的键进行迭代比通过std::map的键进行的迭代慢,但没有以任何方式量化慢是"慢",快是"快"。

最后,再重复一遍,我对一般解不感兴趣,我对小"n"的解感兴趣。因此,例如,如果我有两个解决方案,一个是k_1*log(n),第二个是k_2*n^2,原则上第一个可能更快(对于大的n),但如果k_1>>k2(例如,k_1=1000和k2=2和n=20),第三个对于相对较小的"n"仍然更快(1000*log(20)仍然大于2*20^2)。因此,即使std::unordereded_map中的添加/删除可能在恒定时间O(1)内完成,对于较小的"n",该恒定时间是1纳秒、1微秒还是1毫秒仍然很重要。所以我真的在寻找对小"n"最有效的建议,而不是在大"n"的渐近极限中。

另一种方法(在我看来,只有当元素数量增加至少十倍时才有价值)可能是保持双重索引:

#include<algorithm>
#include<vector>
class didx {
// v == indexes[i] && v > 0  <==> flagged[v-1] == i
std::vector<ptrdiff_t> indexes;
std::vector<ptrdiff_t> flagged;
public:
didx(size_t size) : indexes(size) {}
// loop through flagged items using iterators
auto begin() { return flagged.begin(); }
auto end() { return flagged.end(); }
void flag(ptrdiff_t index) {
if(!isflagged(index)) {
flagged.push_back(index);
indexes[index] = flagged.size();
}
}
void unflag(ptrdiff_t index) {
if(isflagged(index)) {
// swap last item with item to be removed in "flagged", update indexes accordingly
// in "flagged" we swap last element with element at index to be removed
auto idx = indexes[index]-1;
auto last_element = flagged.back();
std::swap(flagged.back(),flagged[idx]);
std::swap(indexes[index],indexes[last_element]);
// remove the element, which is now last in "flagged"
flagged.pop_back();
indexes[index] = 0;
}
}
bool isflagged(ptrdiff_t index) {
return indexes[index] > 0;
}
};