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. 第6章 STL算法的高级使用方式

使用ASCII字符曼德尔布罗特集合

Previous计算两个vector的误差和Next实现分割算法

Last updated 6 years ago

Was this helpful?

1975年,数学家贝诺曼德尔布罗特(Benoit Mandelbrot)创造了一个术语——分形。分形是一个数学图像或者集合,这个术语中包含了很多有趣的数学特性,不过最后看起来分形更像是艺术品。分形图像看起来是无限重复的缩小。其中最为众人所知的分形是曼德尔布罗特(Mandelbrot)集合,其集合看起来就像下图一样:

曼德尔布罗特集合可以通过迭代下面的等式得到:

z和c变量都是复数。曼德尔布罗特集合包含等式所覆盖所有让方程收敛的c值,也就是海报彩色的部分。有些值收敛的早,有些值收敛的晚,这里用不同的颜色对这些值进行描述,所以我们能在海报中看到各种不同的颜色。对于那些不收敛的值,我们则直接将其所在区域直接涂黑。

使用STL的std::complex类,且不使用循环来实现上面的等式。这并不是炫技,只是为了让大家更容易理解STL相关特性的使用方式。

How to do it...

本节,我们将打印类似墙上海报的图,不过是使用ASCII字符将图像打印在终端上:

  1. 包含必要的头文件并声明所使用的命名空间:

    #include <iostream>
    #include <algorithm>
    #include <iterator>
    #include <complex>
    #include <numeric>
    #include <vector>
    
    using namespace std;
  2. 曼德尔布罗特集合和之前的等式,都是对复数进行操作。所以,我们需要使用类型别名,使用cmplx来代表std::complex,并特化为double类型:

       using cmplx = complex<double>;
  3. 我们将使用大约20行的代码来完成一个ASCII组成的曼德尔布罗特集合图像,不过我们会将逻辑逐步实现,最后将所有结果进行组合。第一步就是实现一个函数,用于将整型坐标缩放为浮点坐标。这也就是我们如何在屏幕上特定的位置上打印相应的字符。我们想要的是曼德尔布罗特集合中复数的坐标,就需要实现一个函数,用于将对应的坐标转换成相应的几何图形。用一个Lambda表达式来构建这些变量,并将其返回。该函数能将int类型的函数转换成一个double类型的函数:

    static auto scaler(int min_from, int max_from,
    double min_to, double max_to)
    {
        const int w_from {max_from - min_from};
        const double w_to {max_to - min_to};
        const int mid_from {(max_from - min_from) / 2 + min_from};
        const double mid_to {(max_to - min_to) / 2.0 + min_to};
    
        return [=] (int from) {
               return double(from - mid_from) / w_from * w_to + mid_to;
        };
    }
  4. 现在需要在一个维度上进行坐标变换,不过曼德尔布罗特集合使用的是二维坐标系。为了能将(x, y)坐标系统转换成另一个,我们需要将x-scaler和y-scaler相结合,并且构建一个cmplx实例作为输出:

    template <typename A, typename B>
    static auto scaled_cmplx(A scaler_x, B scaler_y)
    {
        return [=](int x, int y) {
            return cmplx{scaler_x(x), scaler_y(y)};
        };
    }
  5. 将坐标转换到正确的维度上后,就可以来实现曼德尔布罗特方程。现在不管怎么打印输出,一心只关注于实现方程即可。循环中,对z进行平方,然后加上c,知道abs的值小于2。对于一些坐标来说,其值永远不可能比2小,所以当循环次数达到max_iterations时,我们就决定放弃。最后,将会返回那些abs值收敛的迭代次数:

    static auto mandelbrot_iterations(cmplx c)
    {
        cmplx z {};
        size_t iterations {0};
        const size_t max_iterations {1000};
        while (abs(z) < 2 && iterations < max_iterations) {
            ++iterations;
            z = pow(z, 2) + c;
        }
        return iterations;
    }
  6. 那么现在我们就来实现主函数。在主函数中我们会定义缩放函数对象scale,用于对坐标值进行多维变换:

    int main()
    {
        const size_t w {100};
        const size_t h {40};
    
        auto scale (scaled_cmplx(
            scaler(0, w, -2.0, 1.0),
            scaler(0, h, -1.0, 1.0)
        ));
  7. 为了可以在一维上迭代器整个图形,需要完成另一个转换函数,用于将二维图像进行降维操作。其会根据我们所设置的字符宽度进行计算。其会将一维上的长度进行折断,然后进行多行显示,通过使用scale函数对坐标进行变换,然后返回复数坐标:

        auto i_to_xy ([=](int i) { return scale(i % w, i / w); });
  8. 我们将图像的二维坐标(int,int类型)转换为一维坐标(int类型),再将坐标转换成曼德尔布罗特结合坐标(cmplx类型)。让我们将所有功能放入一个函数,我们将使用一组调用链:

        auto to_iteration_count ([=](int i) {
            return mandelbrot_iterations(i_to_xy(i));
        });
  9. 现在我们可以来设置所有数据。假设我们的结果ASCII图像的字符宽度为w,高度为h。这样就能将结果存储在一个长度为w * h数组中。我们使用std::iota将数值范围进行填充。这些数字可以用来作为转换的输入源 ,我们将变换过程包装在to_iteration_count中:

        vector<int> v (w * h);
        iota(begin(v), end(v), 0);
        transform(begin(v), end(v), begin(v), to_iteration_count);
  10. 现在有一个v数组,其使用一维坐标进行初始化,不过后来会被曼德尔布罗特迭代计数所覆盖。因此,我们就可以对图像进行打印。可以将终端窗口设置为w个字符宽度,这样我们就不需要打印换行符。不过,可能会有对std::accumulate有一种创造性的误用。std::accumulate使用二元函数对处理范围进行缩小。我们可以对其提供一个二元函数,其能接受一个输出迭代器(并且我们将在下一步进行终端打印),并使用范围内的单个值进行计算。如果相应值的迭代次数大于50次时,我们会打印*字符到屏幕上。否则,会打印空字符在屏幕上。在每行结束时(因为计数器变量n可被W均匀地分割),我们会打印一个换行符:

        auto binfunc ([w, n{0}] (auto output_it, int x) mutable {
            *++output_it = (x > 50 ? '*' : ' ');
            if (++n % w == 0) { ++output_it = '\n'; }
            return output_it;
        });
  11. 通过对输入范围使用std::accumulate,我们将二元打印函数和ostream_iterator相结合,我们需要在屏幕上刷新计算出的曼德尔布罗特集合:

        accumulate(begin(v), end(v), ostream_iterator<char>{cout},
                  binfunc);
    }
  12. 编译并运行程序,就可以看到如下的输出,其看起来和墙上的海报很像吧!

How it works...

整个计算过程都使用std::transform对一维数组进行处理:

vector<int> v (w * h);
iota(begin(v), end(v), 0);
transform(begin(v), end(v), begin(v), to_iteration_count);

所以,会发生什么呢?我们为什么要这么做?to_iteration_count函数是基于从i_to_xy开始的调用链,从scale到mandelbrot_iterations。下面的图像就能展示我们的转换步骤:

这样,我们就可以使用一维数组作为输入,并且获得曼德尔布罗特方程的迭代次数(使用一维坐标表示的二维坐标上的值)。三个互不相关的转换是件好事。这样代码就可以独立的进行测试,这样就不用互相牵制了。同样,这样更容易进行正确性测试,并寻找并修复bug。