C++课程学习笔记第九周(上):STL(上)
前言:本文主要是根据MOOC网北大课程——《程序设计与算法(三):C++面向对象程序设计》内容整理归纳而来,整理的课程大纲详见 https://www.cnblogs.com/inchbyinch/p/12398921.html
本文先介绍STL概要和函数对象,STL(下)中再详细介绍关联容器、容器适配器和算法。
0 STL
C++语言的核心优势之一就是便于软件的重用,C++中有两个方面体现重用:
- 面向对象的思想:继承和多态,标准类库
- 泛型程序设计(generic programming)的思想:模板机制,以及标准模板库STL
标准模板库(英文:Standard Template Library,缩写:STL),是一个C++软件库,主要包含4个组件,分别为容器、迭代器、算法、函数。简单来说,STL就是一些常用数据结构和算法的模板的集合。
1 容器
可以用于存放各种类型的数据(基本类型的变量,对象等)的数据结构,都是类模版,分为三种:
- 顺序容器:vector, deque, list
- 关联容器:set, multiset, map, multimap
- 容器适配器:stack, queue, priority_queue
注意:
- 许多算法,比如排序,查找,要求对容器中的元素进行比较,有的容器本身就是排序的,所以,放入容器的对象所属的类,往往还应该重载 == 和 < 运算符。
- 对象被插入容器中时,被插入的是对象的一个复制品。C++11推出emplace_front、emplace和emplace_back等函数,用于构造而不是拷贝元素。
- C++11中提供了unordered_map和unordered_multimap,实现方式是哈希表,更好用。
1.1 顺序容器
1.1.1 vector
内部采用动态数组,头文件 < vector >,元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能(大部分情况下是常数时间,除非需要二倍地扩充空间)。
//vector的初始化
vector<int> v1;
vector<string> v2;
vector<int> v3(v1); //用v1初始化
vector<int> v4(v1.begin(),v1.end()) //用v1的部分来初始化
vector<int> v5(10); //创建一个大小为10的vector,默认初始化为0
vector<string> v6(4); //默认初始化为空字符串
vector<int> v7(5, -1); //第一个参数是数目,第二个参数是要初始化的值
vector<string> v8(3, "hi");
vector<int> v9 = { 1,2,3,4,5 }; //列表初始化,注意使用的是花括号
vector<string> v10 = { "hi","my","name","is","lee" };
vector<vector<int> > v(10); //创建二维数组,注意保留空格
//示例
template<class T>
void PrintVector( T s, T e){
for(; s != e; ++s)
cout << * s << " ";
cout << endl;
}
int main(){
int a[5] = { 1,2,3,4,5 };
vector<int> v(a,a+5); //将数组a的内容放入v
cout << "1) " << v.end() - v.begin() << endl;
//两个随机迭代器可以相减,输出 1) 5
cout << "2) "; PrintVector(v.begin(),v.end());
//2) 1 2 3 4 5
v.insert(v.begin() + 2, 13); //在begin()+2位置插入 13
cout << "3) "; PrintVector(v.begin(),v.end());
//3) 1 2 13 3 4 5
v.erase(v.begin() + 2); //删除位于 begin() + 2的元素
cout << "4) "; PrintVector(v.begin(),v.end());
//4) 1 2 3 4 5
vector<int> v2(4,100); //v2 有4个元素,都是100
v2.insert(v2.begin(),v.begin()+ 1,v.begin()+3);
//将v的一段插入v2开头
cout << "5) v2: "; PrintVector(v2.begin(),v2.end());
//5) v2: 2 3 100 100 100 100
v.erase(v.begin() + 1, v.begin() + 3);
//删除 v 上的一个区间,即 2,3
cout << "6) "; PrintVector(v.begin(),v.end());
//6) 1 4 5
return 0;
}
注意:
- 虽然vertor对象可以动态增长,但是也有一点副作用:就是任何一种可能改变vector对象容量的操作,比如push_back,都会使该迭代器失效。所以,对于使用了迭代器的循环体,不要向迭代器所属的容器添加元素!或者添加时要记得更新迭代器。
- vector作为函数的参数或者返回值时,需要注意它的写法:double Distance(vector< int >& a, vector< int >& b) 其中的“&”绝对不能少!
1.1.2 deque
内部采用双向队列,头文件 < deque >,元素在内存连续存放。随机存取任何元素都能在常数时间完成(但次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间,除非需要扩充空间)。
操作上:
- 所有适用于vector的操作都适用于deque
- deque还有 push_front(将元素插入到前面)和pop_front(删除最前面的元素)操作,复杂度是O(1)
1.1.3 list
内部采用双向链表,头文件 < list >,元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
除了具有所有顺序容器都有的成员函数以外,还支持8个成员函数:
- push_front: 在前面插入
- pop_front: 删除前面的元素
- sort: 排序 ( list 不支持 STL 的算法 sort)
- remove: 删除和指定值相等的所有元素
- unique: 删除所有和前一个元素相同的元素(要做到元素不重复,则unique之前还需要sort)
- merge: 合并两个链表,并清空被合并的那个
- reverse: 颠倒链表
- splice: 在指定位置前面插入另一链表中的一个或多个元素,并在另一链表中删除被插入的元素
//示例
class A { //自定义一个类
int n;
public:
A( int n_ ) { n = n_; }
friend bool operator<(const A& a1, const A& a2){ return a1.n < a2.n; }
friend bool operator==(const A& a1, const A& a2){ return a1.n == a2.n; }
friend ostream & operator <<(ostream& o, const A& a){
o << a.n;
return o;
}
};
//打印链表的模板。形参这里不推荐,最好用两个迭代器
template <class T>
void PrintList(const list<T> & lst) {
int tmp = lst.size();
if( tmp > 0 ) {
typename list<T>::const_iterator i;
i = lst.begin();
for( i = lst.begin();i != lst.end(); i++)
cout << * i << ",";
}
}// typename用来说明 list<T>::const_iterator是个类型, 在vs中不写也可以
int main() {
list<A> lst1,lst2;
lst1.push_back(1); lst1.push_back(3); lst1.push_back(2);
lst1.push_back(4); lst1.push_back(2);
lst2.push_back(10); lst2.push_front(20); lst2.push_back(30);
lst2.push_back(30); lst2.push_back(30);
lst2.push_front(40); lst2.push_back(40);
cout << "1) "; PrintList( lst1); cout << endl;
// 1) 1,3,2,4,2,
cout << "2) "; PrintList( lst2); cout << endl;
// 2) 40,20,10,30,30,30,40,
lst2.sort();
cout << "3) "; PrintList( lst2); cout << endl;
//3) 10,20,30,30,30,40,40,
lst2.pop_front();
cout << "4) "; PrintList( lst2); cout << endl;
//4) 20,30,30,30,40,40,
lst1.remove(2); //删除所有和A(2)相等的元素
cout << "5) "; PrintList( lst1); cout << endl;
//5) 1,3,4,
lst2.unique(); //删除所有和前一个元素相等的元素,需有序
cout << "6) "; PrintList( lst2); cout << endl;
//6) 20,30,40,
lst1.merge (lst2); //合并 lst2到lst1并清空lst2
cout << "7) "; PrintList( lst1); cout << endl;
//7) 1,3,4,20,30,40,
cout << "8) "; PrintList( lst2); cout << endl;
//8)
lst1.reverse();
cout << "9) "; PrintList( lst1); cout << endl;
//9) 40,30,20,4,3,1,
lst2.push_back (100);lst2.push_back (200);
lst2.push_back (300);lst2.push_back (400);
list<A>::iterator p1,p2,p3;
p1 = find(lst1.begin(),lst1.end(),3);
p2 = find(lst2.begin(),lst2.end(),200);
p3 = find(lst2.begin(),lst2.end(),400);
lst1.splice(p1,lst2,p2, p3);
//将[p2,p3)插入p1之前,并从lst2中删除[p2,p3)
cout << "10) "; PrintList( lst1); cout << endl;
//10) 40,30,20,4,200,300,3,1,
cout << "11) "; PrintList( lst2); cout << endl;
//11) 100,400,
return 0;
}
1.2 关联容器
元素是排序的,插入任何元素,都按相应的排序规则来确定其位置,便于查找。内部通常以平衡二叉树方式实现,插入和检索的时间都是O(log(N))。
- set/multiset 头文件 < set >。
- set即集合,set中不允许相同元素,multiset中允许存在相同的元素。
- map/multimap头文件 < map >。
- map与set的不同在于map中存放的元素有且仅有两个成员变量,一个名为first,另一个名为second, map根据first值对元素进行从小到大排序,并可快速地根据first来检索元素。
- map同multimap的不同在于是否允许相同first值的元素。
1.3 顺序容器和关联容器的成员函数
顺序容器和关联容器中都有的成员函数:
- begin 返回指向容器中第一个元素的迭代器
- end 返回指向容器中最后一个元素后面的位置的迭代器
- rbegin 返回指向容器中最后一个元素的迭代器
- rend 返回指向容器中第一个元素前面的位置的迭代器
- erase 从容器中删除一个或几个元素
- clear 从容器中删除所有元素
顺序容器的常用成员函数:
- front 返回容器中第一个元素的引用
- back 返回容器中最后一个元素的引用
- push_back 在容器末尾增加新元素
- pop_back 删除容器末尾的元素
- erase 删除迭代器指向的元素(可能会使该迭代器失效),或删除一个区间,返回被删除元素后面的那个元素的迭代器
1.4 容器适配器
- stack 头文件 < stack >
- queue 头文件 < queue >
- priority_queue 头文件 < queue >。优先级队列,最高优先级元素总是第一个出列
2 迭代器
迭代器上可以执行++操作,以使其指向容器中的下一个元素。如果迭代器到达了容器中的最后一个元素的后面,此时再使用它,就会出错,类似于使用NULL或未初始化的指针一样。
- 用于指向顺序容器和关联容器中的元素
- 迭代器用法和指针类似
- 有const和非const两种
- 通过迭代器可以读取它指向的元素
- 通过非const迭代器还能修改其指向的元素
//定义一个普通迭代器
容器类名::iterator 变量名;
//定义一个const迭代器
容器类名::const_iterator 变量名;
//访问一个迭代器指向的元素:
*迭代器变量名
2.1 双向迭代器
若p和p1都是双向迭代器,则可对p、p1可进行以下操作:
- ++p, p++ 使p指向容器中下一个元素
- --p, p-- 使p指向容器中上一个元素
- 取p指向的元素 * p
- p = p1 赋值
- p == p1 , p!= p1 判断是否相等、不等
2.2 随机访问迭代器
若p和p1都是随机访问迭代器,则可对p、p1可进行以下操作:
- 双向迭代器的所有操作
- p += i 将p向后移动i个元素
- p -= i 将p向向前移动i个元素
- p + i 值为: 指向 p 后面的第i个元素的迭代器
- p - i 值为: 指向 p 前面的第i个元素的迭代器
- p – p1 : p1和p之间的元素个数
- p[i] 值为: p后面的第i个元素的引用
- p < p1, p <= p1, p > p1, p>= p1
//随机访问迭代器,下面三种遍历方法均可;而双向迭代器则只能用最后一种
vector<int> v(100);
int i;
for(i = 0; i < v.size(); i++) //根据下标随机访问
cout << v[i];
vector<int>::const_iterator ii;
for( ii = v.begin(); ii < v.end(); ++ii) //根据大小比较符号
cout << * ii;
for( ii = v.begin(); ii != v.end ();++ii) //根据判等符号
cout << * ii;
2.3 容器和算法对迭代器的支持
容器 | vector | deque | list | set/multiset | map/multimap | 容器适配器 |
---|---|---|---|---|---|---|
迭代器类别 | 随机 | 随机 | 双向 | 双向 | 双向 | 不支持迭代器 |
注意:有的算法,例如sort,binary_search需要通过随机访问迭代器来访问容器中的元素,那么list以及关联容器就不支持该算法!
3 算法
- 算法就是一个个函数模板, 大多数在< algorithm > 中定义
- STL中提供能在各种容器中通用的算法,比如查找,排序等
- 算法通过迭代器来操纵容器中的元素。许多算法可以对容器中的一个局部区间进行操作,因此需要两个参数,一个是起始元素的迭代器,一个是终止元素的后面一个元素的迭代器,比如排序和查找。
- 有的算法返回一个迭代器。比如 find() 算法,在容器中查找一个元素,并返回一个指向该元素的迭代器
- 算法可以处理容器,也可以处理普通数组
STL中“大”、“小”、“相等”的概念
STL中“大”、“小”的概念:
- 关联容器内部的元素是从小到大排序的
- 有些算法要求其操作的区间是从小到大排序的,称为“有序区间算法”例:binary_search
- 有些算法会对区间进行从小到大排序,称为“排序算法”,例: sort
- 还有一些其他算法会用到“大”、“小”的概念
- 使用STL时,在缺省的情况下,以下三个说法等价:
- x比y小
- 表达式“x<y”为真
- y比x大
STL中“相等”的概念:
- 有时,“x和y相等”等价于“x==y为真”,如在未排序的区间上进行的算法(顺序查找find等)
- 有时“x和y相等”等价于“x小于y和y小于x同时为假”,如有序区间算法(binary_search,关联容器自身的成员函数find等)
4 函数对象
可参考:
- https://blog.csdn.net/BonChoix/article/details/8050627
- https://www.cnblogs.com/lvpengms/archive/2011/02/21/1960078.html
- https://harttle.land/2015/07/03/stl-function-objects-and-pointers.html
4.1 函数指针
函数指针是指向函数的指针变量,主要有两个作用:用作调用函数和做函数的参数。
- 一般函数的声明为: int func ( int x );
- 一个函数指针的声明方法为:int (* func) (int x);
- 注意(* func)中括号是必要的,这会告诉编译器我们声明的是函数指针而不是声明一个具有返回型为指针的函数;
- 然而这样声明略显繁琐,于是可以先用typedef自定义类型,然后像普通类型一样来使用,如下示例。
//示例:将函数指针作为参数
typedef void (* PFT) ( char ,int );
void bar(char ch, int i){
cout<<"bar "<<ch<<' '<<i<<endl;
return ;
}
void foo(char ch, int i, PFT pf){
pf(ch,i);
return ;
}
PFT pft;
pft = bar;
foo('e', 12, pft);
上述例子我们首先利用一个函数指针pft指向bar(),然后在foo()函数中使用pft指针来调用bar(),实现目的。将这个特点稍加利用,我们就可以构造出强大的程序,只需要同样的foo函数便可以实现对不同bar函数的调用。
4.2 函数对象
一个函数对象,即一个重载了括号操作符"()"的对象。当用该对象调用此操作符时,其表现形式如同普通函数调用一般,因此取名叫函数对象。
从一般的函数回调意义上来说,函数对象和函数指针是相同的,但是函数对象却具有许多函数指针不具有的特点,使程序设计更加灵活和强大。主要有两个优势:
- 函数对象可以有自己的状态。我们可以在类中定义状态变量,这样一个函数对象在多次的调用中可以共享这个状态。但是函数调用没这种优势,除非它使用全局变量来保存状态(但是面向对象编程应尽量避免这种情况);
- 函数对象有自己特有的类型,而普通函数无类型可言。这种特性对于使用C++标准库来说是至关重要的。这样我们在使用STL中的函数时,可以传递相应的类型作为参数来实例化相应的模板,从而实现我们自己定义的规则。比如自定义容器的排序规则。(注:这一条貌似不准确,虽然这一应用上函数对象更有优势,但函数指针也能实现,示例见本文末尾练习题002,可直接将函数指针类型传递给类模板来初始化。)
//示例1:简单案例
class FuncT{
public:
template<typename T>
T operator() (T t1, T t2){
cout << t1 << '+' << t2 << '=' << t1+t2 <<endl;
return t1;
}
};
template <typename T>
T addFuncT(T t1, T t2, FuncT& funct){
funct(t1, t2);
return t1;
}
FuncT funct;
addFuncT(2, 4, funct);
addFuncT(1.4, 2.3, funct);
//示例2:函数对象在STL中accumulate模板上的应用
//STL中accumulate模板
//对[first,last)中的每个迭代器I,执行val= pr(val,*I),返回最终的val
//pr是个函数对象,Pr也可以是个函数。
template<class InIt, class T, class Pred>
T accumulate(InIt first, InIt last, T val, Pred pr);
//Dev C++ 中的 Accumulate 源代码1:
template<typename _InputIterator, typename _Tp> //typename等效于class
_Tp accumulate(_InputIterator __first, _InputIterator __last, _Tp __init){
for ( ; __first != __last; ++__first)
__init = __init + *__first;
return __init;
}
//Dev C++ 中的 Accumulate 源代码2:
template<typename _InputIterator, typename _Tp, typename _BinaryOperation>
_Tp accumulate(_InputIterator __first, _InputIterator __last,
_Tp __init, _BinaryOperation __binary_op){
for ( ; __first != __last; ++__first)
__init = __binary_op(__init, *__first);
return __init;
}
//主函数
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <functional>
using namespace std;
int SumSquares(int total, int value){ return total + value * value; }
template <class T>
void PrintInterval(T first, T last){ //输出区间[first,last)中的元素
for( ; first != last; ++ first)
cout << * first << " ";
cout << endl;
}
template<class T>
class SumPowers{
int power;
public:
SumPowers(int p) : power(p) { }
//计算value的power次方,加到total上
const T operator() (const T & total, const T & value){
T v = value;
for( int i = 0; i < power - 1; ++ i)
v = v * value;
return total + v;
}
};
int main(){
const int SIZE = 3;
int a1[] = { 1,2,3 };
vector<int> v(a1, a1+SIZE);
cout << "1) 原数据:"; PrintInterval(v.begin(), v.end());
int result = accumulate(v.begin(), v.end(), 0, SumSquares);
cout << "2) 平方和:" << result << endl;
result = accumulate(v.begin(), v.end(), 0, SumPowers<int>(3));
cout << "3) 立方和:" << result << endl;
result = accumulate(v.begin(), v.end(), 0, SumPowers<int>(4));
cout << "4) 4次方和:" << result;
return 0;
}
对示例2的分析:
- 相比于函数指针SumSquares,函数对象SumPowers< int > 的明显优势是具有内部状态。
- 当编译器遇到 int result = accumulate(v.begin(), v.end(), 0, SumSquares); 语句和 accumulate(v.begin(), v.end(), 0, SumPowers< int >(3)); 语句的时候,分别实例化出下面两个函数。
int accumulate(vector<int>::iterator first, vector<int>::iterator last,
int init, int ( * op)(int, int)){
for ( ; first != last; ++first)
init = op(init, *first);
return init;
}
int accumulate(vector<int>::iterator first, vector<int>::iterator last,
int init, SumPowers<int> op){
for ( ; first != last; ++first)
init = op(init, *first);
return init;
}
应用:< functional > 里还有以下函数对象类模板,均可用来生成函数对象:
- accumulate
- equal_to
- greater
- less
注意:
- 在调用用到函数对象的标准库算法时,除非显式地指定模板类型为“传引用”,否则默认情况下函数对象是”按值传递“的!因此,如果传递的是一个具有内部状态的函数对象,则被改变状态的是函数内部被复制的临时对象,函数结束后随之消失。真正传进来的函数对象状态并为改变。
- 引入函数对象后,STL中的“大”、“小”关系:关联容器和STL中许多算法,都是可以自定义比较器的,在自定义了比较器op的情况下,x小于y等价于op(x,y)为true。
- 自定义排序规则的写法,不能造成比较 a1、a2 返回 true, 比较 a2、a1 也返回 true
- < functional > 里还有以下函数对象类模板,均可用来生成函数对象:accumulate、equal_to、greater、less等。
5 练习
//002:按距离排序 http://cxsjsxmooc.openjudge.cn/2019t3fall8/002/
//要求自定义sort的比较器,即Closer类的函数对象,使得数据按照离指定值的距离大小排序
#include <iostream>
#include <cmath>
#include <algorithm>
#include <string>
using namespace std;
/*************code start here****************/
template <class T1, class T2>
struct Closer {
T1 v;
T2 dis;
Closer(T1 t, T2 op) : v(t), dis(op){ }
bool operator()(const T1& t1, const T1& t2){
int d1 = dis(t1, v);
int d2 = dis(t2, v);
if(d1 < d2)
return true;
else if(d1 > d2)
return false;
else
return t1 < t2;
}
};
/*************code end here****************/
int Distance1(const int n1, const int n2) {
return abs(n1-n2);
}
int Distance2(const string & s1, const string & s2) {
return abs((int)s1.length()- (int)s2.length());
}
int a[10] = { 0,3,1,4,7,9,20,8,10,15};
string b[6] = {"American","Jack","To","Peking","abcdefghijklmnop","123456789"};
int main(){
int n;
string s;
while(cin >> n >> s) {
sort(a, a+10, Closer<int, int(*)(int, int)>(n,Distance1));
for(int i = 0; i < 10; ++i)
cout << a[i] << "," ;
cout << endl;
sort(b, b+6, Closer<string, int(*)(const string&, const string&)>(s, Distance2));
for(int i = 0; i < 6; ++i)
cout << b[i] << "," ;
cout << endl;
}
return 0;
}
//006:我自己的ostream_iterator http://cxsjsxmooc.openjudge.cn/2019t3fall8/006/
//程序填空,使得输出为:
//5,21,14,2,3,[换行]1.4--5.56--3.2--98.3--3.3--
template <class T1,class T2>
void Copy(T1 s, T1 e, T2 x){
for(; s != e; ++s,++x)
*x = *s;
}
/************code start here***************/
template<class T>
class myostream_iterator{
ostream& o;
string s;
T val;
public:
myostream_iterator(ostream& oo, const char* ss) : o(oo), s(ss){ }
T& operator*(){ return val; }
void operator++(){ o << val << s; }
};
/************code end here***************/
int main(){
const int SIZE = 5;
int a[SIZE] = {5,21,14,2,3};
double b[SIZE] = { 1.4, 5.56,3.2,98.3,3.3};
list<int> lst(a,a+SIZE);
myostream_iterator<int> output(cout,",");
Copy( lst.begin(),lst.end(),output);
cout << endl;
myostream_iterator<double> output2(cout,"--");
Copy(b,b+SIZE,output2);
return 0;
}