STL常用法,备忘

567 阅读24分钟

前言

这个应该是看封面有C++17的开源书的笔记,书名《C++17 STL Cook book》

vector

一直比较迷糊的问题:容器中元素的填充是按复制进行的,所以容纳的类型要提供复制构造函数

std::remove_if( , , 谓词函数),最大优点就是可自定义谓词函数

它是将谓词函数返回为真的数移除,返回新的尾后迭代器,但并没将容器中尾后迭代器之后多余的元素删除
可根据该返回的迭代器行删除操作
紧跟着容器可作重新调整大小的操作

#include <iostream>  
#include <vector>  
#include <algorithm>  
using namespace std;

vector<int> v{1, 2, 3, 2, 5, 2, 6, 2, 4, 8};

const auto new_end(remove(begin(v), end(v), 2)); 
v.erase(new_end, end(v));

// 自定义谓词函数,自定义删除器,只删除奇数
const auto odd([](int i){return i % 2 != 0;});
v.erase(remove_if(begin(v), end(v), odd), end(v));
v.shrink_to_fit();

要想尽量简洁地删除元素,如下条件;因为std::remove()针对何种容器都是一样,元素虽然移走了,但是容器大小不变,容器尾部仍然会有残留;线性容器的成员函数版remove()等效于std::remove()

  • 1 非线性容器比如 list
  • 2 调用成员函数版的remove() 或 remove_if()

vector容器中不移动元素的情况下删除某元素

  • 1 要点:旧位置覆盖用一个右值对象
  • 2 要点:迭代器模板类型,学用typename
    template <typename T>
    void quick_remove_at(std::vector<T> &v, std::size_t idx)
    {
        if (idx < v.size()) {
            v[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();
        }
    }   
    

学习try catch用法

try {
	std::cout << "Out of range element value: "
        	  << v.at(container_size + 10) << '\n';
} catch (const std::out_of_range &e) {
	std::cout << "Ooops, out of range access detected: "
              << e.what() << '\n';
}

std::sort()并非所有容器都支持,该函数所对应的算法需要容器为可随机访问容器,例如std::list就无法进行排序。

  • 判断容器是否是有序
    std::is_sorted(std::begin(vec), std::end(vec));
    
  • 如果容器本身已经按升序排布,现在自定义一个有序的插入函数
    函数将会找到大于或等于第三个参数的首个位置,然后返回指向这个位 置的迭代器
    template <typename C, typename T>
    void insert_sorted(C &v, const T &item)
    {
        const auto insert_pos (lower_bound(begin(v), end(v), item));
        v.insert(insert_pos, item);
    }
    

map

Q:用map来统计各个国家的富豪数量,输入是世界各国的富豪信息

  • 1 学习的点:函数的返回值是std::pair<iterator, bool>类型,该如何承接返回值?
  • 2 std::map::try_emplace() 如果容器中已经存在该键,那么就不能再构建与这个已知的键所同名的键值对.这么做的目的是:起判断作用,该键已存在就不要再修改该键对应的值了
    // 包含该头文件,否则报错:"“<<”: 没有找到接受“std::string”类型的右操作数的运算符"
    #include <string>
    list<billionaire> billionaires {
        {"Bill Gates", 86.0, "USA"},
        {"Warren Buffet", 75.6, "USA"},
        {"Jeff Bezos", 72.8, "USA"},
        {"Amancio Ortega", 71.3, "Spain"},
        {"Mark Zuckerberg", 56.0, "USA"},
        {"Carlos Slim", 54.5, "Mexico"},
        // ...
        {"Bernard Arnault", 41.5, "France"},
        // ...
        {"Liliane Bettencourt", 39.5, "France"},
        // ...
        {"Wang Jianlin", 31.3, "China"},
        {"Li Ka-shing", 31.2, "Hong Kong"}
        // ...
    };
    
    map<string, pair<const billionaire, size_t>> m;
    
    for (const auto &b : billionaires) {
        auto [iterator, success] = m.try_emplace(b.country, b, 1);
        // 如果该国的首富信息已经保存到map容器中,那么就不再构造{国名, {富豪信息, 1}}这种键值对,而是将1+=增加计数;
        if (!success) {
            iterator->second.second += 1;
        }
    }
    
    for (const auto & [key, value] : m) {
        const auto &[b, count] = value;
        cout << b.country << " : " << count << " billionaires. Richest is "
             << b.name << " with " << b.dollars << " B$\n";
    }    
    

Note:

std::mapinsertemplace方法完全相同。try_emplace与它们不同的地方在于,在遇到已经存在的键时,不会去构造组对。当相应对象的类型需要很大开销进行构造时,这对于程序性能是帮助的。

Q:map中的键要修改,如何弄?

C++17之前,因为对应的键已经存在,我们不得不将整个键-值对从树中移除,然后再插入。这种方法的确定很明显,其需要分配出一些不必要的内存,从C++17起,我们无需重新分配内存,就可以删除和重新插入map键值对。先把键值对从map中分离出来(map::size()可观察到变化),然后对ket_type赋值,再将键值对插入map

  • 学习要点,学习extract()函数使用:
    当使用第二种方式去提取一个不存在的节点时,会返回一个空node_type实例。empty()成员函数会返回一个布尔值,用来表明node_type实例是否为空。以任何方式访问一个空的实例都会产生未定义行为。
    node_type extract(const_iterator position);
    node_type extract(const key_type& x)
    
  • 学习要点:std::move,因为node_type只有移动构造,并没有拷贝构造函数
    map<int, string> race_placement {
        {1, "Mario"}, {2, "Luigi"}, {3, "Bowser"},
        {4, "Peach"}, {5, "Yoshi"}, {6, "Koopa"},
        {7, "Toad"}, {8, "Donkey Kong Jr."}
    };
    
    // 分离键值对
    auto a (race_placement.extract(3));  
    // 改动Key_type
    a.key() = 17;  
    // 重新插入,注意,用移动构造  
    race_placement.insert(std::move(a));
    

unordered_map

如果键可以排序--> 选用map; 键不可排序,比如盛放自定义的类型(structclass) --> 选用unordered_map

  • 要点:选用unordered_map,要重载判等操作符
  • 为了使用STL哈希的能力,打开了std命名空间,并且创建了一个特化的std::hash模板。其使用using将特化类型进行别名
  • 重载该类型的括号表达式。我们只是为coord结构体添加数字,这是一个不太理想的哈希方式,不过这里只是展示如何去实现这个函数。一个好的散列函数会尽可能的将值均匀的分布在整个取值范围内,以减少哈希碰撞。
    template <>
    struct hash<coord>
    {
        using argument_type = coord;
        using result_type   = size_t;
    
        result_type operator()(const argument_type &c) const
        {
            return static_cast<result_type>(c.x)
                 + static_cast<result_type>(c.y);
        }
    };
    
    完整示例
    #include <iostream>
    #include <unordered_map>
    
    struct coord {
        int x;
        int y;
    };
    
    bool operator==(const coord &l, const coord &r)
    {
        return l.x == r.x && l.y == r.y;
    }
    
    namespace std
    {
    template <>
    struct hash<coord>
    {
        using argument_type = coord;
        using result_type   = size_t;
    
        result_type operator()(const argument_type &c) const
        {
            return static_cast<result_type>(c.x)
                 + static_cast<result_type>(c.y);
        }
    };
    
    }
    
    int main()
    {
        std::unordered_map<coord, int> m {{{0, 0}, 1}, {{0, 1}, 2}, {{2, 1}, 3}};
    
        for (const auto & [key, value] : m) {
            std::cout << "{(" << key.x << ", " << key.y << "): " << value << "} ";
        }
        std::cout << "\n";
    }
    

Note 通常实例化一个基于哈希的map表(比如: std::unordered_map)时,我们会这样写:

std::unordered_map<key_type, value_type> my_unordered_map;

编译器为我们创建特化的std::unordered_map时,这句话背后隐藏了大量的操作。所以,让我们来看一下其完整的模板类型声明:

template<
    class Key,
    class T,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator< std::pair<const Key, T> >
> class unordered_map;

这里第一个和第二个模板类型,我们填写的是coordint。另外的三个模板类型是选填的,其会使用已有的标准模板类。对于这个例子,class Hash模板参数是最有趣的一个:当我们不显式定义任何东西时,其就指向std::hash<key_type>。STL已经具有std::hash的多种特化类型,比如std::hash<std::string>std::hash<int>std::hash<unique_ptr>等等。这些类型中可以选择最优的一种类型类解决对应的问题。
不过,STL不知道如何计算我们自定义类型coord的哈希值。所以我们要使用我们定义的类型对哈希模板进行特化编译器会从std::hash特化列表中,找到我们所实现的类型,也就是将自定义类型作为键的类型
如果新特化一个std::hash<coord>类型,并且将其命名成my_hash_type,我们可以使用下面的语句来实例化这个类型:

std::unordered_map<coord, value_type, my_hash_type> my_unordered_map;

这样命名就很直观,可读性好,而且编译器也能从哈希实现列表中找到与之对应的正确的类型。


std::set 过滤用户的重复输入,并以字母序将重复信息打印出——std::set

  • 学习流迭代器, 适配器,将流对象包装成迭代器
  • 程序中有两个有趣的部分。第一个是使用了std::istream_iterator来访问用户输入,另一个是将std::set实例使用std::inserter用包装后,在使用std::copy填充
  • 流迭代器比较迷糊,剖析 :std::istream_iterator只传入了一个模板参数。也就我们输入数据的类型。我们选择std::string是因为我们假设是文本输入,不过这里也可以是float型的输入。基本上任何类型都可以使用cin >> var;完成。构造函数接受一个istream实例。标准输入使用全局输入流std::cin表示,例子中其为istream的参数。
  • 学习std::copy() ;及这几个函数back_inserter front_inserter(对随机访问容器支持,迭代器指针能随机移动的;比如list就不是) inserter ;调用std::copy时,我们使用itend作为输入迭代器。第三个参数必须是一个输出迭代器。因此,不能使用s.begin()s.end()。一个空的set中,这二者是一致的,所以不能对set的迭代器进行解引用(无论是读取或赋值)。
    这就使std::inserter有了用武之地。其为一个函数,返回一个std::insert_iterator,返回值的行为类似一个迭代器,不过会完成普通迭代器无法完成的事。当对其使用加法时,其不会做任何事。当我们对其解引用,并赋值给它时,它会连接相关容器,并且将赋值作为一个新元素插入容器中
  • 输出流重定向到输出迭代器上
    // 例子
    ostream_iterator<int> out_print{cout, ","};
    copy(begin(容器对象), end(容器对象), out_print);
    // 抑或例子  
    map<int, string> m;
    auto shell_it(ostream_iterator<pair<int, string>>{cout, ", "});
    copy(begin(m), end(m), shell_it);
    
  • 完整源码
    // echo "a a a b c foo bar foobar foo bar bar" | ./program         
    #include <iostream>
    #include <set>
    #include <string>
    #include <iterator> // for ostream_iterator
    
    using namespace std;
    
    int main()
    {
            set<string> s;
    
            istream_iterator<string> it{ cin };
            istream_iterator<string> end;
    
            copy(it, end, inserter(s, s.end()));
    
            for (const auto word : s) {
                    cout << word << ", ";
            }
            cout << '\n';
    }
    

stack

  • 运算符后置<-->逆波兰数,栈实现表达式计算求值
    • 无脑将数push进栈
    • 直到遇到运算符,则将该运算符前的两个数pop出,并计算,将计算结果push进栈
    • 继续无脑push数
  • 首先区分一个字符串是数还是操作数这件事比较有趣
    double evaluate_rpn(IT it, IT end)
    {
        for (; it != end; ++it) {
            stringstream ss{ *it };
            // 能被定向成double,它的返回值就是true 
            if (double val; ss >> val) {
                std::cout << "val is double: " << val << std::endl;
                val_stack.push(val);		
            }
            else {
            // 无法被定向成double,那它就是string,当作操作符
                std::cout << "val is string: " << *it << std::endl;  
                ...
    
  • 学习把函数装进容器里,形式表现为装函数指针
  • 学习匿名lambda写法,归纳性强,不散乱
  • 我们使用try代码块将计算代码包围,因为我们的计算可能会出错。在调用map的成员函数at时,可能会抛出一个out_of_range异常,由于用户具体会输入什么样的表达式,并不是我们能控制的。所以,我们将会重新抛出一个不同的异常,我们称之为invalid argument异常,并且携带着程序未知的操作符
    try {
            val_stack.push(ops.at(*it)(l, r));
    }
    catch (const out_of_range &) {
            throw invalid_argument(*it);
    }
    
    完整代码
    #include <iostream>
    #include <stack>
    #include <iterator>
    #include <map>
    #include <sstream>
    #include <cassert>
    #include <vector>
    #include <stdexcept>
    #include <cmath>
    #include <stdio.h>
    using namespace std;
    
    template <typename IT>
    double evaluate_rpn(IT it, IT end)
    {
        stack<double> val_stack;
        
        // 函数装进容器内
        map<string, double(*)(double, double)> ops{
                {"+", [](double a, double b) { return a + b; }},
                {"-", [](double a, double b) { return a - b; }},
                {"*", [](double a, double b) { return a * b; }},
                {"/", [](double a, double b) { return a / b; }},
                {"^", [](double a, double b) { return pow(a, b); }},
                {"%", [](double a, double b) { return fmod(a, b); }},
        };
        
        // 封装一下,本身的pop()不会返回值,稍加完善
        auto pop_stack([&]() { auto r(val_stack.top()); val_stack.pop(); return r; });
    
        for (; it != end; ++it) {
                stringstream ss{ *it };
    
                // 这里判断是操作数还是操作符比较巧
                if (double val; ss >> val) {
                    val_stack.push(val);		
                }
                else {
                    assert(val_stack.size() >= 2);
    
                    const auto r{ pop_stack() };
                    const auto l{ pop_stack() };
    
                    try {
                            val_stack.push(ops.at(*it)(l, r));
                    }
                    catch (const out_of_range &) {
                            throw invalid_argument(*it);
                    }
                }
            }
        return val_stack.top();
    }
    
    int main()
    {
        stringstream s{ "3 2 1 + * 2 /" };
        cout << evaluate_rpn(istream_iterator<string>{s}, {}) << '\n';
    
        vector<string> v{ "3", "2", "1", "+", "*", "2", "/" };
        cout << evaluate_rpn(begin(v), end(v)) << '\n';
    }
    

map和vector 实现词频计数,map计数,vector辅助实现键值对的值排序

  • 比较出最大数

    int max_word_len{ 0 }; // 初值  
    max_word_len = max<int>(max_word_len, filtered.length()); // 输入一个数,比较出最大数
    
  • 字符串的过滤,截断

  • map用作计数器

  • map中的值排序,拷贝到vector中自定义排序

  • 自定义降序排序

  • 注意,容器的拷贝,向vectorback_inserter(word_counts)
    std::move()能避免不必要的拷贝,将源容器整个搬移到目的容器;如果要搬移的元素非常多move()就可大显身手

    #include <iostream>
    #include <map>
    #include <vector>
    #include <algorithm>
    #include <iomanip>
    #include <string>
    #include <stdio.h>
    
    using namespace std;
    
    // 字符串筛选与截断
    string filter_punctuation(const string &s)
    {
            const char * forbidden{ ".,:; " };
            const auto idx_start(s.find_first_not_of(forbidden));
            const auto idx_end(s.find_last_not_of(forbidden));
    
            return s.substr(idx_start, idx_end - idx_start + 1);
    }
    
    int main()
    {
            map<string, size_t> words;
            int max_word_len{ 0 };
    
            string s;
            while (cin >> s) {
                    if ("q" == s) break;
                    auto filtered(filter_punctuation(s));
                    max_word_len = max<int>(max_word_len, filtered.length());
                    // 精华,初始化值为0;
                    ++words[filtered];
            }
    
            printf("max_word_len: %d \n", max_word_len);
            
            // 准备容器来"模拟"map,因为与map一样,元素都是pair<string, size_t>
            vector<pair<string, size_t>> word_counts;
            word_counts.reserve(words.size());
    
            // move(begin(words), end(words), back_inserter(word_counts));
            copy(begin(words), end(words), back_inserter(word_counts));
    
            // Get the most frequent words to the front
            // 自定义降序排序法
            sort(begin(word_counts), end(word_counts),
                    [](const auto &a, const auto &b) { return a.second > b.second; });
    
            cout << "# " << setw(max_word_len) << "<WORD>" << " #<COUNT>\n";
            for (const auto &[word, count] : word_counts) {
                    cout << setw(max_word_len + 2) << word << " #" << count << '\n';
            }
            return 0;
    }
    

统计接收字符串(文本内容)句子的长度(单词个数为单位) multimap

  • 涉及到用迭代器作为while循环的判断,涉及到迭代器的更新
  • 字符串中根据空格数来计算单词个数,要点在于统计空格个数
  • 充分利用临时对象,如果要将对象保存到容器,首先看是否是临时对象,是的话一举两得,将临时对象std::move
  • 如何排除字符串中不感兴趣的字符
    #include <iostream>
    #include <iterator>
    #include <map>
    #include <algorithm>
    
    using namespace std;
    
    // 排除不感兴趣的字符,输出一个子串
    string filter_ws(const string &s)
    {
        const char *ws {" \r\n\t"};
        const auto a (s.find_first_not_of(ws));
        const auto b (s.find_last_not_of(ws));
        if (a == string::npos) {
            return {};
        }
        return s.substr(a, b - a + 1);
    }
    
    multimap<size_t, string> get_sentence_stats(const string &content)
    {
    
        multimap<size_t, string> ret;
    
        const auto end_it (end(content));
        auto it1 (begin(content));         // start at beginning of string
        auto it2 (find(it1, end_it, '.')); // Start at first '.' dot character
    
        // end_it 迭代器毋需更新,永远指向容器的尾后迭代器  
        // it1 需要更新,即句子的首迭代器, 它总是逐渐从左向尾部移动, 它的更新结果和我自己想的有差别,差一  
        // it2 需要更新,句子的尾迭代器,即指向句号.;
        while (it1 != end_it && distance(it1, it2) > 0) {
    
            string s {filter_ws({it1, it2})};
    
            if (s.length() > 0) {
                // 巧妙的办法, 统计字符串中的空格数
                const auto words (count(begin(s), end(s), ' ') + 1);
                ret.emplace(make_pair(words, move(s)));
            }
    
            if (it2 == end_it) {
                // Need to get out here, because the next line would set it1
                // _past_ end_it.
                break;
            }
            // 更新it1 迭代器,我自己只想到it2, it2 实际指向句号(.) 加一是句号后的字符
            it1 = next(it2, 1);
            // 找到it1 到尾后迭代器之间的第一个句号
            it2 = find(it1, end_it, '.');
        }
    
        return ret;
    }
    
    int main()
    {
        // Note that using istreambuf_iterator may be faster for reading
        // the whole file in one step.
        //
        // For reading large files it is also better to know
        // their size in advance in order to reserve the space in memory.
        // This way the buffer does not need to grow which brings a lot of
        // reallocations. See also Chapter10/dupe_compress.cpp, where
        // we do it like this.
    
        cin.unsetf(ios::skipws);
        string content {istream_iterator<char>{cin}, {}};
    
        for (const auto & [word_count, sentence]
                : get_sentence_stats(content)) {
            cout << word_count << " words: " << sentence << ".\n";
        }
    }
    

deque 非静态线性,元素默认是非排序,也就是说非随机访问迭代器


自定义的容器要实现范围for循环

  • 首先,逻辑是:自定义容器要提供being(),和end()方77法.原因是std::begin()std::end()会调用成员函数版的begin()``end()
    • begin() end()会返回迭代器;该迭代器对象的方法至少要提供:
      • operator*()
      • operator++() 前置自加
      • operator!=() 以上三种是范围for展开时实际会涉及到的操作

shrink_to_fit()内存自适应(缩小到最小程度,但不一定刚好是size()),

  • begin() end() 得到的迭代器必须即取即用,如果容器的size变了,插入新元素,那旧的迭代器指向谁?

lambda可以理解成可调对象类型

  • 一 可调对象的初始化,即[] 方括号里定义的变量,唯一bug的是该变量不提供显式类型声明,比如[str = "heelo"][i = 7]类型是自动推导的
  • (){} 可以理解成对象的operator()(){}函数;调用该匿名对象时其实就是在执行该函数
  • lambda 的捕捉列表:
    • 对捕捉到的变量要作修改则使用mutable说明
    • [&] 对外部变量取引用
    • [=] 要用到外部变量的副本,比如调用外部函数
    • [a, &b] 显式地声明要捕获的变量及形式
    • 返回类型一般是自动推导,完全控制返回类型的写法
      auto fun{[]()->int{}}

函数指针声明式只是函数签名,并不是该指针的定义式

  • 比如下面这个是函数指针的定义
    // 即定义一个变量,它是函数指针类型的变量
    int (*p_fun)(int, int)
    
  • 这个是函数指针的声明,也即是类型的声明
    using TYPE_P_FUN = int(*)();
    typedef int(*TYPE_P_FUN1)();
    

仔细看下这个函数的返回值能被函数指针变量接收吗?

  • Q: 已知,是能将lambda对象保存进一个函数指针里,那么下面该返回的lambda对象能被函数指针接收吗?
  • A:直接下结论,不能
    template <typename C>
    static auto consumer (C &container)
        return [&] (auto value) {
        	container.push_back(value);
        };
    }
  • 正确答案(完整代码):
    #include <iostream>
    #include <deque>
    #include <list>
    #include <vector>
    #include <functional>
    
    template <typename C>
    static auto consumer (C &container)
    {
        return [&] (auto value) {
            container.push_back(value);
        };
    }
    
    template <typename C>
    static void print (const C &c)
    {
        for (auto i : c) {
            std::cout << i << ", ";
        }
        std::cout << '\n';
    }
    
    int main()
    {
        std::deque<int>  d;
        std::list<int>   l;
        std::vector<int> v;
        // 将函数指针封装成std::function类型
        const std::vector<std::function<void(int)>> consumers {consumer(d), consumer(l), consumer(v)};
    
        for (size_t i {0}; i < 10; ++i) {
            for (auto &&consume : consumers) {
                consume(i);
            }
        }
    
        print(d);
        print(l);
        print(v);
    }
    

并置函数

阅读以下代码,并思考:

  • 首先,哪些是编译时确定的?
    A: 返回匿名对象的那句, 编译时确定可调对象t ts(个人认为其实是确定了函数指针)
  • concat似乎看起来会有递归调用, 那么何时开始递归
    A: 在这句,调用operator()(2, 3)
    std::cout << combined(2, 3) << '\n';
    
  • 在确定lambda对象时,即根据函数指针类型(可调对象类型)T Ts确定了返回的可调对象
    template <typename T, typename ...Ts>
    auto concat(T t, Ts ...ts)
    {
        if constexpr (sizeof...(ts) > 0) {
            return [=](auto ...parameters) { return t(concat(ts...)(parameters...)); };
        } else  {
            return t;
        }
    }
    
  • 在执行以下时,才是在调用可调对象的可调函数,类似于operator()(), 也即这时(2, 3) parameters参数才算起作用
    std::cout << combined(2, 3) << '\n';
    
  • 对下面这句的理解应先从,生成可调对象的角度,运行时调用的角度主要是看对参数(auto ...parameters)的如何处理
    return [=](auto ...parameters) { return t(concat(ts...)(parameters...)); };
    
    这行只是在返回可调对象, 可调对象上还有附加信息, 就是,可调对象与参数是层层绑定的. 比如t这个对象的形式是
    t(concat(ts...)(parameters...));
    
    完整代码
    #include <iostream>
    #include <functional>
    
    template <typename T, typename ...Ts>
    auto concat(T t, Ts ...ts)
    {
        if constexpr (sizeof...(ts) > 0) {
            return [=](auto ...parameters) { return t(concat(ts...)(parameters...)); };
        } else  {
            return t;
        }
    }
    
    int main()
    {
        auto twice  ([] (int i) { return i * 2; });
        auto thrice ([] (int i) { return i * 3; });
    
        auto combined (concat(twice, thrice, std::plus<int>{}));
    
        std::cout << combined(2, 3) << '\n';
    }
    

通过逻辑连接创建复杂谓词

  • STL已经提供了一些非常有用的函数对象,例如std::logical_andstd::logical_or等等
  • 有时候可以复用已经存在的谓词,并将它们结合起来使用。比如,如果我们既想要检查输入字符串的开头是否是foo,又想检查结尾是否为“bar”时,就可以将之前提到的两个谓词组合起来使用。
  • 这个写法算是必知必会
    #include <iostream>
    #include <functional>
    #include <string>
    #include <iterator>
    #include <algorithm>
    
    
    static bool begins_with_a (const std::string &s)
    {
        return s.find("a") == 0;
    }
    
    static bool ends_with_b (const std::string &s)
    {
        return s.rfind("b") == s.length() - 1;
    }
    
    template <typename A, typename B, typename F>
    auto combine(F binary_func, A a, B b)
    {
        return [=](auto param) {
            return binary_func(a(param), b(param));
        };
    }
    
    int main()
    {
        auto a_xxx_b (combine(std::logical_and<>{}, begins_with_a, ends_with_b));
    
        std::copy_if(std::istream_iterator<std::string>{std::cin}, {},
                     std::ostream_iterator<std::string>{std::cout, ", "},
                     a_xxx_b);
        std::cout << '\n';
    }
    

initializer_list

  • 这个示例有很大的特点就是很好地控制了函数调用顺序
  • 将多个模板参数放到initializer_list
  • 巧妙运用initializer_list 的构造函数来执行相关代码
    标准叫法"initializer_list展开参数包",包括上文并置函数ts也是参数包
    必知必会的用法
    #include <iostream>
    
    template <typename ... Ts>
    static auto multicall (Ts ...functions)
    {
        return [=](auto x) {
            (void)std::initializer_list<int>{
                ((void)functions(x), 0)...
            };
        };
    }
    
    template <typename F, typename ... Ts>
    static auto for_each (F f, Ts ...xs) {
        (void)std::initializer_list<int>{
            ((void)f(xs), 0)...
        };
    }
    
    static auto brace_print (char a, char b) {
        return [=] (auto x) {
            std::cout << a << x << b << ", ";
        };
    }
    
    int main()
    {
        auto f  (brace_print('(', ')'));
        auto g  (brace_print('[', ']'));
        auto h  (brace_print('{', '}'));
        auto nl ([](auto) { std::cout << '\n'; });
    
        auto call_fgh (multicall(f, g, h, nl));
    
        for_each(call_fgh, 1, 2, 3, 4, 5);
    }	  
    

算法 5-0

  • 一行就实现累加
    vector<int> v {100, 400, 200 /*, ... */ };
    cout << accumulate(begin(v), end(v), 0) << '\n';
    
  • 例如,将vector中的所有元素都填充0时,就可以使用std::fill。因为vector使用的是一段连续的内存,对于这种使用连续内存存放数据的结构都可以使用std::fill进行填充,这个函数类似于C中的memset函数。当开发者将容器类型从vector改为list,STL算法就不能再使用memset了,并且需要逐个迭代list的元素,并将元素赋0。开发者不能为了使用memset将数据类型写死为vectorarray,因为实际项目中,还是有很多数据结构存储的地址并不是连续的。

5-1 容器间相互复制元素

  • std::copy算法可以很好的展示迭代器是如何将不同的数据结构进行抽象,而后将一个容器的数据拷贝到另一个容器。
  • 再次复习用ostream_iterator将输出流定向到cout
  • copy() copy_n()move()用法,move()生效,将源数据搬空,此例将v中的键值对搬空
  • 考察在不同的容器之间拷贝的用法,将vector中的前几个整型字符串对使用std::copy_n拷贝到map中。因为vectormap是两种完全不同的结构体,我们需要对vector中的数据进行变换,这里就要使用到insert_iterator适配器。std::inserter函数为我们提供了一个适配器。在算法中使用类似std::copy_n的算法时,需要与插入迭代器相结合,这是一种更加通用拷贝/插入元素的方式(从一种数据结构到另一种数据结构),但这种方式不是最快的。使用指定数据结构的成员函数插入元素无疑是更加高效的方式:
    // 经过此move原始对象已被转移,不应再访问原始对象
    auto g = move(v);
    
    完整示例
    #include <iostream>
    #include <vector>
    #include <map>
    #include <string>
    #include <tuple>
    #include <iterator>
    #include <algorithm>
    
    using namespace std;
    
    namespace std {
            ostream& operator<<(ostream &os, const pair<int, string> &p)
            {
                    return os << "(" << p.first << ", " << p.second << ")";
            }
    }
    
    int main()
    {
        vector<pair<int, string>> v{
                {1, "one"}, {2, "two"}, {3, "three"}, {4, "four"}, {5, "five"} };
    
        map<int, string> m;
    
        copy_n(begin(v), 3, inserter(m, begin(m)));
    
        auto shell_it(ostream_iterator<pair<int, string>>{cout, ", "});
        copy(begin(m), end(m), shell_it);
        cout << '\n';
    
        m.clear();
        // 经move()后不宜再访问源对象, 此例是把源的键值对中的值搬移
        move(begin(v), end(v), inserter(m, begin(m)));
    
        copy(begin(m), end(m), shell_it);
        cout << '\n';
        // 
        copy(begin(v), end(v), shell_it);
        cout << '\n';  
    }
    
    (1, one), (2, two), (3, three),
    (1, one), (2, two), (3, three), (4, four), (5, five),
    (1, ), (2, ), (3, ), (4, ), (5, ),
    

Note:

如果类型只有一个或多个(使用classstruct包装)的矢量类型或是类,那么其拷贝赋值通常是轻量的,所以可以使用memcopymemmove进行赋值操作,而不要使用自定义的赋值操作符进行操作。

Markdown Preview

5-2 容器元素排序

  • 自定义数据结构的排序解决办法,自定义排序函数
  • 如何制造随机数种子
  • 打乱函数
  • 如何对数据部分排序, 数据根据条件进行分割
    #include <iostream>
    #include <algorithm>
    #include <vector>
    #include <iterator>
    #include <random>
    
    using namespace std;
    
    static void print(const vector<int> &v)
    {
            copy(begin(v), end(v), ostream_iterator<int>{cout, ", "});
            cout << '\n';
    }
    
    int main()
    {
            vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    
            random_device rd;
            mt19937 g{ rd() };
    
            cout << is_sorted(begin(v), end(v)) << '\n';
    
            shuffle(begin(v), end(v), g);
    
            cout << is_sorted(begin(v), end(v)) << '\n';
            print(v);
    
            // 从小到大排列, 升序
            sort(begin(v), end(v), std::less<int>());
            std::cout << "been less sorted" << std::endl;
            cout << is_sorted(begin(v), end(v)) << '\n';
            print(v);
    
            shuffle(begin(v), end(v), g);
    
            // 其实该函数该叫"分割", 此例将比8小的数据扔到左边, p 指向 >= 8的第一个数
            auto p = partition(begin(v), end(v), [](int i) { return i < 8; });
            std::cout << "been partition sorted *p: " << *p << std::endl;  
    
            print(v);
    
            shuffle(begin(v), end(v), g);
    
            // middle()返回中间节点迭代器
            auto middle(next(begin(v), int(v.size()) / 2));
            partial_sort(begin(v), middle, end(v));
            std::cout << "been partial_sort sorted" << std::endl;
            print(v);
    
            struct mystruct {
                    int a;
                    int b;
            };
    
            vector<mystruct> mv{ {5, 100}, {1, 50}, {-123, 1000}, {3, 70}, {-10, 20} };
    
            // 自定义数据结构的排序  
            // 使用自定义的排序函数
            sort(begin(mv), end(mv),
                    [](const mystruct &lhs, const mystruct &rhs) {
                    return lhs.b < rhs.b;
            });
    
            for (const auto &[a, b] : mv) {
                    cout << "{" << a << ", " << b << "} ";
            }
            cout << '\n';  
            return 0;
    }
    

5-3 从容器中删除指定元素

  • 将过滤出的元素从数据结构中移除,或是简单的移除其中一个,但对于不同数据结构来说,操作上就完全不一样了。在链表中(比如std::list),只要将对应节点的指针进行变动就好。不过,对于连续存储的结构体来说(比如std::vectorstd::array,还有部分std::deque),删除相应的元素时,将会有其他元素来替代删除元素的位置。当一个元素槽空出来后,那么后面所有的元素都要进行移动,来将这个空槽填满。这个听起来都很麻烦,不过本节中我们只是想要从字符串中移除空格,这个功能没有太多的工作量。
  • 删除指定某一个元素
  • 删除满足条件的数, 比如删除奇数/偶数. 满足条件以return true 来记
  • 替换某些数
  • 拷贝某些满足条件的数
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <iterator>
    
    using namespace std;
    
    void print(const vector<int> &v)
    {
        copy(begin(v), end(v), ostream_iterator<int>{cout, ", "});
        cout << '\n';
    }
    
    int main()
    {
        vector<int> v {1, 2, 3, 4, 5, 6};
    
        print(v);
    
        {
            const auto new_end (remove(begin(v), end(v), 2));
            v.erase(new_end, end(v));
        }
        print(v);
    
        {
            auto odd_number ([](int i) { return i % 2 != 0; });
            const auto new_end (remove_if(begin(v), end(v), odd_number));
            v.erase(new_end, end(v));
        }
        print(v);
    
        replace(begin(v), end(v), 4, 123);
        print(v);
    
        v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
        vector<int> v2;
        vector<int> v3;
    
        auto odd_number  ([](int i) { return i % 2 != 0; });
        auto even_number ([](int i) { return i % 2 == 0; });
    
        remove_copy_if(begin(v), end(v), back_inserter(v2), odd_number);
        copy_if(       begin(v), end(v), back_inserter(v3), even_number);
    
        print(v2);
        print(v3);
    }
    

map中的数,把他按值大小排序

  • 思路:
    先把map中的键值对放在vector中,再对vector排序
    int main()
    {
        unordered_map<int, int> iMap;
        iMap[1] = 20;
        iMap[2] = 10;
        iMap[5] = 30;
        iMap[4] = 0;
    
        vector<pair<int, int>> vtMap;
        for (auto it = iMap.begin(); it != iMap.end(); it++)
            vtMap.push_back(make_pair(it->first, it->second));
    
        sort(vtMap.begin(), vtMap.end(), 
            [](const pair<int, int> &x, const pair<int, int> &y) -> int {
            return x.second < y.second;
        });
    
        for (auto it = vtMap.begin(); it != vtMap.end(); it++)
            cout << it->first << ':' << it->second << '\n';
        return 0;
    }
    


常用算法相关汇总

函数对象 std::accumulate()算法使用,第四参数是可选的

  • 函数对象的调用方式确实如预料,函数对象要好好用
#include <iostream>
#include <stdio.h>
#include <numeric>
#include <vector>
class Obj
{
public:
    int operator()(const int& x, const int& y) { std::cout << "operator() done" << std::endl; return x+y+(owner++); }
private:
    int owner{ 0 };
};

int main()
{
    std::vector<int> vec_int;
    for (int i = 0; i < 5; ++i)
    {
        vec_int.emplace_back(i);
    }
    Obj obj;
    int ret = std::accumulate(vec_int.begin(), vec_int.end(), 0, obj);
    printf("ret is %d \n", ret);  // ret = 20
}

vector中删除重复的元素

// 如果xiongmai的api不准, 加算法, 删除掉重复的ip      
std::sort(vec_device_info_in.begin(), vec_device_info_in.end());
vec_device_info_in.erase(std::unique(vec_device_info_in.begin(), vec_device_info_in.end()), vec_device_info_in.end());

顺序容器遍历容器并查找目标元素(偶数元素),删除找到的目标;更新迭代器

// 把vec容器中所有的偶数全部删除
auto it2 = vec.begin();
while (it2 != vec.end())
{
        if (*it2 % 2 == 0)
        {
                it2 = vec.erase(it2);
        }
        else
        {
                ++it2;
        }
}  

关联容器删除指定元素,并更新迭代器

set1.erase(20); // 按key值删除元素

for (it1 = set1.begin(); it1 != set1.end(); )
{
        if (*it1 == 30)
        {
                it1 = set1.erase(it1); // 调用erase,it1迭代器就失效了
        }
        else
        {
                ++it1;
        }
}

it1 = set1.find(20);
if (it1 != set1.end())
{
        set1.erase(it1);
}

海量数据查重

int array[10000];   
std::unordered_map<int, int> un_map;
for (const auto& k: array)
{
        un_map[k]++;
}