C++ Primer Plus(速记版)-容器和算法
第九章 顺序容器
容器是存储特定类型对象的集合,标准库提供了多种容器类型以支持不同的使用场景。其中,顺序容器(如vector、list、deque)根据元素添加到容器中的顺序来存储和访问元素,与元素值无关。
这些顺序容器各有特点:
vector:数组实现,可能需要移动大量元素,因此插入和删除操作可能较慢,
list:基于双向链表实现,支持在任意位置快速插入和删除元素,但不支持随机访问,只能通过迭代器遍历。
deque:双端队列,支持在容器的前端和后端快速插入和删除元素,也支持随机访问,但性能可能略逊于vector。
标准库还提供了三种容器适配器:
这些适配器是基于其他容器提供的操作,通过定义新的接口来适应特定的使用场景:
stack:后进先出,支持push(压栈)、pop(弹栈)、top(访问栈顶元素)等操作。
queue:先进先出,支持push(入队)、pop(出队)、front(访问队首元素)和back(访问队尾元素)等操作。
priority_queue:优先级队列,支持push(入队)、pop(弹出优先级最高的元素)、top(访问优先级最高的元素)等操作。元素按优先级顺序出队。
容器和算法库共同提供了丰富的操作来处理和转换容器中的元素。容器定义了基本的操作接口,而算法库则提供了更多高级操作,如排序、查找、复制等。这些操作的设计遵循了统一的接口规范,使得不同容器类型之间可以无缝地使用相同的算法。
9.1. 顺序容器的定义
顺序容器头文件
#include <vector>
#include <list>
#include <deque>
所有的容器都是类模板。
容器类型的定义方式为:容器名<存储类型>
vector<string> svec; //放string类型的空vector容器
list<int> ilist; // 放int类型的空list容器
deque<Sales_item> items; //放自定义类的空deque容器
所有容器类型都定义了默认构造函数,用于创建指定类型的空容器对象。
9.1.1 容器元素的初始化
除了默认构造函数,容器类型还提供其他的构造函数,使程序员可以指定元素初值。
/*默认构造函数*/
C<T> c; // 创建一个空的容器c,C是容器类型,T是元素类型。
/*拷贝构造函数*/
C<T> c(c2); // 创建一个容器c,是c2的副本。
/*范围构造函数*/
C<T> c(b, e); // 创建一个容器c,包含从迭代器b到e(不包括e)所指向的元素的副本。
/*填充构造函数*/
C<T> c(n, t); // 创建一个容器c,包含n个值为t的元素。
/*值初始化构造函数*/
C<T> c(n); // 创建一个包含n个值初始化元素的容器c。
vector<int> ivec;
vector<int> ivec2(ivec); // 拷贝构造函数
list<int> ilist(ivec);
list<string> slist(svec.begin(), svec.end());//范围构造函数。迭代器指定范围
const list<int>::size_type list_size = 64; //创建一个list_size类型
list<string> slist(list_size, "eh?"); //填充构造函数,list_size个"eh?"
list<string> slist(list_size); //值初始化构造函数,list_size个初始值
9.1.2 容器内元素的类型约束
容器元素类型的基本要求:
支持赋值操作。
可复制。
引用类型不能作为容器元素类型,引用类型不支持赋值和复制。
输入输出(IO)标准库类型,如流文件对象,以及特定的智能指针类型(如std::auto_ptr,但注意std::auto_ptr已在C++11中被弃用)也不支持作为容器元素类型,它们不支持复制或赋值。
高级用法:
容器本身可以作为其他容器的元素类型,即可以创建容器的容器。
std::vector<std::vector<int>> vec;
自定义类型(如Sales_item)如果满足上述基本要求,也可以作为容器元素类型。
9.1.3 容器操作的特殊要求
容器元素需支持复制和赋值。
特定操作,指定大小和初始化值创建容器,要求元素类型有默认构造函数。若元素类无默认构造,则这些操作不可用。
如果容器元素没有默认构造函数,代表没有固定的初始化值,也就不能调用值初始化构造函数,但可以用填充构造函数,指定个数和要初始化的值。
9.1.4 容器的容器
定义容器的容器时,需确保两个 > 符号间有空格,以避免编译器误读为右移操作符。
vector<vector<string> > //正确定义了一个字符串容器的容器,缺少空格会导致编译错误。
9.2. 迭代器和迭代器范围
每种容器都提供若干种迭代器,所有容器类型提供的迭代器都有共同接口,支持相同操作如解引用、自增和自减,以统一方式访问容器元素。
*iter //返回迭代器 iter 所指向的元素的引用
iter->mem //对 iter 进行解引用,获取指定元素中名为 mem 的成员。等效于(*iter).mem
++iter //自增,指向下一个元素
iter++ //自增,指向下一个元素
--iter //自减,指向前一个元素
iter-- //自减,指向前一个元素
iter1 == iter2 //比较两个迭代器是否指向同一个元素,或者末端元素的下一个位置
iter1 != iter2 //比较两个迭代器是否指向同一个元素,或者末端元素的下一个位置
9.2.1 vector 和 deque 容器的迭代器提供额外的运算
C++中,仅 vector 和 deque 容器支持迭代器算术运算及除 == 和 != 外的关系比较,因为它们一个底层是数组,一个底层是双端队列,内存布局允许高效的随机访问。
而其他容器迭代器通常仅支持==和!=。
/*迭代器上加(减)整数值 n . 范围限于容器内、末端元素的下一个元素*/
iter + n
iter - n
/*两个迭代器加、减,运算结果给左值*/
iter1 += iter2
iter1 -= iter2
iter1 - iter2
/*关系操作符,大于、小于、大于等于、小于等于*/
iter1 > iter2
iter1 >= iter2
iter1 < iter2
iter1 <= iter2
9.2.2. 迭代器范围
C++ 语言使用一对迭代器标记迭代器范围,这两个迭代器分别指向同一个容器中的两个元素或超出末端元素的下一位置,通常将它们命名为 first 和 last,或 begin 和 end,用于标记容器中的一段元素范围。范围是左闭右开,也就是包括 begin的元素,不包括 end处。
使用左闭右开区间的编程意义在于,begin和 end相等时,代表一个元素也没有,不相等时则至少有一个元素。可以安全地使用 while(begin!=end) 循环。
9.3. 顺序容器的操作
每种顺序容器都提供了一组有用的类型定义以及以下操作:
• 在容器中添加、删除元素。
• 设置容器大小。
• 获取容器内的第一个和最后一个元素。
9.3.1. 容器定义的类型和别名
所有容器都提供这几种类型。
size_type //无符号整型,用来替代字面值
iterator //迭代器
const_iterator //只读迭代器
reverse_iterator //按逆序寻址元素的迭代器
const_reverse_iterator //只读逆序迭代器
difference_type //存储两个迭代器差值的有符号整型,可为负数
value_type //元素类型
reference //元素的左值类型,是 value_type& 的同义词
const_reference //元素的常量左值类型,等效于 const value_type&
逆序迭代器从后向前遍历容器,并反转了某些相关的迭代器操作:例如,在逆序迭代器上做 ++ 运算将指向容器中的前一个元素。
value_type、reference、const_reference三种类型使程序员无须直接知道容器元素的真正类型,就能使用它。需要使用元素类型时,只要用 value_type 即可。如果要引用该类型,则通过 reference 和 const_reference 类型实现。
9.3.2. begin 和 end 成员
c.begin() //返回迭代器,指向容器 c 的第一个元素
c.end() //返回迭代器,它指向容器 c 的末端元素的下一位置
c.rbegin() //返回逆序迭代器,它指向容器 c 的末端元素
c.rend() //返回逆序迭代器,它指向容器 c 的首元素的前一个位置
//注意迭代器的末端元素的下一个位置,是逆序迭代器的首端
上述每个操作都有两个不同版本:一个是 const 成员,另一个是非 const 成员。
如果容器不是 const,则这些操作返回 iterator 或 reverse_iterator 类型。
如果容器是 const,则其返回类型要加上 const_ 前缀,也就是 const_iterator 和const_reverse_iterator 类型。
9.3.3. 在顺序容器中添加元素
所有顺序容器都支持 push_back 操作,在容器尾部插入一个元素。
list 和 deque 容器类型还提供了 push_front,在容器首部插入新元素。
使用 push_back 和 push_front 操作可以非常方便地在顺序容器的尾部或首部添加单个元素。而 insert 操作则提供了一组更通用的插入方法,实现在容器的任意指定位置插入新元素。
c.push_back(t) //从后往前添加元素。返回void
//在迭代器 p 所指向的元素前面插入值为 t 的新元素。返回新迭代器
c.insert(p,t)
//在迭代器 p 所指向的元素前面插入 n 个值为 t 的新元素。返回 void 类型
c.insert(p,n,t)
//在迭代器 p 所指向的元素前面插入由迭代器 b 和 e 标记的范围内的元素。返回 void 类型
c.insert(p,b,e)
//从前往后添加元素。只适用于 list 和 deque 容器类型. 返回void
c.push_front(t)
添加元素可能会使迭代器失效
任何 insert 或 push 操作都可能导致迭代器失效。
当编写循环插入元素到 vector 或 deque 容器中时,应确保迭代器在每次循环后都得到更新。因为 vector和 deque都用到了数组实现,扩容可能导致原迭代器指向的地址失效。
不要存储 end 操作返回的迭代器。添加或删除 deque 或vector 容器内的元素必定导致 end操作返回的迭代器失效。
9.3.4. 关系操作符
所有的容器类型都支持用关系操作符来实现两个容器的比较。
容器类型的关系比较类似于字符串的比较。
两个容器长度相同且元素一一对应相等,则它们相等。
长度不同,较短的容器所有元素与较长容器对应位置元素相等,则较短容器小于较长容器。
长度不同、元素不同,则比较第一个不相等的元素。
所有容器都通过比较元素来实现关系运算。
假设 ivec1 和 ivec2 都是 vector<int> 类型的容器,则 ivec1 < ivec2 使用了内置 int 型定义的小于操作符。如果这两个 vector 容器存储的是 strings 对象,则使用 string 类型的小于操作符。
9.3.5. 容器大小的操作
所有容器类型都提供四种与容器大小相关的操作:
size 操作返回容器内元素的个数。
maxsize 操作返回容器能装下的最大元素个数。
empty 操作则返回一个容器元素个数是否为0的布尔值
resize 操作调整容器的长度大小。可选填充。
//返回容器中的元素个数。返回类型为 c::size_type
c.size()
//返回容器 c 可容纳的最多元素个数,返回类型为 c::size_type
c.max_size()
//返回标记容器大小是否为 0 的布尔值
c.empty()
//调整容器大小。缩减则删,扩容则用类型初值填充
c.resize(n)
//调整容器大小,指定容量和填充值
c.resize(n,t)
resize 操作可能会使迭代器失效。
9.3.6. 访问元素
//返回容器的最后一个元素的引用
c.back()
//返回容器的第一个元素的引用
c.front()
//返回下标指定元素的引用。下标不准越界。只适用于 vector 和 deque 容器。
c[n]
//返回下标指定元素的引用。下标不准越界。只适用于 vector 和 deque 容器
c.at(n)
所有容器都可以用 back、front操作访问元素。
vector和deque 可以使用 at(n)、c[n]访问元素,注意访问元素返回的是引用。
使用越界的下标,或调用空容器的 front 或 back 函数,都会导致程序出现严重的错误。
9.3.7. 删除元素
容器类型提供了通用的 insert 操作在容器的任何位置插入元素。
vector和deque 额外支持使用 push_front 和 push_back 操作在容器首部或尾部插入新元素。
类似地,容器类型提供了通用的 erase 操作和特定的 pop_front 和 pop_back 操作来删除容器内的元素。
//删除迭代器 p 指向的元素,返回一个迭代器指向被删除元素后面的元素
c.erase(p)
//删除两个迭代器所标记的范围内所有的元素,左闭右开,返回一个迭代器
c.erase(b,e)
//删除容器内的所有元素。返回 void
c.clear()
//删除容器 c 的最后一个元素。如果 c 为空容器,则该函数未定义
c.pop_back()
//删除容器第一个元素。返回 void。只适用于 list 或 deque 容器
c.pop_front()
erase 函数的迭代器对版本提供了删除一部分元素的功能,原理如下
// 定义两个迭代器
list<string>::iterator elem1, elem2;
//利用find函数去找元素范围内指向特定元素的迭代器,不存在则指向超出元素末端的下一位置
elem1 = find(slist.begin(), slist.end(), val1);
// 利用find函数去找元素范围内指向特定元素的迭代器,不存在则指向超出元素末端的下一位置
elem2 = find(elem1, slist.end(), val2);
//擦除这两个迭代器的范围,左闭右开
slist.erase(elem1, elem2);
9.3.8. 赋值与 swap
与赋值相关的操作符作用于整个容器。除 swap 操作外,assign() 和赋值操作符“=” 都可以用 erase 和 insert 操作实现。两侧容器类型、元素类型必须相同。
//赋值操作符。擦除左容器所有元素,然后右边复制过去。
c1 = c2
//交换内容
c1.swap(c2)
//分配内容.擦除原容器所有内容。将另一个容器的迭代器左闭右开的内容复制到容器中
c.assign(b,e)
//分配内容.擦除原容器所有内容。将容器重设为填充n个值为t的元素
c.assign(n,t)
赋值操作符(=)用于将一个容器的内容替换为另一个相同容器类型且相同元素类型的内容。
如果容器类型相同但元素类型不兼容,则需使用assign函数来手动指定如何赋值,例如将vector中的char*元素赋给string类型的list容器。
赋值操作符首先 erases 其左操作数容器中的所有元素,然后将右操作数容器的所有元素 inserts 到左边容器中。赋值后,左右两边的容器长度和内容都一样。
assign操作也会删除原容器所有内容,导致迭代器失效。
赋值操作符和 assign 操作使左操作数容器的所有迭代器失效。
swap操作则不会使迭代器失效。但迭代器指向处的元素会改变。
完成 swap操作后,尽管被交换的元素已存放在另一容器中,但迭代器仍然指向相同的位置。
#include <iostream>
#include <vector>
#include <algorithm> // 对于std::swap
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it1 = vec.begin() + 1; // 指向2
auto it2 = vec.begin() + 3; // 指向4
std::swap(*it1, *it2); // 交换2和4
// 现在it1仍然指向原来的位置,但那个位置上的值是4
// 同样,it2仍然指向原来的位置,但那个位置上的值是2
std::cout << "vec[1] = " << *it1 << std::endl; // 输出 4
std::cout << "vec[3] = " << *it2 << std::endl; // 输出 2
return 0;
}
使用 swap 操作以节省删除元素的成本
swap 操作允许交换两个相同容器类型和元素类型的容器的所有元素。执行后,左操作数容器将包含原本右操作数容器的元素,右操作数容器则包含左操作数容器原来的元素。
swap操作不会删除或插入任何元素,而且保证在常量时间内实现交换。由于容器内没有移动任何元素,因此迭代器不会失效。
指针交换:std::vector 和 std::string 等动态数组型容器的 swap 操作通过交换内部数据指针,无需移动或复制元素。
节点链接:链表(如 std::list)的 swap 操作通过交换头尾节点链接快速交换整个链表。
其他数据结构:对于像 std::deque 这样的复杂容器,swap 操作通过交换内部数据结构来实现快速内容交换,避免元素移动或复制。
没有移动元素这个事实意味着迭代器不会失效。它们指向同一位置。
9.4. vector 容器的自增长
当在vector中添加元素,若空间不足,会重新分配连续内存以存放新旧元素,这虽成本高但访问快。list通过链接新元素避免重分配,但访问不如vector快。vector通过预留额外空间来减少重新分配次数,提高增长效率,尽管实际实现中预留量因库而异,但通常使其性能优于list和deque在增长操作上。
vector 的 capacity()(容量) 和 size()(长度) 是不同概念:
size() 是当前元素数量,
capacity() 是当前最大容量,
reserve() 是设置容器的容量。
vector<int> ivec;
ivec.size(); //长度
ivec.capacity();//容量
ivec.reserve(); //设置容量
只要有剩余的容量,vector 就不必为其元素重新分配存储空间。再添加元素导致超出容量时才开始分配新的空间。
vector 每次扩容为原来的两倍。
9.5. 容器的选用
容器类型(如vector、deque、list)影响其内存分配和元素操作的性能。
vector和deque提供快速随机访问,但中间插入/删除代价高;
list在任何位置插入/删除快,但随机访问慢。选择哪种容器取决于程序的具体操作需求。
插入操作如何影响容器的选择
list 容器存储不连续,支持快速在任意位置插入或删除元素,但不支持随机访问,需遍历。
vector 在除尾部外的位置插入或删除元素需移动后续所有元素,影响效率。
deque 结构复杂,两端操作高效,中间操作较慢,支持随机访问,但中部插入/删除会使所有迭代器失效。
deque(双端队列)的内部结构由多个连续内存块组成,每个块存储多个元素,块之间通过指针连接,形成逻辑上的连续序列。这种设计允许在容器两端高效地进行内存块的添加或拆分,从而实现快速的插入和删除操作。
迭代器失效的原因简述如下:
中间部位插入:在中间插入元素可能涉及内存块的拆分或新内存块的添加,这会改变后续元素的存储位置,导致指向这些元素的迭代器失效。
前端和尾端插入:在两端插入元素通常只需要在相应的端点添加或拆分内存块,不改变中间或另一端元素的存储位置,因此指向未受影响元素的迭代器保持有效。
元素的访问如何影响容器的选择
vector 和 deque都支持高效随机访问,允许直接访问任意位置的元素。
vector 通过固定偏移访问元素特别高效。list不支持这种快速随机访问,只能通过顺序遍历元素来访问。通常,除非有特定理由选择其他容器,vector是最佳选择。
9.6. 再谈 string 类型
9.6.1. string容器操作
string 对象的常用操作:
//创建一个名为s的新空字符串对象
string s;
//使用C风格字符串(以null结尾)的字符串始化新字符串对象s
string s(cp);
//创建一个新字符串对象s,作为s2的副本
string s(s2);
//从输入流中读取一个字符串(以空白字符分隔)并存储到s中. 空格、制表、换行、回车、null都是空白字符
is >> s;
//将字符串s写入输出流os中.
os << s;
//从输入流中读取一行(直到换行符)并存储到s中,换行符不包含在内。
getline(is, s);
//将字符串s1和s2连接成一个新字符串对象
s1 + s2;
//将字符串s2追加到s1的末尾
s1 += s2;
字符串支持关系操作符(==, !=, <, <=, >, >=),用于比较两个字符串的字典顺序(区分大小写)
string 类型支持大多数顺序容器(如vector)的操作,但有一些关键区别。
string 类型被视为字符的容器,提供与vector相似的操作,但不支持栈操作(如front、back、pop_back)。
string 支持迭代器、构造函数、添加元素操作、长度操作、下标访问、at访问、begin和end迭代器、erase和clear操作、赋值操作,以及容量和预留空间操作(capacity和reserve),因为string的字符也是连续存储的。
(无back、front)(无push_front)(无pop_back、pop_front)
string 类型因其支持类似顺序容器的操作,使得原本用于 vector 等容器的代码可以轻松改写为处理 string 对象。示例中展示了使用迭代器遍历并输出 string 对象中的每个字符,这与处理 vector 元素的代码非常相似。
string s("Hiya!");
string::iterator iter = s.begin();//字符串迭代器
while (iter != s.end()) //迭代器遍历
cout << *iter++ << endl; //迭代打印旧值
简而言之,string类型在继承通用容器操作的基础上,增加了针对字符串处理的特有功能。
9.6.3. 只适用于 string 类型的操作
string类型提供了几个其他容器类型所不支持的特殊操作,包括:
substr函数,用于从当前string对象中提取并返回一个子串。
append向string对象末尾添加内容。
replace函数,替换string对象中的部分内容。
一系列find函数,用于在string对象中查找特定内容,并返回找到的位置索引。
//返回一个 string 类型的字符串,包含从下标 pos 开始的 n 个字符
s.substr(pos, n)
//返回一个 string 类型的字符串,它包含从下标 pos 开始到末尾的所有字符
s.substr(pos)
//返回字符串的副本
s.substr()
//追加.返回引用
s.append( args)
//替换pos开始len个长度,用指定字符.返回引用
s.replace(pos, len, args)
//删除迭代器b、e之间标记,左闭右开,的所有字符,用args替换
s.replace(b, e, args)
//查找第一次出现的位置
s.find( args)
//查找最后一次出现的位置
s.rfind( args) 在 s 中查找 args 的最后一次出现
//查找字符串中任意字符的第一次出现
s.find_first_of( args)
//查找字符串中任意字符的最后一次出现
s.find_last_of( args)
//查找第一个不属于字符串的字符
s.find_first_not_of( args)
//查找最后一个不属于字符串的字符
s.find_last_not_of( args)
9.7. 容器适配器
标准库中的顺序容器适配器包括
queue、 //队列
priority_queue、 //优先级队列
stack, //栈
适配器是一种机制,用于修改一个对象的行为,使其表现得像另一种类型的对象。
容器适配器利用现有的顺序容器(如vector、deque等),通过不同的抽象层来工作,如 stack 让任何顺序容器都遵循后进先出(LIFO)的栈原则。这样,即使底层使用的是不同类型的容器,上层操作(如入栈、出栈)的接口和行为都是一致的。
适配器通用的操作和类型:
size_type //一种类型,足以存储此适配器类型最大对象的长度
value_type //元素类型
container_type //基础容器的类型,适配器在此容器类型上实现
A a; //创建一个新空适配器,命名为 a
A a(c); //创建一个名为 a 的新适配器,初始化为容器 c 的副本
关系操作符 所有适配器都支持全部关系操作符:==、 !=、 <、 <=、 >、>=
#include <stack> // 栈适配器
#include <queue> // 队列适配器
9.7.1. 适配器的初始化
所有适配器都定义了两个构造函数:
默认构造函数用于创建空对象,而带一个容器参数的构造函数将参数容器的副本作为其基础值。
例如,假设 deq 是 deque<int> 类型的容器,则可用 deq 初始化一个新的栈,如下所示:
stack<int> stk(deq); // 从顺序容器复制元素到栈适配器
9.7.2. 覆盖基础容器类型
默认的 stack 和 queue 都基于 deque 容器实现,
而 priority_queue 则在 vector 容器上实现。在创建适配器时,通过将一个顺序容器指定为适配器的第二个类型实参,可覆盖其关联的基础容器类型:
template<
class T, // 栈中元素的类型
class Container = std::deque<T> // 默认的底层容器类型,这里使用 deque 但用户不能直接指定
> class stack;
template<
class T, // 队列中元素的类型
class Container = std::deque<T> // 默认的底层容器类型,这里使用 vector 但用户不能直接指定
> class queue;
template<
class T, // 优先级队列中元素的类型
class Container = std::vector<T> // 默认的底层容器类型,这里使用 deque 但用户不能直接指定
> class priority;
9.7.3. 适配器的关系运算
只要适配器的基础元素类型支持各种关系运算符,那么适配器就支持。要求类型相同。
9.7.4. 栈适配器
栈适配器支持的操作:
//如果栈为空,则返回 true,否则返回 stack
s.empty()
//返回栈中元素的个数
s.size()
//删除栈顶元素的值,但不返回其值
s.pop()
//返回栈顶元素的值,但不删除该元素
s.top()
//在栈顶压入新元素
s.push(item)
所有容器适配器都根据其基础容器类型所支持的操作来定义自己的操作。默认情况下,栈适配器建立在 deque 容器上,因此采用 deque 提供的操作来实现栈功能。
9.7.5. 队列和优先级队列
priority_queue 允许用户为队列中存储的元素设置优先级。标准库默认使用元素类型的 < 操作符来确定它们之间的优先级关系。也就是默认从大到小排序。类似于大根堆。
队列和优先级队列支持的操作:
//如果队列为空,则返回 true,否则返回 false
q.empty()
//返回队列中元素的个数
q.size()
//删除队首元素,但不返回其值
q.pop()
//返回队首元素的值,但不删除该元素.该操作只适用于队列
q.front()
//返回队尾元素的值,但不删除该元素 该操作只适用于队列
q.back()
//返回具有最高优先级的元素值,但不删除该元素 该操作只适用于优先级队列
q.top()
//对于 queue,在队尾压入一个新元素,
//对于 priority_quue,在基于优先级的适当位置插入新元素
q.push(item)
第十章 关联容器
关联容器和顺序容器的本质差别在于:关联容器通过键(key)存储和读取元素,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。关联容器默认按照key从小到大排列。
关联容器类型
//关联数组:元素通过键来存储和读取
map
//大小可变的集合,支持通过键实现的快速读取
set
//支持同一个键多次出现的 map 类型
multimap
//支持同一个键多次出现的 set 类型
multiset
10.1. pair 类型(键值对)
开始介绍关联容器之前,必须先了解一种与之相关的简单的标准库类型——pair,该类型在 utility 头文件中定义。
pair类型提供的操作:
//创建一个空的 pair 对象,它的两个元素分别是 T1 和 T2 类型,采用值初始化
pair<T1, T2> p1;
//创建一个 pair 对象,它的两个元素分别是 T1 和 T2 ,
//其中 first 成员初始化为 v1,而 second 成员初始化为 v2
pair<T1, T2> p1(v1, v2);
//以 v1 和 v2 值创建一个新 pair 对象,其元素类型分别是 v1 和 v2 的类型
make_pair(v1, v2)
//小于运算,遵循字典次序
p1 < p2
//等于运算,字典次序
p1 == p2
//返回 p 中名为 first 的(公有)数据成员
p.first
//返回 p 的名为 second 的(公有)数据成员
p.second
10.1.1. pair 的创建和初始化
pair 包含两个数据值。与容器一样,pair 也是一种模板类型。
但又与之前介绍的容器不同,在创建 pair 对象时,必须提供两个类型名。
pair<string, string> anon; // 持有两个 string类型
pair<string, int> word_count; // 持有一个 string 一个 int类型
pair<string, vector<int> > line; // 持有一个 string,一个 vector<int>类型
如果在创建 pair 对象时不提供初始化式,则调用默认构造函数对其成员采用值初始化。int就是0,string、vector这种就是空值。
也可在定义时为每个成员提供初始化式。
pair<string, string> author("James", "Joyce");
pair 类型的使用相当繁琐,因此,如果需要定义多个相同的 pair 类型对象,可考虑利用 typedef 简化其声明:
typedef pair<string, string> Author;
Author proust("Marcel", "Proust");
Author joyce("James", "Joyce");
10.1.2. pair对象的操作
与其他标准库类型不同,对于 pair 类,可以直接访问其数据成员。
其成员都是仅有的,分别命名为 first 和 second。只需使用普通的点操作符——成员访问标志即可访问其成员。
string firstBook;
//直接访问pair对象的成员
if (author.first == "James" && author.second == "Joyce")
firstBook = "Stephen Hero";
10.1.3. 生成新的pair对象
除了构造函数,标准库还定义了一个 make_pair 函数,由传递给它的两个实参生成一个新的 pair 对象。
pair<string, string> next_auth;
string first, last;
while (cin >> first >> last) {
//生成pair对象
next_auth = make_pair(first, last);
// process next_auth...
}
10.2. 关联容器
//关联数组:元素通过键来存储和读取
map
//大小可变的集合,支持通过键实现的快速读取
set
//支持同一个键多次出现的 map 类型
multimap
//支持同一个键多次出现的 set 类型
multiset
关联容器共享大部分的顺序容器操作。
关联容器不提供 front、 push_front、 pop_front、back、push_back 以及 pop_back 操作。
顺序容器和关联容器公共的操作包括下面的几种:
构造函数:
C<T> c; //默认构造函数
C<T> c1(c2); // 拷贝构造函数
C<T> c(b, e); // 两个迭代器,左闭右开,取中间内容拷贝
关系运算。
begin、end、rbegin、rend操作。
对于 map 容器,value_type 并非元素的类型,而是描述键及其关联值类型的 pair 类型。
根据键排列元素
关联容器使用键来重新定义基本操作,确保元素按键的次序排列,迭代访问时遵循此键序,与存储位置无关。
10.3. map 类型
map 是键-值对的集合。
map 类型通常可理解为关联数组(associative array):可使用键作为下标来获取一个值,正如内置数组类型一样。而关联的本质在于元素的值与某个特定的键相关联,而并非通过元素在数组中的位置来获取。
10.3.1. map 对象的定义
要使用 map 对象,则必须包含 <map> 头文件。在定义 map 对象时,必须分别指明键和值的类型(value type):
map<string, int> word_count; // 创建空的键值对集合
map 的构造函数:
//创建map对象 k键 v值
map<k, v> m;
//创建map对象,初始化为参数副本,有相同键类型值类型
map<k, v> m(m2);
//创建map对象,迭代器左闭右开范围.元素的类型必须能转换为 pair<const k, v>
map<k, v> m(b, e);
10.3.2. 键类型的约束
关联容器的键类型必须能够定义或提供一个比较函数,该函数默认是<操作符。
如果键类型没有提供默认的 < 操作符,或者默认的<操作符不符合业务逻辑的需求,可以通过在关联容器模板中指定第三个模板参数(即比较函数或比较对象)来自定义比较逻辑。这提供了极大的灵活性,允许开发者根据实际应用场景来定义键之间的比较规则。
10.3.3. map 定义的类型
map 对象的元素是键-值对,也即每个元素包含两个部分:键以及由键关联的值。
map 的 value_type 就反映了这个事实。
map 的 value_type 是存储元素的键以及值的 pair 类型,而且键为 const。
map定义的类型:
//在 map 容器中,用做索引的键的类型
map<K, V>::key_type
//在 map 容器中,键所关联的值的类型
map<K, V>::mapped_type
//一个 pair 类型,它的 first 元素为 const map<K, V>::key_type 类型,
//而 second 元素则为 map<K, V>::mapped_type 类型
map<K, V>::value_type
谨记 value_type 是 pair 类型,它的值成员可以修改,但键成员不能修改。
map 迭代器进行解引用将产生 pair 类型的对象
对迭代器进行解引用时,将获得一个引用,指向容器中一个 value_type 类型的值。
// 获得迭代器,指向 pair类型地址
map<string, int>::iterator map_it = word_count.begin();
// 对迭代器解引用, 得到一个pair类型 pair
cout << map_it->first; // pair.first 是key,为常量
cout << " " << map_it->second; // pair.second 是value
对于 map 容器,其 value_type 是 pair 类型:对迭代器进行解引用将获得一个 pair 对象,它的 first 成员存放键,为const,而 second 成员则存放值。
10.3.4. map 容器额外定义的类型别名(typedef)
map 类额外定义了两种类型:key_type 和 mapped_type,以获得键或值的类型。
如同顺序容器一样,可使用作用域操作符来获取类型成员,如 map<string, int>::key_type。
10.3.5. 给 map 添加元素
定义了 map 容器后,下一步工作就是在容器中添加键-值元素对。
可使用 insert 成员函数实现;
或者,先用下标操作符获取元素,然后给获取的元素赋值。
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> word_count;
// 使用insert函数添加单个元素
word_count.insert(std::make_pair("apple", 3));
// 或者使用C++11及以后版本的初始化列表
word_count.insert({"banana", 5});
// 插入一个已存在的键会导致其值被更新(不会插入新元素)
word_count.insert(std::make_pair("apple", 10)); // "apple"的值现在变为10
// 遍历并打印map
for (const auto& pair : word_count) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
map <string, int> word_count;
//使用下标Key访问
word_count["Anna"] = 1;
用下标访问不存在的元素将导致在 map 容器中添加一个新元素,它的键即为该下标值,值则采用值初始化。
10.3.6. map::insert 的使用
map 容器提供了 insert操作。
//插入。不存在则新增,存在则不操作.
//返回 一个pair类型,key为指向该元素的迭代器,value为bool表示是否插入
m.insert(e)
//左闭右开迭代器.范围内的元素如果不存在则新增.返回 void
m.insert(beg, end)
//插入,并以迭代器为起点搜索新元素的位置.返回一个迭代器,指向给定键的元素
m.insert(iter, e)
//插入
word_count.insert(map<string, int>::value_type("Anna", 1));
谨记 map::value_type 是 pair<const K, V> 类型的同义词,K 为键类型,而 V 是键所关联的值的类型。
让 insert代替下标运算
在添加新 map 元素时,使用 insert 成员可避免使用下标操作符所带来的副作用:不必要的初始化。
map <string, int> word_count;
//使用下标Key访问
word_count["Anna"] = 1;
传递给 insert 的实参相当笨拙。可用两种方法简化:使用 make_pair,或者使用 typedef。
word_count.insert(make_pair("Anna", 1));
typedef map<string,int>::value_type valType;
word_count.insert(valType("Anna", 1));
检测 insert的返回值
map 容器中的每个键是唯一的,并映射到一个特定的值。
使用 insert 函数向 map 中添加一个新的键值对时,如果键已存在,则不会插入新的元素,而是保持原有键值对不变。
对于接受一个键值对作为参数的 insert 方法,它会返回一个包含迭代器和布尔值的键值对。这个迭代器指向 map 中具有该键的元素,而布尔值则指示是否成功插入了新元素:
如果键不存在于 map 中,则新元素被插入,返回布尔值为 true,迭代器指向新插入的元素。
如果键已存在,则不执行插入操作,但返回的布尔值为 false(尽管没有新元素被插入,但逻辑上认为操作“成功”找到了已存在的元素),迭代器指向已存在的那个元素。
10.3.7. 查找并读取 map 中的元素
下标操作符
map<string,int> word_count;
int occurs = word_count["foobar"];
下标存在一个很危险的副作用:如果该键不在 map 容器中,那么下标操作会插入一个具有该键的新元素。
map 容器提供了两个读取操作:count 和 find。用于检查某个键是否存在而不会插入该键。
//返回 map中 key 的出现次数
m.count(k)
//存在Key,则返回迭代器指向位置;不存在,则返回迭代器超出末端,即iteraotr.end()
m.find(k)
10.3.8. 从 map 对象中删除元素
从 map 容器中删除元素的 erase 操作有三种变化形式。
//删除 m 中键为 k 的元素。返回 size_type 类型的值,表示删除的元素个数
m.erase(k)
//从 m 中删除迭代器iterator所指向的元素。返回 void
m.erase(iterator)
//删除。迭代器范围左闭右开。返回end迭代器
m.erase(ite_a, ite_b)
10.3.9. map 对象的迭代遍历
与其他容器一样,map 同样提供 begin 和 end 运算,以生成用于遍历整个容器的迭代器。
map<string, int>::const_iterator map_it = word_count.begin();
while (map_it != word_count.end()){...}
10.4. set 类型
set 容器只是单纯的键的集合。
除了两种例外情况,set 容器支持大部分的 map 操作,包括下面几种:
• 所有通用的容器操作。
• 无参、复制、俩迭代器做范围的构造函数。
• insert 操作。
• count 和 find 操作。
• erase 操作。
set 不支持下标操作符,而且没有定义 mapped_type 类型。
set 容器的 value_type 不是 pair 类型,而是与 key_type 相同的类型。
与 map 一样,set 容器存储的键也必须唯一,而且不能修改。
10.4.1. set 容器的定义和使用
定义
vector<int> ivec;
set<int> iset(ivec.begin(), ivec.end());//类型兼容,所以借助迭代器,用vector初始化set
在 set 中添加元素
set<string> set1;
set1.insert("the");
set1.insert("and");
iset1.insert(ivec.begin(), ivec.end());
从 set 中获取元素
iset.find(1)//找到key为1的元素位置,返回迭代器指向它
10.5. multimap 和 multiset 类型
map 和 set 容器中,一个键只能对应一个实例。而 multiset 和 multimap类型则允许一个键对应多个值。最常见电话簿。
multimap 和 multiset 所支持的操作分别与 map 和 set 的操作相同,
但是,multimap 不支持下标运算。
10.5.1. 元素的添加和删除
insert 操作和 erase 操作同样适用于multimap 以及 multiset 容器,实现元素的添加和删除。
由于键不要求是唯一的,因此每次调用 insert 总会添加一个元素。
10.5.2. 在 multimap 和 multiset 中查找元素
关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放。
在 map 或 set 容器中查找一个元素很简单——该元素要么在要么不在容器中。但对于 multimap 或 multiset,该过程就复杂多了:某键对应的元素可能出现多次。
使用 find 和 count 操作
count 函数求出某键出现的次数。
find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例。
typedef multimap<string, string>::size_type sz_type;//size_type类型
sz_type entries = authors.count(search_item);//用size_type存放count操作统计的数量
multimap<string,string>::iterator iter = authors.find(search_item);//find操作返回第一个元素的迭代器
lower_bound 和 upper_bound
lower_bound 和 upper_bound。适用于所有的关联容器,也可用于普通的 map 和 set 容器,但更常用于 multimap 和 multiset。所有这些操作都需要传递一个键,并返回一个迭代器。
//返回一个迭代器,指向键不小于 k 的第一个元素
m.lower_bound(k)
//返回一个迭代器,指向键大于 k 的第一个元素
m.upper_bound(k)
//返回一个迭代器的 pair 对象
//它的 first 成员等价于 m.lower_bound(k)。而 second 成员则等价于 m.upper_bound(k)
m.equal_range(k)
在同一个键上调用 lower_bound 和 upper_bound,将产生一个迭代器范围。并有左开右闭的效果。
equal_range 函数
调用 equal_range 函数来取代调用 upper_bound 和 lower_bound 函数。equal_range 函数返回存储一对迭代器的 pair 对象。
如果该值存在,则 pair 的key相当于 lower_bound返回的迭代器,pair的value相当于 upper_bound返回的迭代器。有左开右闭的效果。
第十一章 泛型算法
标准库提供了泛型算法,这些算法能执行常见操作而不依赖于特定容器类型,包括标准库容器(如vector、list)和内置数组等,实现了操作的通用性和灵活性。
11.1. 概述
泛型算法不依赖于特定的容器类型,而是普遍适用于能够遍历且元素间可比较的集合。其关键要求包括:
遍历机制:需要一种方式来顺序访问集合中的每个元素,即从当前元素移动到下一个元素。
终止条件:必须能够判断是否已经遍历完集合中的所有元素,即达到集合的末尾。
比较能力:算法需要对集合中的每个元素与待查找的元素进行比较。
位置或未找到指示:算法执行后,需要有一种方式来指示是否找到了元素。
泛型算法用迭代器来解决第一个要求:遍历容器。所有迭代器都支持自增操作符,从一个元素定位下一个元素,并提供解引用操作符访问元素的值。
大多数算法通过两个迭代器来指定操作元素的范围:第一个迭代器指向范围的起始元素,第二个迭代器范围外的第一个位置,作为遍历的终止哨兵。这种设计允许算法以统一的方式处理不同大小和类型的容器。
当泛型算法(如查找算法)未找到指定元素时,返回超出末端迭代器作为未找到的指示。这种方式简化了算法的设计和使用。
泛型算法默认使用元素类型的相等(==)操作符来比较元素。如果元素类型不支持此操作或需要不同的比较逻辑,算法提供了接受自定义比较函数的版本。这增加了算法的灵活性和适用性。
泛型算法的实现不依赖于具体的容器类型,也不直接操作容器。它们通过迭代器来访问和遍历元素,这使得算法能够应用于任何支持迭代器操作的集合。这种设计使得算法具有高度的通用性和可重用性。
标准库提供了超过100种算法,像find、count这类,这些算法具有一致的结构和原理。
11.2. 初窥算法
在研究算法标准库的结构之前,先看一些例子。上一节已经介绍了 find 函数的用法;本节将要使用其他的一些算法。使用泛型算法必须包含 algorithm 头文件:
#include <algorithm>
标准库还定义了一组泛化的算术算法,其命名习惯与泛型算法相同。使用这些算法则必须包含 numeric 头文件:
#include <numeric>
11.2.1. 只读算法
许多算法只会读取其输入范围内的元素,而不会写这些元素。find 就是一个这样的算法。另一个简单的只读算法是 accumulate,该算法在 numeric 头文件中定义。
vector<int> vec;
int sum = accumulate(vec.begin(), vec.end(), 42);//范围内相加,再加个指定值,加法运算
string sum = accumulate(v.begin(), v.end(), string(""));//范围内相加,再加个指定字符,加法拼接
find_first_of 的使用
//找到范围2中任何元素第一次在范围1出现的位置,返回迭代器。没有任何匹配则返回roster1.end()
find_first_of(roster1.start(), roster1.end(), roster2.begin(), roster2.end())
11.2.2. 写容器元素的算法
写入输入序列的元素
写入到输入序列的一个简单算法是 fill 函数。
fill(vec.begin(), vec.end(), 0); // 填充范围的值为0
fill 带有一对迭代器形参,用于指定要写入的范围,而所写的值是它的第三个形参的副本。执行时,如果输入范围有效,则可安全写入。
不检查写入操作的算法
fill_n 函数从迭代器指向的元素开始,将指定数量的元素设置为给定的值。初学者常犯的错误的是:在没有元素的空容器上调用 fill_n 函数(或者类似的写元素算法)。
vector<int> vec;
fill_n(vec.begin(), 10, 0);//从指定位置开始,填充指定数量的元素
在没有元素的空容器上调用 fill_n 函数。容器是空的。很可能导致严重的运行时错误。
对指定数目的元素做写入运算,或者写到目标迭代器的算法,都不检查目标的大小是否足以存储要写入的元素。
引入 back_inserter()
确保算法有足够的元素存储输出数据的一种方法是使用插入迭代器。
迭代器适配器 back_inserter() 在试图给元素赋值时,赋值运算将调用 push_back 在容器中添加一个具有指定值的元素。
fill_n (back_inserter(vec), 10, 0);
现在,fill_n 函数每写入一个值,都会通过 back_inserter 生成的插入迭代器实现。效果相当于在 vec 上调用 push_back,在 vec 末尾添加 10 个元素,每个元素的值都是 0。
出现这种区别的原理是 vec.begin() 是一个简单的随机访问迭代器,它指向容器中的固定位置;而 back_inserter() 是一个特殊的迭代器适配器,它在被解引用时执行插入操作,会返回新的迭代器。
写入到目标迭代器的算法
第三类算法向目标迭代器写入未知个数的元素。这类算法中最简单的是 copy 函数。
vector<int> ivec; //空容器
copy (ilst.begin(), ilst.end(), back_inserter(ivec));//复制一个容器范围到另一个容器
当然,这个例子的效率比较差:通常,如果要以一个已存在的容器为副本创建新容器,更好的方法是直接用输入范围作为新构造容器的初始化式。
vector<int> ivec(ilst.begin(), ilst.end());
算法的 _copy 版本
有些算法提供所谓的“复制(copying)”版本。不改变输入序列,而是创建一个新容器存储元素的处理结果。
replace 算法就是一个很好的例子。该算法对输入序列做读写操作,将序列中特定的值替换为新的值。
replace(ilst.begin(), ilst.end(), 0, 42);//将范围内特定的值替换为新值
如果不想改变原序列,那么想办法获得一个序列的副本。
//ivec存储副本,将指定范围内的指定元素替换为指定值
replace_copy (ilst.begin(), ilst.end(), back_inserter(ivec), 0, 42);
11.2.3. 对容器元素重新排序的算法
排序
sort 算法带有两个迭代器实参,指出要排序的元素范围。这个算法使用小于(<)操作符比较元素。
//排序,默认用<,从小到大
sort(words.begin(), words.end());
去重:unique 的使用
unique算法可以将元素不重复的内容复制到序列前端,覆盖重复的元素,并返回迭代器指向无重复尾部的下一个位置。
注意,unique没有删除元素,而是覆盖。
//将指定范围内无重复的元素复制到序列前端,覆盖重复的元素
//返回迭代器,指向无重复尾部的下一个位置
unique(words.begin(), words.end());
使用容器操作删除元素
如果要删除重复的项,必须使用容器操作,调用 erase 实现该功能。
对没有重复元素的 vector 对象,调用 erase 也是安全的。
算法不直接修改容器的大小。如果需要添加或删除元素,则必须使用容器操作。
定义需要的实用函数(谓词)
考虑统计vector<string>中长度不小于6的单词个数。
为了解决这个问题,需要用到另外两个泛型算法:stable_sort 和 count_if。
使用这些算法,还需要一个配套的实用函数,称为谓词。谓词是做某些检测的函数,返回用于条件判断的类型,指出条件是否成立。
为了实现排序,必须定义一个谓词函数来实现两个 string 对象的比较,并返回一个 bool 值,指出第一个字符串是否比第二个短:
bool isShorter(const string &s1, const string &s2)
{
return s1.size() < s2.size();
}
另一个所需的谓词函数将判断给出的 string 对象的长度是否不小于 6:
bool GT6(const string &s)
{
return s.size() >= 6;
}
排序算法
除 sort 之外,标准库还定义了 stable_sort 算法,stable_sort 保留相等元素的原始相对位置。
//不改变相等元素的原始相对位置的排序
stable_sort(words.begin(), words.end(), isShorter);//注意第三个函数是排序方法,[谓词],自定义的
统计算法
执行 count_if 时,首先读取它的头两个实参所标记的范围内的元素。每读出一个元素,就将它传递给第三个实参表示的谓词函数。
vector<string>::size_type wc =
count_if(words.begin(), words.end(), GT6);
11.3. 再谈迭代器
标准库所定义的泛型迭代器不依赖于特定的容器。
事实上,C++ 语言还提供了另外三种迭代器:
插入迭代器:
back_inserter()。front_inserter()。inserter。
这类迭代器与容器绑定在一起,实现在容器中插入元素的功能。
iostream 迭代器:
istream_iterator、ostream_iterator。
这类迭代器可与输入或输出流绑定在一起,用于迭代遍历所关联的 IO 流。
反向迭代器:
reverse_iterator。
这类迭代器实现向后遍历,而不是向前遍历。所有容器类型都定义了自己的反向,由 rbegin 和 rend 成员函数返回。
11.3.1. 插入迭代器
插入迭代器是一种迭代器适配器,带有一个容器参数,并生成一个迭代器,用于在指定容器中插入元素。
C++ 语言提供了三种插入器,其差别在于插入元素的位置不同。
• back_inserter,创建使用 push_back 实现插入的迭代器。尾插。
• front_inserter,使用 push_front 实现插入。头插。
• inserter,使用 insert 实现插入操作。指定位置插。
list<int>::iterator it =
find (ilst.begin(), ilst.end(), 42); //找到范围内指定元素的位置,返回迭代器
/*replace_copy函数,*/
replace_copy (ivec.begin(), ivec.end(),
inserter (ilst, it), 100, 0);//普通插入迭代器inserter返回一个指定位置的迭代器
//replace_copy
//将另一个容器指定范围的指定内容替换为指定值
11.3.2. iostream 迭代器
虽然 iostream 类型不是容器,但标准库同样提供了在 iostream 对象上使用的迭代器:istream_iterator 用于读取输入流,而 ostream_iterator 则用于写输出流。
iostream 迭代器的构造函数:
//创建从输入流 strm 中读取 T 类型对象的 istream_iterator 对象
istream_iterator<T> in(strm);
//istream_iterator 对象的超出末端迭代器
istream_iterator<T> in; //一个输入流迭代器,没绑定流,用处是作为其他迭代器是否到达end的判断对象
//创建将 T 类型的对象写到输出流 strm 的 输出流迭代器 对象
ostream_iterator<T> in(strm);
//创建将 T 类型的对象写到输出流 strm 的 输出流迭代器 对象,
//在写入过程中使用 delim 作为元素的分隔符。delim 是以空字符结束的字符数组
ostream_iterator<T> in(strm, delim);
流迭代器只定义了最基本的迭代器操作:自增、解引用和赋值。此外,可比较两个输入迭代器istream_iterator是否相等(或不等)。而输出迭代器 ostream_iterator则不提供比较运算。
istream_iterator 的操作
//比较两个迭代器是否相等,
//其实就是比较是否都指向end,
//创建流迭代器时使用的构造函数是否一样
it1 == it2
it1 != it2
//迭代器解引用。从流中读取的对象
*it
//解引用。从流中读取的对象的成员
it->mem //是 (*it).mem 的同义诩。
//通过使用元素类型提供的 >> 操作从输入流中读取下一个元素值,
//移动迭代器并返回迭代器+1后的引用
++it it++
流迭代器的定义
流迭代器都是类模板:任何已定义输入操作符(>> 操作符)的类型都可以定义 istream_iterator。类似地,任何已定义输出操作符(<< 操作符)的类型也可定义 ostream_iterator。
//创建从输入流cin中读取int类型对象的迭代器对象。
istream_iterator<int> cin_it(cin);
//输入流迭代器,没绑定流,可被用来 ==、!= ,作为其他迭代器是否到达end的判断对象
istream_iterator<int> end_of_stream;
ofstream outfile; //定义文件输出流
//创建输出流迭代器,用来将元素写出到输出流,以" "分隔
ostream_iterator<int> output(outfile, " ");
istream_iterator 对象上的操作
构造与流绑定在一起的 istream_iterator 对象时将对迭代器定位,以便第一次对该迭代器进行解引用时即可从流中读取第一个值。
//创建一个从输入流读int的输入流迭代器
istream_iterator<int> in_iter(cin);
//创建一个end迭代器,不需要绑定输入流
istream_iterator<int> eof;
//判断有没有到end
while (in_iter != eof)
//把输入流元素添加到vec容器
vec.push_back(*in_iter++);
更有趣的是可以这样重写程序:
istream_iterator<int> in_iter(cin); //输入流迭代器,绑定输入流
istream_iterator<int> eof; //输入流end迭代器
//迭代器指定范围,来初始化vector
vector<int> vec(in_iter, eof);
ostream_iterator 对象和 ostream_iterator 对象的使用
可使用 ostream_iterator 对象将一个值序列写入流中。
//将每个用\n分割的字符串写入到标准输出流stdout
ostream_iterator<string> out_iter(cout, "\n");
//创建一个从cin读字符串的输入迭代器,和 输入迭代器的end迭代器
istream_iterator<string> in_iter(cin), eof;
//输入迭代器没有遍历到end
while (in_iter != eof)
//把输入迭代器的内容读到输出迭代器
*out_iter++ = *in_iter++;
流迭代器的限制
流迭代器有下面几个重要的限制:
输出流迭代器 ostream_iterator 只写,输入流迭代器 istream_iterator只读。
一旦给 ostream_iterator 对象赋了一个值,写入就提交了。赋值后,没有办法再改变这个值。此外,ostream_iterator 对象中每个不同的值都只能正好输出一次。
ostream_iterator 没有 解引用 -> 操作符。
与算法一起使用流迭代器
算法是基于迭代器操作实现的。同样支持 查找、排序、计数等算法。
find、find_first_of()、sort、count
istream_iterator<int> cin_it(cin);
istream_iterator<int> end_of_stream;
vector<int> vec(cin_it, end_of_stream);
sort(vec.begin(), vec.end());
ostream_iterator<int> output(cout, " ");
//将指定范围内无重复的元素复制到序列前端,覆盖重复的元素
//返回迭代器,指向无重复尾部的下一个位置.内容副本给到output
unique_copy(vec.begin(), vec.end(), output);
11.3.3. 反向迭代器
反向迭代器是一种反向遍历容器的迭代器。反向迭代器将自增(和自减)的含义反过来了:对于反向迭代器,++ 运算将访问前一个元素,而 -- 运算则访问下一个元素。
所有容器都定义了 begin 和 end 成员,返回指向容器首元素和尾元素下一位置的迭代器。容器还定义了 rbegin 和 rend 成员,返回指向容器尾元素和首元素前一位置的反向迭代器。
为了以降序排列 vector,只需向 sort传递一对反向迭代器:
// 从小到大排序
sort(vec.begin(), vec.end());
// 从大到小排序
sort(vec.rbegin(), vec.rend());
11.3.4. const 迭代器
如果该容器是 const 对象,则调用start、end、rstart、rend等操作返回的迭代器是 const_iterator 类型;否则,就是普通的 iterator 类型。
11.3.5. 五种迭代器
可根据算法要求它的迭代器提供什么类型的操作,对算法分类。
有一些算法,例如 find,只要求迭代器提供读取所指向内容和自增的功能。另一些算法,,比如 sort,则要求其迭代器有读、写和随机访问元素的能力。算法要求的迭代器操作分为五个类别。
Input iterator(输入迭代器) 读,不能写;只支持自增运算
Output iterator(输出迭代器) 写,不能读;只支持自增运算
Forward iterator(前向迭代器) 读和写;只支持自增运算
Bidirectional iterator(双向迭代器) 读和写;支持自增和自减运算
Random access iterator(随机访问迭代器) 读和写;支持完整的迭代器算术运算
11.4. 泛型算法的结构
正如所有的容器都建立在一致的设计模式上一样,算法也具有共同的设计基础。
11.4.1. 算法的形参模式
算法通常通过一组特定的参数来定义其行为,这些参数包括定义操作范围的迭代器和可能的其他参数。常见的算法参数模式分为四种:
alg (beg, end, other parms);
alg (beg, end, dest, other parms);
alg (beg, end, beg2, other parms);
alg (beg, end, beg2, end2, other parms);
单范围操作:作用于一个元素范围,从beg到end(不含end),可能包括其他特定参数。
单范围到目的地:除了指定输入范围,还通过dest指定输出目标,常用于复制、转换或合并到另一容器。
双范围操作:同时作用于两个序列,第一个从beg到end,第二个从beg2开始,用于比较、合并等。
双范围到双范围:操作两个完整序列,各由一对迭代器定义,常用于基于两个输入序列的操作,如比较、复制合并等。
带有单个目标迭代器的算法
dest迭代器用于指向算法输出数据的存储位置。调用算法前,需确保目标有足够的容量,否则可能出错。
常用插入迭代器(如std::back_inserter)自动扩展容器,或ostream_iterator写入输出流,避免容量问题。
带第二个输入序列的算法
带有beg2和end2的算法操作两个完整输入范围,beg/end为第一个范围,beg2/end2为第二个范围。仅带beg2的算法假定第二个范围从beg2开始,至少与第一个范围一样大,但没有明确指定结束点。这些算法均假定两个序列大小相匹配或第二个序列足够大。
11.4.2. 算法的命名规范
标准库算法遵循命名和重载规范,分为测试元素算法和排序算法两大类。
测试元素算法使用 == 或 < 等操作符,并提供可选的谓词函数参数版本,如find_if用于更灵活的查找。
排序算法默认使用 < 操作符,但可通过额外参数提供自定义比较函数。由于某些算法(如查找)的两种版本参数数量相同,为避免重载引起的二义性,标准库采用不同命名(如find与find_if)而非重载。对于重新排列元素的算法,标准库提供了写入原地和复制到指定目标的两种版本,后者通过添加_copy后缀来区分(如reverse与reverse_copy)。
11.5. 容器特有的算法
list 容器使用双向迭代器,不支持随机访问,因此不能使用需要随机访问迭代器的算法(如sort)。尽管一些泛型算法(如merge、remove、reverse、unique)可用于 list,但效率可能不如针对 list 优化的算法。list容器提供了特定于它的精细操作集,利用这些操作可以实现更高效的算法,而不仅仅是依赖通用的迭代器操作。
list 容器特有的操作
//将已排序的 lst2 合并到 lst 中,lst2 被清空。
lst.merge(lst2) //使用 < 操作符.
lst.merge(lst2, comp) //使用自定义比较函数
//删除等于指定值 val的元素
lst.remove(val)
//删除使谓词函数返回真的所有元素。
lst.remove_if(unaryPred)
//反转 lst 中元素的顺序。
lst.reverse()
//对 lst 中的元素进行排序。
lst.sort()
//将 lst2 中的元素移动到 lst 中指定迭代器 iter 指向的元素之前。lst2 中相应的元素被删除。
lst.splice(iter, lst2)
lst.splice(iter, lst2, iter2) //只移动iter2指向的元素
lst.splice(iter, beg, end) //移动范围元素
//删除重复的元素
lst.unique() //使用 ==判断
lst.unique(binaryPred) //使用 谓词函数判断
对于 list 对象,应该优先使用 list 容器特有的成员版本,而不是泛型算法。
list 容器特有的算法与其泛型算法版本之间有两个至关重要的差别。
其中一个差别是 remove 和 unique 的 list 版本修改了其关联的基础容器:真正删除了指定的元素。
另一个差别是 list 容器提供的 merge 和 splice 运算会破坏它们的实参。使用 merge 的泛型算法版本时,合并的序列将写入目标迭代器指向的对象,而它的两个输入序列保持不变。但是,使用 list 容器的 merge 成员函数时,当参对象的元素合并到调用 merge 函数的list 对象时,实参对象的元素被移出并删除。