第9章 序列与关联容器
1.概述
(1)容器:一种特殊的类型,其对象可以放置其它类型的对象(元素)
需要支持的操作:对象的添加、删除、索引、遍历
有多种算法可以实现容器,每种方法各有利弊
(2)容器分类
1序列容器:其中的对象有序排列,使用整数值进行索引
2关联容器:其中的对象顺序并不重要,使用键进行索引
3适配器:调整原有容器的行为,使得其对外展现出新的类型、接口或返回新的元素
4生成器:构造元素序列
(3)迭代器:用于指定容器中的一段区间,以执行遍历、删除等操作
获取迭代器: ©begin/©end ; ©rbegin/©rend
迭代器分类:分成 5 类( category ),不同的类别支持的操作集合不同
#include <iostream>
#include <vector>
int main()
{
std:::vector<int> x{1, 2, 3};
auto b = x.begin();
auto e = x.end(); //[b, e)
for (auto ptr = b; ptr < e; ptr++)
{
std::cout << *ptr << ' ';
}
}
2.序列容器
(1)C++ 标准库中提供了多种序列容器模板
1 array :元素个数固定的序列容器
2 vector :元素连续存储的序列容器
3 forward_list / list :基于链表 / 双向链表的容器
4 deque : vector 与 list 的折中
5 basic_string :提供了对字符串专门的支持
(2)注意点
1需要使用元素类型来实例化容器模板,从而构造可以保存具体类型的容器。
2不同的容器虽然所提供的接口大致相同,但根据容器性质的差异,其内部实现与复杂度不同。
3对于复杂度过高的操作,提供相对较难使用的接口或者不提供相应的接口
(3)array 容器模板
1 具有固定长度的容器,其内部维护了一个内建数组,与内建数组相比提供了复制操作。
#include <iostream>
#include <array>
int main()
{
std::array<int, 3> a;
std::array<int, 3> b = a;
}
2 提供的接口
1)构造(与数组类似)
2)成员类型: value_type 等
#include <iostream>
#include <array>
#include <type_traits>
int main()
{
std::array<int, 3> a;
std::cout << std::is_same_v<std::array<int, 3>::value_type, int> ;
}
3)元素访问: [] , at , front , back , data
#include <iostream>
#include <array>
int main()
{
std::array<int, 3> a = {1, 2, 3};
std::cout << a[0] << std::endl;
std::cout << a.at(0) << std::endl; //大于有效长度程序会崩溃
std::cout << a.front() << std::endl;
}
4)容量相关(平凡实现): empty , size , max_size
#include <iostream>
#include <array>
int main()
{
std::array<int, 3> a = {1, 2, 3};
std::cout << a.size() << std::endl;
std::cout << a.empty() << std::endl;
}
5)填充与交换: fill , swap
#include <iostream>
#include <array>
int main()
{
std::array<int, 3> a;
a.fill(100);
std::cout << a[0] << ' '
<< a[1] << ' '
<< a[2] << ' ' << endl;
}
6)比较操作: <=>
7)迭代器
(4)vector 容器模板:元素可变
1提供的接口
1)与 array 很类似,但有其特殊性
2)容量相关接口: capacity / reserve / shrink_to_fit
#include <iostream>
#include <vector>
int main()
{
std:::vector<int> a;
a.reserve(1024);
for (int i = 0; i < 1024; i++)
{
a,push_back(i);
}
}
3)附加元素接口: push_back / emplace_back
#include <iostream>
#include <vector>
#include <string>
int main()
{
std:::vector<std::string> a;
a.push_back("hello");
a.emplace_back("hello"); //直接在内存中构造string
}
4)元素插入接口: insert / emplace
5)元素删除接口: pop_back / erase / clear
2注意点
vector 不提供 push_front / pop_front ,可以使用 insert / erase 模拟,但效率不高
swap 效率较高
写操作可能会导致迭代器失效
(5)list 容器模板:双向链表
1与 vector 相比, list
1)插入、删除成本较低,但随机访问成本较高
2)提供了 pop_front / splice 等接口
3)写操作通常不会改变迭代器的有效性
(6)forward_list 容器模板:单向链表
1目标:一个成本较低的线性表实现
2其迭代器只支持递增操作,因此无 rbegin/rend
3不支持 size
4不支持 pop_back / push_back
5XXX_after 操作
(7)deque容器模板: vector 与 list 的折中
push_back / push_front 速度较快
在序列中间插入、删除速度较慢
(8)basic_string 容器模板:实现了字符串相关的接口
使用 char 实例化出 std::string
提供了如 find , substr 等字符串特有的接口
提供了数值与字符串转换的接口
针对短字符串的优化
3.关联容器
(1)使用键进行索引
1 set / map / multiset / multimap
2unordered_set / unordered_map / unordered_multiset / unordered_multimap
set / map / multiset / multimap 底层使用红黑树实现
unordered_xxx 底层使用 hash 表实现
#include <iostream>
#include <map>
int main()
{
std::map<char, int> m{{'a', 3}, {'b', 4}};
std::cout << m['a'] << std::endl;
}
(2)set
1通常来说,元素需要支持使用 < 比较大小,或者采用自定义的比较函数来引入大小关系
#include <iostream>
#include <set>
int main()
{
std::set<int> s{3, 100, 45, 8};
//std::set<int, std::greater<int>> s{3, 100, 45, 8};
for (auto ptr = s.begin();ptr != s.end(); ptr++)
{
std::cout << *ptr << std::endl;
}
}
#include <iostream>
#include <set>
struct Str
{
int x;
};
bool Mycomp(const Str& val1, const Str& val2)
{
return val1.x < val2.x;
}
int main()
{
std::set<Str, decltype(&Mycomp)> s({Str{3}, Str{5}}, Mycomp);
}
2插入元素: insert / emplace / emplace_hint
#include <iostream>
#include <set>
struct Str
{
int x;
};
bool Mycomp(const Str& val1, const Str& val2)
{
return val1.x < val2.x;
}
int main()
{
std::set<Str, decltype(&Mycomp)> s({Str{3}, Str{5}}, Mycomp);
s.insert(Str{100});
s.enplace(100);
}
3删除元素: erase
4访问元素: find / contains
#include <iostream>
#include <set>
int main()
{
std::set<int> s{3, 100, 45, 8};
auto ptr = s.find(3);
if (ptr != s.end())
std::cout << *ptr << std::endl;
std::cout << s.contains(100) << std::endl;
}
5修改元素: extract
注意: set 迭代器所指向的对象是 const 的,不能通过其修改元素
(3)map
1树中的每个结点是一个 std::pair
#include <iostream>
#include <map>
int main()
{
std::map<char, int> m{{'a', 3}, {'b', 4}};
for (auto ptr = m.begin();ptr != m.end(); ptr++)
{
auto p = *ptr; //std::pair<const char, int>
std::cout << p.first << ' ' << p.second << std::endl;
}
//基于range-based-for
for (auto p : m)
{
std::cout << p.first << ' ' << p.second << std::endl;
}
//或者
for (auto [k, v] : m)
{
std::cout << k << ' ' << v << std::endl;
}
}
2键 (pair.first) 需要支持使用 < 比较大小,或者采用自定义的比较函数来引入大小关系
3访问元素: find / contains / [] / at
#include <iostream>
#include <map>
int main()
{
std::map<int, bool> m;
m.insert(std::pair<const int, bool>(3, true));
auto ptr = m.find(3);
std::cout << ptr->first << '' << ptr->second;
std::cout << m[3] << std::endl;
}
4注意
map 迭代器所指向的对象是 std::pair ,其键是 const 类型
[] 操作不能用于常量对象
#include <iostream>
#include <map>
void fun(const std::map<int, int>& m)
{
auto ptr = m.find(3);
if (ptr != m.end())
{
std::cout << ptr->second;
}
}
int main()
{
std::map<int, bool> m;
m.insert(std::pair<const int, bool>(3, true));
fun(m);
}
(4)multiset / multimap
1与set / map类似,但允许重复键
#include <iostream>
#include <set>
int main()
{
//std::set<int> s{1, 2, 4, 1};
std::multiset<int> s{1, 2, 4, 1};
for (auto i : s)
{
std::cout << i << std::endl;
}
}
2元素访问
1)find 返回首个查找到的元素
#include <iostream>
#include <set>
int main()
{
std::multiset<int> s{1, 2, 4, 1};
auto ptr = s.find(1);
std::cout << *ptr << std::endl;
std::cout << *(++ptr) << std::endl;
}
2)count返回元素个数
#include <iostream>
#include <set>
int main()
{
std::multiset<int> s{1, 2, 4, 1};
std::cout << s.count(1) << std::endl;
}
3)lower_bound / upper_bound / equal_range 返回查找到的区间
#include <iostream>
#include <set>
int main()
{
std::multiset<int> s{1, 2, 4, 1};
auto b = s.lower_bound(1);
auto e = s.upper_bound(1);
for (auto ptr = b; ptr != e;++ptr)
{
std::cout << *ptr << std::endl;
}
//或者
auto p = s.equal_range(1);
for (auto ptr = p.first; ptr != p.second; ++ptr)
{
std::cout << *ptr << std::endl;
}
}
(5)unordered_set / unordered_map / unordered_multiset / unordered_multimap
1与 set / map 相比查找性能更好,但插入操作一些情况下会慢
2键需要支持两个操作
1)转换为 hash 值
2)判等
#include <iostream>
#include <unordered_set>
int main()
{
std::unordered_set<int> s{3, 1, 5, 4, 1};
for (auto p : s)
{
std::cout << p << std::endl;
}
}
4.适配器与生成器
(1)类型适配器
1basic_string_view ( C++17 )
可以基于 std::string , C 字符串,迭代器构造
提供成本较低的操作接口
不可进行写操作
#include <iostream>
#include <string_view>
void fun(std::string_view str)
{
if (!str.empty())
std::cout << str[0] << std::endl;
}
int main()
{
fun("12345"); //c
fun(std::string("12345")); //c++
}
#include <iostream>
#include <string>
#include <string_view>
void fun(std::string_view str)
{
std::cout << str << std::endl;
}
int main()
{
fun("12345"); //c
std::string s("12345");
fun(std::string_view(s.begin(), s.begin() + 3)); //c++
}
#include <iostream>
#include <string_view>
std::string_view fun(std::string_view input)
{
return input.substr(0, 3);
}
int main()
{
std::string s("12345");
auto res = fun(s);
std::cout << res << std::endl;
}
2 span ( C++20 )
可基于 C 数组、 array 等构造
可读写
#include <iostream>
#include <vector>
#include <span>
void fun(std::span<int> input)
{
for (auto p : input)
{
std::cout << p << ' ';
}
std::cout << std::endl;
}
int main()
{
std::vector<int> s{1, 2, 3};
fun(s);
}
(2)接口适配器
1 stack / queue / priority_queue
对底层序列容器进行封装,对外展现栈、队列与优先级队列的接口
priority_queue 在使用时其内部包含的元素需要支持比较操作
(3)数值适配器 (c++20)
std::ranges::XXX_view, std::ranges::views::XXX, std::views::XXX
1可以将一个输入区间中的值变换后输出
2数值适配器可以组合,引入复杂的数值适配逻辑
#include <iostream>
#include <vector>
#include <ranges>
bool isEven(int i)
{
return i % 2 == 0;
}
int Square(int i)
{
return i * i;
}
int main()
{
std::vector<int> s{1, 2, 3, 4, 5};
//1
for (auto p : std::ranges::filter_view(v, isEven))
{
std::cout << p << ' ';
}
std::cout << std::endl;
//2
for (auto p : std::ranges::transform_view(v, Square))
{
std::cout << p << ' ';
}
std::cout << std::endl;
//3
auto x0 = std::views::filter(isEven);
for (auto p : x0(v)) //或 v | x0
{
std::cout << p << ' ';
}
std::cout << std::endl;
//4
auto x = std::views::filter(isEven);
auto y = std::views::transform(Square);
for (auto p : v | x | y)
{
std::cout << p << ' ';
}
std::cout << std::endl;
}
(4)生成器 (c++20)
std::ranges::itoa_view, std::ranges::views::itoa, std::views::itoa
可以在运行期生成无限长或有限长的数值序列
#include <ranges>
#include <iostream>
int main()
{
for (int i : std::ranges::iota_view{1, 10})
std::cout << i << ' ';
std::cout << '\n';
for (int i : std::views::iota(1, 10))
std::cout << i << ' ';
std::cout << '\n';
for (int i : std::views::iota(1) | std::views::take(9))
std::cout << i << ' ';
std::cout << '\n';
}