可用于存储和管理整数集合的最佳C++数据结构是什么

What is the best C++ data structure that could be used for storing and managing a collection of integers?

本文关键字:最佳 C++ 数据结构 是什么 集合 整数 用于 存储 管理      更新时间:2023-10-16

这是我的第一个 StackOverflow 问题,所以如果我没有遵循这个问题的社区准则,请告诉我,如果我应该删除它。

我收到了我的第一个面试问题,但由于我的实施,我被拒绝了。

问题是:

设计和实现存储整数集合的 C++ 类。在构造时,集合应为空。同一号码可以存储多次。

实现以下方法:

  1. 插入(整数 x)。 插入值"x"的条目。

  2. 擦除(int x). 从集合中删除一个值为"x"的条目(如果存在)。

  3. 擦除(int from, int to)。 删除值在 [from, to] 范围内的所有条目。

  4. 计数(int from, int to). 计算有多少条目的值在 [from, to] 范围内。

我认为一个好的实现是使用链表,因为它使用非连续内存,删除条目不需要打乱大量数据(如向量或数组)。但是,我从公司得到反馈说我的实现是O(n^2)时间复杂度并且效率非常低,所以我被拒绝了。如果在另一次面试中出现类似的问题,我不想重复同样的错误,所以我想知道解决这个问题的最佳方法是什么(一位朋友建议使用地图,但他也不确定)。

我的代码是:

void IntegerCollector::insert(int x)
{
entries.push_back(x);
}
void IntegerCollector::erase(int x)
{
list<int>::iterator position = find(entries.begin(), entries.end(), x);
if (position != entries.end())
entries.erase(position);
}
void IntegerCollector::erase(int from, int to)
{
list<int>::iterator position = entries.begin();
while (position != entries.end())
{
if (*position >= from && *position <= to)
position = entries.erase(position);
else
position++;
}
}
int IntegerCollector::count(int from, int to)
{
list<int>::iterator position = entries.begin();
int count = 0;
while (position != entries.end())
{
if (*position >= from && *position <= to)
count++;
position++;
}
return count;
}

反馈提到,他们只会雇用能够实施具有O(nlogn)复杂性的解决方案的候选人。

这里的关键考虑因素是相同值的整数是无法区分的。因此,您需要做的就是在容器中存储每个不同值的计数

然后,您可以使用std::map<int, size_t>作为支持结构,将每个整数(键)映射到数据结构中存在的次数(值 = 计数)。

插入和擦除单个元素只是递增和减少(在后一种情况下可能删除)给定键的值(两者都O(log(distinct_values_in_container))用于查找键)。

由于std::map是有序的,因此您可以使用lower_boundupper_bound进行二进制搜索,因此在[from,to)中查找键非常有效(查找范围也很O(log(distinct_values_in_container)))。擦除它们或对它们的计数求和很容易(这里的运行时更复杂)。


如果你想获得额外的学分,了解渐近运行时的局限性是值得的。请考虑以下几点:

这些渐近运行时在实践中的含义在很大程度上取决于使用模式。如果没有插入重复项,我们就处于O(n),但如果有很多相同的元素(例如,如果每个键都有O(exp(n))值,那么O(distinct_values_in_container) = O(log(n))),您也可以获得任意好的时间(以n= 插入次数表示)。在所有相关整数都相同的极端情况下,所有操作都O(1)

作为受访者,我还会谈谈这些渐近运行时在实践中是否有意义。可能是映射的树结构(对缓存和分支预测器有害)输给了给定应用程序的简单std::vector<std::pair<int, size_t>>(如果擦除总是批量的)甚至std::vector<size_t>(如果键是"密集的")。


我认为您的主要错误(以及您被拒绝的原因)是没有意识到没有必要单独存储每个插入的整数。不幸的是,您似乎也错过了保持列表排序的可能性,但我看不出O(n^2)来自哪里。

如果你被聘为一个不需要任何编程经验的角色,那么我不会仅仅在这个代码样本上拒绝你。

使用std::list是一个有趣的选择,表明你已经考虑过这一点。事实上,你使用了一个C++标准库容器,而不是试图从较低级别构建它,这对我来说是一个是雇用的标志。使用你的方法(1)很快,但(2),(3)和(4)会很慢。在没有任何其他信息的情况下,您应该安排事情,以便读取(包括查询)数据比写入更快。你的方法反过来了。有时,这就是您想要的 - 例如,在实时测量时,您希望数据转储阶段尽可能快,而牺牲其他任何事情。对于该应用程序,您的解决方案将难以击败!

预订,但绝不是红线:

整数并不意味着int。在无法澄清的情况下,建立你的类

template<typename Y> std::map<Y, std::size_t>

其中Y是整型。请注意计数器使用std::size_t。它计算特定Y存在的次数。

下次包括一些程序注释。

不要使用using namespace std;.虽然教程是为了清楚起见,但专业程序员不会。

你应该使用map<int,size_t>int用于值,size_t用于计数。

如果需要实现代码,则应选择平衡二叉树以达到"log N"复杂性。

因此,您有以下节点:

struct Node
{
int key;
size_t count;
Node *left, *right; 
size_t leftNodesCount, rightNodesCount;
};

leftNodesCountrightNodesCount都表示平衡有多好,所以任何插入和删除都会改变它一直到根。 平衡树是当整个树 leftNodesCount 和 rightNodesCount 几乎相等(平均差不大于 1。 但您可以将容差设置为更高的值,如 2 或 3)

现在你应该实现InsertDeleteBalance方法。

要平衡平衡树,您应该旋转不平衡的节点,左旋转表示将节点替换为节点的右侧,并将节点添加到左侧,向右旋转是另一个方向上的相同操作。

平衡共谋也是"log N"。 请注意,在插入和删除之后,您应该以保持树的共谋的方式调用平衡,以使树的同谋与"log N"有关

内存管理是C++最重要的部分。事实上,访问连续内存比访问非连续内存快几倍。对于您的问题向量将比列表更有效。列表的问题是非连续的内存分配,这会导致大量缓存未命中。

这就是为什么专家说"尽可能避免列出清单">