C++17 STL Cook Book
  • Introduction
  • 前言
  • 关于本书
  • 各章梗概
  • 第1章 C++17的新特性
    • 使用结构化绑定来解包绑定的返回值
    • 将变量作用域限制在if和switch区域内
    • 新的括号初始化规则
    • 构造函数自动推导模板的类型
    • 使用constexpr-if简化编译
    • 只有头文件的库中启用内联变量
    • 使用折叠表达式实现辅助函数
  • 第2章 STL容器
    • 擦除/移除std::vector元素
    • 以O(1)的时间复杂度删除未排序std::vector中的元素
    • 快速或安全的访问std::vector实例的方法
    • 保持对std::vector实例的排序
    • 向std::map实例中高效并有条件的插入元素
    • 了解std::map::insert新的插入提示语义
    • 高效的修改std::map元素的键值
    • std::unordered_map中使用自定义类型
    • 过滤用户的重复输入,并以字母序将重复信息打印出——std::set
    • 实现简单的逆波兰表示法计算器——std::stack
    • 实现词频计数器——std::map
    • 实现写作风格助手用来查找文本中很长的句子——std::multimap
    • 实现个人待办事项列表——std::priority_queue
  • 第3章 迭代器
    • 建立可迭代区域
    • 让自己的迭代器与STL的迭代器兼容
    • 使用迭代适配器填充通用数据结构
    • 使用迭代器实现算法
    • 使用反向迭代适配器进行迭代
    • 使用哨兵终止迭代
    • 使用检查过的迭代器自动化检查迭代器代码
    • 构建zip迭代适配器
  • 第4章 Lambda表达式
    • 使用Lambda表达式定义函数
    • 使用Lambda为std::function添加多态性
    • 并置函数
    • 通过逻辑连接创建复杂谓词
    • 使用同一输入调用多个函数
    • 使用std::accumulate和Lambda函数实现transform_if
    • 编译时生成笛卡尔乘积
  • 第5章 STL基础算法
    • 容器间相互复制元素
    • 容器元素排序
    • 从容器中删除指定元素
    • 改变容器内容
    • 在有序和无序的vector中查找元素
    • 将vector中的值控制在特定数值范围内——std::clamp
    • 在字符串中定位模式并选择最佳实现——std::search
    • 对大vector进行采样
    • 生成输入序列的序列
    • 实现字典合并工具
  • 第6章 STL算法的高级使用方式
    • 使用STL算法实现单词查找树类
    • 使用树实现搜索输入建议生成器
    • 使用STL数值算法实现傅里叶变换
    • 计算两个vector的误差和
    • 使用ASCII字符曼德尔布罗特集合
    • 实现分割算法
    • 将标准算法进行组合
    • 删除词组间连续的空格
    • 压缩和解压缩字符串
  • 第7章 字符串, 流和正则表达
    • 创建、连接和转换字符串
    • 消除字符串开始和结束处的空格
    • 无需构造获取std::string
    • 从用户的输入读取数值
    • 计算文件中的单词数量
    • 格式化输出
    • 使用输入文件初始化复杂对象
    • 迭代器填充容器——std::istream
    • 迭代器进行打印——std::ostream
    • 使用特定代码段将输出重定向到文件
    • 通过集成std::char_traits创建自定义字符串类
    • 使用正则表达式库标记输入
    • 简单打印不同格式的数字
    • 从std::iostream错误中获取可读异常
  • 第8章 工具类
    • 转换不同的时间单位——std::ratio
    • 转换绝对时间和相对时间——std::chrono
    • 安全的标识失败——std::optional
    • 对元组使用函数
    • 使用元组快速构成数据结构
    • 将void*替换为更为安全的std::any
    • 存储不同的类型——std::variant
    • 自动化管理资源——std::unique_ptr
    • 处理共享堆内存——std::shared_ptr
    • 对共享对象使用弱指针
    • 使用智能指针简化处理遗留API
    • 共享同一对象的不同成员
    • 选择合适的引擎生成随机数
    • 让STL以指定分布方式产生随机数
  • 第9章 并行和并发
    • 标准算法的自动并行
    • 让程序在特定时间休眠
    • 启动和停止线程
    • 打造异常安全的共享锁——std::unique_lock和std::shared_lock
    • 避免死锁——std::scoped_lock
    • 同步并行中使用std::cout
    • 进行延迟初始化——std::call_once
    • 将执行的程序推到后台——std::async
    • 实现生产者/消费者模型——std::condition_variable
    • 实现多生产者/多消费者模型——std::condition_variable
    • 并行ASCII曼德尔布罗特渲染器——std::async
    • 实现一个小型自动化并行库——std::future
  • 第10章 文件系统
    • 实现标准化路径
    • 使用相对路径获取规范的文件路径
    • 列出目录下的所有文件
    • 实现一个类似grep的文本搜索工具
    • 实现一个自动文件重命名器
    • 实现一个磁盘使用统计器
    • 计算文件类型的统计信息
    • 实现一个工具:通过符号链接减少重复文件,从而控制文件夹大小
Powered by GitBook
On this page
  • How to do it...
  • How it works...

Was this helpful?

  1. 第2章 STL容器

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

因为其他元素要填补删除元素所留下来的空隙,从而需要进行移动,所以从std::vector中删除元素的时间复杂度为O(n)。

移动其他元素也与此类似,当很多很大或很复杂的元素需要移动,那么就会花费很长的时间。当无法保证顺序时,我们需要对其进行优化,这就是本节的内容。

How to do it...

我们继续使用一些数字来填充std::vector实例,并且实现一个快速删除函数,以O(1)的时间复杂度删除vector中的元素。

  1. 首先,包含必要的头文件:

    #include <iostream>
    #include <vector>
    #include <algorithm>
  2. 定义主函数,并定义一个vector实例:

    int main(){
        std::vector<int> v{123, 456, 789, 100, 200};
  3. 下一步就要删除索引为2的元素(789)。我们所要用的来删除元素的函数在后面进行实现,我们先假设已经实现好了。执行完成后,来看下vector中的内容。

        quick_remove_at(v, 2);
        for (int i : v){
            std::cout << i << ", ";
        }
        std::cout << '\n';
  4. 现在,我们将删除另外一个元素。我们想删除123,但是要假装不知道其索引。因此,我们要使用std::find函数在vector的合法范围内查找这个值,并返回其位置信息。得到索引信息后,我们就可以用quick_remove_at将对应元素删除了,这里所使用到的是一个重载版本,能接受迭代器作为输入参数。

        quick_remove_at(v, std::find(std::begin(v), std::end(v), 123));
        for (int i : v) {
               std::cout << i << ", ";
        }
        std::cout << '\n';
    }
  5. 我们实现了两种quick_remove_at函数。具体实现代码中,需要注意与main函数的前后关系。两个函数都能接收一个vector实例的引用,所以这里允许用户使用各种类型的变量作为元素。对于我们来说,其类型就是T。第一个 quick_remove_at函数用来接收索引值,是一个具体的数,所以其接口如下所示:

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

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

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

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

            *it = std::move(v.back());
            v.pop_back();
        }
    }
  10. 这就完事了。让我们来编译程序,并运行:

    $ ./main
    123, 456, 200, 100,
    100, 456, 200,

How it works...

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中,或存储了另一个vector或map——这种情况下,简单的赋值将对这些值进行拷贝,那么就会带来非常大的开销。这里使用std::move可将这部分开销优化掉:比如字符串,指向堆内存上存储的一个大字符串。我们无需拷贝它。只需要移动这个字符串即可,就是将目标指针指向这块地址即可。移动源保持不变,不过出于无用的状态,这样做可以类似的让目标指针指向源指针所在的位置,然后将原始位置的元素删除,这样做即完成了元素移动,又免去了移动消耗。

Previous擦除/移除std::vector元素Next快速或安全的访问std::vector实例的方法

Last updated 6 years ago

Was this helpful?