为什么erase()函数如此昂贵?

时间:2011-01-11 22:33:44

标签: c++ erase

考虑一个二维向量vector < vector <int> > N,让我们说它的内容如下:

1 1 1 1
2 2 2 2
3 3 3 3
4 4 4 4

所以这里N的大小是4,即N.size() = 4

现在,请考虑以下代码:

int i = 0;
while(N != empty()){
N.erase(i);
++i;
}

我只为这段代码计算了N的各种大小的时间,结果如下:

N的大小为1000 执行时间:0.230000s

N的大小是10000 执行时间:22.900000s

N的大小是20000 执行时间:91.760000s

N的大小是30000 执行时间:206.620000s

N的大小是47895 执行时间:526.540000s

我的问题是为什么这个功能如此昂贵?如果是这样,那么许多程序中的条件擦除语句可能会因为这个功能而永远存在。当我在std::map中使用擦除功能时也是如此。有没有替代这个功能。像Boost这样的其他图书馆会提供吗?

请不要说我可以N.erase()作为一个整体,因为我只是想分析这个功能。

6 个答案:

答案 0 :(得分:15)

考虑删除向量的第一个元素时会发生什么。向量的其余部分必须被一个索引“移动”,这涉及复制它。尝试从另一端擦除,看看是否有所作为(我怀疑它会...)

答案 1 :(得分:6)

因为你的算法是O(n ^ 2)。每次调用erase都会强制vector移回已擦除元素后的所有元素。所以在你的4元素向量循环中,第一个循环导致3个元素被移位,第二个迭代导致1个元素被移位,之后你有未定义的行为。

如果你有8个元素,第一个迭代将移动7个元素,下一个将移动5个元素,下一个将移动3个元素,最后的枚举将移动1个元素。 (再次,你有未定义的行为)

当遇到这种情况时,通常应该使用标准算法(即std::removestd::remove_if),因为它们在容器中运行一次并转换典型的O(n ^ 2)算法进入O(n)算法。有关更多信息,请参阅Scott Meyers的“有效STL”第43项:首选算法调用显式循环。

答案 2 :(得分:2)

std :: vector在内部只是一个元素数组。如果删除中间的元素,则必须将其后面的所有元素向下移动。这可能非常昂贵 - 如果元素具有可以完成大量工作的自定义operator=,则更是如此!

如果你需要erase()快,你应该使用std::list - 这将使用双向链表结构,允许从中间快速擦除(但是,其他操作变得稍慢)。如果您只需要快速从列表的 start 中删除,请使用std::deque - 这将创建一个数组的链接列表,并提供std::vector的大部分速度优势仍然允许从开始或结束快速擦除。

此外,请注意你的循环使问题变得更糟 - 你首先扫描所有等于零的元素并擦除它们。扫描需要O(n)时间,擦除也需要O(n)时间。然后重复1,依此类推 - 总体而言,O(n ^ 2)时间。如果需要擦除多个值,则应使用迭代器并使用erase()的迭代器变体自行浏览std::list。或者,如果您使用vector,您会发现将其复制到新的矢量中会更快。

至于std::map(和std::set) - 这根本不是问题。 std::map能够随机删除元素,以及随机搜索元素,O(lg n)时间 - 这对于大多数用途来说非常合理。即使你的天真循环也不应该太糟糕;在一次通过中手动迭代并删除要删除的所有内容会更有效率,但与std::list和朋友的差距不大。

答案 3 :(得分:1)

vector.erase会在i前进后将所有元素前进1.这是一个O(n)操作。

此外,您按值而不是通过引用传递矢量。

您的代码也不会删除整个矢量。

例如: i = 0 擦除N [0] N = {{2,2,2,2},{3,3,3,3},{4,4,4,4}}

i = 1 擦除N [1] N = {{2,2,2,2},{4,4,4,4}}

i = 2 擦除N [2]没有任何反应,因为最大索引是N [1]

最后,我认为这是vector.erase()的正确语法。您需要将迭代器传递到开始位置以擦除所需的元素。 试试这个:

vector<vector<int>> vectors; // still passing by value so it'll be slow, but at least erases everything
for(int i = 0; i < 1000; ++i)
{
    vector<int> temp;
    for(int j = 0; j < 1000; ++j)
    {
        temp.push_back(i);
    }
    vectors.push_back(temp);
}

// erase starting from the beginning
while(!vectors.empty())
{
    vectors.erase(vectors.begin());
}

您还可以将其与结尾的擦除进行比较(它应该明显更快,尤其是在使用值而不是引用时):

// just replace the while-loop at the end
while(!vectors.empty())
{
    vectors.erase(vectors.end()-1);
}

答案 4 :(得分:0)

向量是一个在向其添加元素时自动增长的数组。因此,向量中的元素在存储器中是连续的。这允许对元素进行恒定的时间访问。因为它们从最后开始增长,所以它们还需要摊销不变的时间来添加或删除结尾。

现在,当您在中间移除时会发生什么?嗯,这意味着在擦除的元素必须向后移动一个位置之后存在的任何东西。这非常昂贵。

如果你想在中间插入/删除大量的内容,请使用链接列表,例如std :: list of std :: deque。

答案 5 :(得分:0)

正如Oli所说,从向量的第一个元素中删除意味着必须向下复制它后面的元素,以便数组按照需要运行。

这就是为什么链接列表用于从列表中的随机位置删除元素的情况 - 它更快(在较大的列表上),因为没有复制,只重置一些节点指针。

相关问题