前言
这个应该是看封面有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::map
中insert
和emplace
方法完全相同。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
; 键不可排序,比如盛放自定义的类型(struct
或class
) --> 选用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;
这里第一个和第二个模板类型,我们填写的是coord
和int
。另外的三个模板类型是选填的,其会使用已有的标准模板类。对于这个例子,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
时,我们使用it
和end
作为输入迭代器。第三个参数必须是一个输出迭代器。因此,不能使用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
中自定义排序 -
自定义降序排序
-
注意,容器的拷贝,向
vector
的back_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_and
,std::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
将数据类型写死为vector
或array
,因为实际项目中,还是有很多数据结构存储的地址并不是连续的。
5-1 容器间相互复制元素
std::copy
算法可以很好的展示迭代器是如何将不同的数据结构进行抽象,而后将一个容器的数据拷贝到另一个容器。- 再次复习用
ostream_iterator
将输出流定向到cout
copy()
copy_n()
及move()
用法,move()
生效,将源数据搬空,此例将v
中的键值对
的值
搬空- 考察在不同的容器之间拷贝的用法,将
vector
中的前几个整型字符串对使用std::copy_n
拷贝到map
中。因为vector
和map
是两种完全不同的结构体,我们需要对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:
如果类型只有一个或多个(使用
class
或struct
包装)的矢量类型或是类,那么其拷贝赋值通常是轻量的,所以可以使用memcopy
或memmove
进行赋值操作,而不要使用自定义的赋值操作符进行操作。
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::vector
,std::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]++;
}