【问题标题】:What is the best C++ data structure that could be used for storing and managing a collection of integers?可用于存储和管理整数集合的最佳 C++ 数据结构是什么?
【发布时间】:2018-12-17 14:15:54
【问题描述】:

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

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

问题是:

设计并实现一个存储整数集合的 C++ 类。在构建时,集合应该是空的。同一个号码可以存储不止一次。

实现以下方法:

  1. 插入(int x)。为值“x”插入一个条目。

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

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

  4. 计数(整数从,整数到)。计算有多少条目的值在 [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) 复杂度的解决方案的候选人。

【问题讨论】:

  • FWIW, std::list 通常不是您要使用的数据结构。对于您描述的用例,std::multiset 听起来像您想要的。几乎所有的操作都是O(logN),这还不错。
  • 你的似乎是 O(n),比 O(nlogn) 好
  • @ben 是的,但想象一下他有一个包含 n 项的 IntegerCollection,他想通过为每个元素调用擦除来擦除所有这些。这是每个元素的线性运算,使得时间复杂度呈二次方。
  • 这是一个精心设计的问题,但我怀疑它主要基于意见。我不确定我们中的任何人都可以告诉你面试官认为哪种实施方式是“最好的”。
  • 我的意思是,大 O 在这里具有误导性,因为它是一个玩具示例,但关键是我怀疑答案被拒绝是因为它很幼稚,而不是因为它实际上是错误的。使用列表存储整数在内存和可能主导大 O 的现代架构中效率低下。

标签: c++ collections integer


【解决方案1】:

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

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

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

由于std::map是有序的,所以可以使用lower_boundupper_bound进行二分查找,所以在[from, to)中查找key非常高效(查找范围也是O(log(distinct_values_in_container)))。擦除它们或将它们的计数相加很容易(运行时在这里更复杂)。


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

这些渐近运行时在实践中的含义在很大程度上取决于使用模式。如果没有重复插入,我们在O(n),但如果有很多相同的元素(例如,如果每个键都有@ 987654329@ 值然后O(distinct_values_in_container) = O(log(n)))。在所有涉及的整数都相同的极端情况下,所有操作都是O(1)

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


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

【讨论】:

    【解决方案2】:

    如果您被聘用的职位不需要任何以前的编程经验,那么我不会仅凭该代码示例就拒绝您。

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

    保留,但绝不是红线:

    整数并不意味着int。在无法澄清的情况下,将您的课程建立在

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

    其中Y 是一个整数类型。请注意使用std::size_t 作为计数器。它计算特定Y 出现的次数。

    下次添加一些程序cmets。

    不要使用using namespace std;。尽管教程是为了清晰起见,但专业程序员却不是。

    【讨论】:

    • 感谢您的建议。我没有在这里包含 cmets,但我将它们包含在我提交的代码中。从现在开始,我不会使用命名空间 std。你能澄清一下“大招聘旗”是什么意思吗?我应该使用结构做一个链表吗?
    • 另外,请参阅 this answer 了解 为什么 using namespace std; 是不好的做法。
    • 你能说明为什么使用 STL 容器是一个标志吗?在这里做这件事似乎很聪明。从问题陈述来看,除了操作参数之外什么都没有给出,那么为什么要重新发明轮子呢?您可以节省公司时间并用它编写更好的代码。如果我知道我可以在有限的系统上节省资源或使其更快地用于小众用例,我只会重新发明轮子。
    • @TechnoSam 上面写着“雇佣旗帜”,这是好事而不是坏事。
    • 确实如此:雇用旗帜是件好事!很抱歉造成混乱。
    【解决方案3】:

    您应该使用map&lt;int,size_t&gt; 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'

    【讨论】:

      【解决方案4】:

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

      这就是为什么专家说“尽可能避免列出”

      【讨论】:

      • std::vector 在这方面可能比std::list 更好,但它的渐近复杂度比现有答案更差。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-20
      • 1970-01-01
      相关资源
      最近更新 更多