在C++中,从已排序的整数向量中搜索和删除元素的最快方法

Fastest method of searching and removing elements from a sorted vector of integers in C++

本文关键字:删除 元素 方法 搜索 整数 C++ 排序 向量      更新时间:2023-10-16

我有一个排序整数的大向量。我需要快速查找并从数组中删除八个值。

例如,向量a包括元素

{1, 4, 7, 15, 16, 19, 24, 26, 31, 53, 67, 68, 73, 75, 77, 82}

向量b包括八个值

{4, 15, 19, 24, 67, 68, 73, 75}

完成操作后,矢量a现在应该具有

{1, 7, 16, 26, 31, 53, 77, 82}

我以前的解决方案很慢:

for (vector<int>::iterator val = b.begin(); val != b.end(); val++)
        a.erase(remove(a.begin(), a.end(), *val), a.end());

有没有更快的方法?

编辑:

实际上,我的"A"向量比我的"B"向量大很多。也许通过二进制搜索来搜索单个元素并删除它们更好?

第2版:

也许向量不是这种操作的好容器。我认为我不能使用forward_list,因为我不能用C++11进行编译。也许我可以使用不同的容器,然后将结果复制到向量中?

我可能会做这样的事情:

std::vector<int> temp;
std::set_difference(a.begin(), a.end(), 
                    b.begin(), b.end(),
                    std::back_inserter(temp));
std::swap(a, temp);

根据编辑的问题进行编辑:

假设你的a向量比b向量大得多,我会考虑第二个问题:完成后,你需要a保持排序吗?

如果允许重新排列a中的元素,那么您可以大幅提高速度:您可以将要删除的项目与a的最后一个元素交换,然后从末尾删除它(它具有恒定的复杂性),而不是执行删除/擦除操作来从a的中间删除项目。这使得删除量恒定,因此总体复杂度为O(N log M)(其中N=b.size(),M=a.size())

如果你必须保持秩序,你仍然可以在一定程度上提高速度:不是从a中删除元素,然后立即擦除删除的元素,而是执行std::remove_if以找到a中需要删除的所有元素,然后执行一次擦除以删除所有这些元素。

现在,对于您移除的a的每个元素,都使用对remove的单独调用。对于每个remove,复制(或移动,如果适用)每个移除点之后的a的所有元素。这意味着,如果从a中删除10个元素,则(平均)将a的一半复制10次。通过使用单个remove_if,可以只复制a的每个元素一次。

不幸的是,remove_if并不能为您提供一个利用b中的订购的好方法。你可以使用二进制搜索,这对一些人有帮助,但没有你想要的那么多。

如果你不介意写自己的循环,你可以充分利用ab都是排序的,比如

#include <vector>
#include <iostream>
// Compute the difference between two "set"s in-place. Each 'set' must be a
// sorted sequence.
//
template <class FwdIt, class InIt>
FwdIt 
inplace_set_difference(FwdIt b1, FwdIt e1, InIt b2, InIt e2) {
    FwdIt pos = b1;
    while (pos != e1 && b2 != e2) {
        if (*pos < *b2)
            *b1++ = *pos++;
        else if (*b2 < *pos)
            ++b2;
        else
            ++pos;
    }
    while (pos != e1)
        *b1++ = *pos++;
    return b1;
}
int main() { 
    std::vector<int> a{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 };
    std::vector<int> b{ 2, 5, 9 };
    auto it = inplace_set_difference(a.begin(), a.end(), b.begin(), b.end());
    a.erase(it, a.end());
    for (auto i : a)
        std::cout << i << 't';
}

如果你想让它最大限度地通用,你可能想把它改为只在迭代器上使用后增量,允许用户传递比较运算符,而不是直接使用<,等等。这些都是可怕的"读者练习"。

可能值得注意的是,这与set_difference通常使用的算法基本相同,只是进行了细微的调整即可操作。不过,这确实会导致接口发生实质性变化:因为它可以(而且确实)在集合中创建重复元素,所以它可以应用于排序序列(向量、deque、hash等),但不能应用于关联容器([unordered_][multi_](set|map))。

由于它分别正好遍历ab一次,所以它显然是O(N + M),但由于我们从M(=b.size())小的想法开始,它实际上是O(N)

注意:测试代码大量使用C++11特性(例如,初始化两个向量),但我认为算法实现本身在C++98/03中应该很好。

与其删除元素,不如将项添加到新向量中。假设您的输入是"original"(A)和"toRemove"(B),只需为original和toRemove:创建迭代器

如果原始中的下一个项目与toRemove中的下个项目匹配,则将其删除。否则,将其复制到结果中。如果它大于toRemove中的下一个项目,请转到toRemove的下一项目,然后再次运行比较。

通过这种方式,您只对每个列表迭代一次,而不是在"remove"操作期间不断复制数组值。

此解决方案将在O(A+B)中运行,这比您当前(和建议的)解决方案更快。

出于比较目的:您现有的解决方案大致为O(A*A*B)(A表示删除,A表示非优化搜索,B表示在B上迭代)
您的编辑建议进行二进制搜索以删除元素;这只会将您的原始解决方案改进为O(logA*A*B)(A用于删除,logA用于优化搜索,B用于在B上迭代)。

矢量头中的内置函数速度更快。例如,

向量vect;

在添加所有元素后,您可以使用

排序(vect.begin(),vect.end());

这将按升序对列表进行排序。对于下降,您可能需要执行

reverse(vect.begin,vect.end());

排序列表上。