以O(1)的时间复杂度删除未排序std::vector中的元素

因为其他元素要填补删除元素所留下来的空隙,从而需要进行移动,所以从std::vector中删除元素的时间复杂度为O(n)。高效擦除/移除(Erase–remove idiom)std::vector元素
移动其他元素也与此类似,当很多很大或很复杂的元素需要移动,那么就会花费很长的时间。当不需要保证vector内元素顺序时,我们可以对其进行优化。

#include <iostream>
#include <vector>
#include <algorithm>

template <typename T>
void quick_remove_at(std::vector<T> &v, std::size_t idx)
{
    if (idx < v.size()) {
        v.at(idx) = std::move(v.back());
        v.pop_back();
    }
}

template <typename T>
void quick_remove_at(std::vector<T> &v, typename std::vector<T>::iterator it)
{
    if (it != std::end(v)) {
        *it = std::move(v.back());
        v.pop_back();
    }
}


int main()
{
    std::vector<int> v {123, 456, 789, 100, 200};

    quick_remove_at(v, 2);

    for (int i : v) {
        std::cout << i << ", ";
    }
    std::cout << '\n';

    quick_remove_at(v, std::find(std::begin(v), std::end(v), 123));

    for (int i : v) {
        std::cout << i << ", ";
    }
    std::cout << '\n';

    std::vector<int> vv;

    quick_remove_at(vv, 0);
}

  • 我们实现了两种quick_remove_at函数。具体实现代码中,需要注意与main函数的前后关系。两个函数都能接收一个vector实例的引用,所以这里允许用户使用各种类型的变量作为元素。对于我们来说,其类型就是T。第一个 quick_remove_at函数用来接收索引值,是一个具体的数,所以其接口如下所示:

    template <typename T>
    void quick_remove_at(std::vector<T> &v, std::size_t idx)
    {
    
  • 现在来展示一下本节的重点——如何在不移动其他元素的情况下,快速删除某个元素?首先,将vector中最后一个元素进行重写。第二,删除vector中最后一个元素。就这两步。我们的代码会对输入进行检查。如果输入的索引值超出了范围,函数不会做任何事情。另外,该函数会在传入空vector的时候崩溃。

        if (idx < v.size()) {
            v[idx] = std::move(v.back());
            v.pop_back();
        }
    }
    
  • 另一个quick_remove_at实现也很类似。用std::vector<T>的迭代器替换了具体的索引数值。因为泛型容器已经定义了这样的类型,所以获取它的类型并不复杂。

    template <typename T>
    void quick_remove_at(std::vector<T> &v,
                        typename std::vector<T>::iterator it)
    {
    
  • 现在我们来访问这些迭代器所指向的值。和另一个函数一样,我们会将最后一个元素进行重写。因为这次处理的是迭代器,所以我们要对迭代器指向的位置进行检查。如果其指向了一个错误的位置,我们就会阻止其解引用。

        if (it != std::end(v)) {
    
  • 在该代码块中,我们会做和之前一样的事情——我们要覆盖最后一个位置上的值——然后将最后一个元素从vector中剪掉。

            *it = std::move(v.back());
            v.pop_back();
        }
    }
    

quick_remove_at函数移除元素非常快,而且不需要动其他元素。这个函数使用了更加具有创造性的做法:这是一种与实际元素交换的方式,然后将最后一个元素从vector中删除。虽然,最后一个元素与选中的元素没有实际的关联,但是它在这个特别的位置上,而且删除最后一个元素的成本最低!vector的长度在删除完成后,也就减少1,这就是这个函数所要做的。并且无需移动任何元素。看一下下面的图,可能有助于你理解这个函数的原理。

在这里插入图片描述

完成这两步的代码如下:

v.at(idx) = std::move(v.back());
v.pop_back();

迭代器版本实现几乎一模一样:

*it = std::move(v.back());
v.pop_back();

逻辑上,我们将选定元素与最后一个元素进行交换。不过,在代码中元素并没有进行交换,代码直接使用最后一个值覆盖了选定元素的值。为什么要这样?当我们交换元素时,就需要将选定的元素存储在一个临时变量中,并在最后将这个临时变量中的值放在vector的最后。这个临时变量是多余的,而且要删除的值对于我们来说是没有意义的,所以这里选择了直接覆盖的方式,更加高效的实现了删除。

好了,交换是无意义的,覆盖是一种更好的方式。让我们来看下这个,当我们要获取vector最后元素的迭代器时,只需要简单的执行*it = v.back();就行了,对吧?完全正确,不过试想我们存储了一些非常长的字符串在vector中,或存储了另一个vectormap——这种情况下,简单的赋值将对这些值进行拷贝,那么就会带来非常大的开销。这里使用std::move可将这部分开销优化掉:比如字符串,指向堆内存上存储的一个大字符串。我们无需拷贝它。只需要移动这个字符串即可,就是将目标指针指向这块地址即可。移动源保持不变,不过出于无用的状态,这样做可以类似的让目标指针指向源指针所在的位置,然后将原始位置的元素删除,这样做即完成了元素移动,又免去了移动消耗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值