可用于存储和管理整数集合的最佳C++数据结构是什么
What is the best C++ data structure that could be used for storing and managing a collection of integers?
这是我的第一个 StackOverflow 问题,所以如果我没有遵循这个问题的社区准则,请告诉我,如果我应该删除它。
我收到了我的第一个面试问题,但由于我的实施,我被拒绝了。
问题是:
设计和实现存储整数集合的 C++ 类。在构造时,集合应为空。同一号码可以存储多次。
实现以下方法:
-
插入(整数 x)。 插入值"x"的条目。
-
擦除(int x). 从集合中删除一个值为"x"的条目(如果存在)。
-
擦除(int from, int to)。 删除值在 [from, to] 范围内的所有条目。
-
计数(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_bound
和upper_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;
};
leftNodesCount
和rightNodesCount
都表示平衡有多好,所以任何插入和删除都会改变它一直到根。 平衡树是当整个树 leftNodesCount 和 rightNodesCount 几乎相等(平均差不大于 1。 但您可以将容差设置为更高的值,如 2 或 3)
现在你应该实现Insert
、Delete
和Balance
方法。
要平衡平衡树,您应该旋转不平衡的节点,左旋转表示将节点替换为节点的右侧,并将节点添加到左侧,向右旋转是另一个方向上的相同操作。
平衡共谋也是"log N"。 请注意,在插入和删除之后,您应该以保持树的共谋的方式调用平衡,以使树的同谋与"log N"有关
内存管理是C++最重要的部分。事实上,访问连续内存比访问非连续内存快几倍。对于您的问题向量将比列表更有效。列表的问题是非连续的内存分配,这会导致大量缓存未命中。
这就是为什么专家说"尽可能避免列出清单">
- 在C#中处理C++指针而不使用unsafe的最佳方法
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 实现无开销push_back的最佳方法是什么
- 在c代码之间共享数据的最佳方式
- 使用std::source_location报告错误的最佳实践
- 派生类销毁的最佳实践是什么
- 将寄存器设计成可由C和C++访问的外设的最佳实践
- 在两台机器之间进行时间戳的最佳c++chrono函数是什么
- 使用QQuickFramebufferObject时同步数据的最佳方式是什么
- 在C++中向零方向近似的最佳方法
- 使用不同的CRT将新的C++代码与旧的(二进制)组件隔离开来的最佳方法是什么
- 从嵌套在std::映射中的std::列表中删除元素的最佳方式
- 如果条件为TRUE(最佳方式?),则在do while循环中后置增量
- 检测win32服务创建和删除的最佳方法
- 在reactor中存储eventHandlers的最佳方式是什么
- 在C++中样板"冷/never_inline"错误处理技术的最佳方法是什么?
- 在 c++ 中对类中的 c 字符串动态数组进行排序的最佳方法是什么?
- 在AVX通道中混洗的最佳方式
- 程序顶部的声明与定义(最佳实践)
- 别名模板的专业化 C++11 中没有开销的最佳替代方案