读者可能有这样的经历,自己编写了动态数组类、链表类、集合类和映射类等程序,然后小心地维护着。其实 STL 提供了专家级的几乎我们所需要的各种容器,功能更好,效率更高,复用性更强,所以开发应用系统应该首选 STL 容器类,摒弃自己的容器类,尽管它可能花费了你很多的开发时间。
本章将介绍 STL 中的通用容器
包括 vector、deque、list、queue和stack、priority_queue、bitset、set和multiset、map和multimap等等。
概述
容器分类
(1)序列性容器
按照线性排列来存储某类型值的集合,每个元素都有自己特定的位置,顺序容器主要有 vector、deque、list。
vector:动态数组。它在堆中分配内存,元素连续存放,有保留内存,分配的内存大小只增加不减小,当它满时再添加元素会重新分配一块更大的内存,此时需要移动内存,如果你的元素是结构体或者是类,那么移动的同时还会进行构造和析构操作。对最后元素操作最快(在后面添加删除最快),此时一般不需要移动内存。
deque:与 vector 类似,支持随机访问和快速插入删除,它在容器某一位置上的操作花费的是线性时间。与 vector 不同的是,deque 支持从开始端插入、删除元素。由于它主要对前端、后端进行操作,因此也叫做双端队列。
list:又叫链表,只能顺序访问(从前往后或者从后往前),与前面两种容器有一个明显的区别就是它不支持随机访问。访问链表某一个元素需要从表头或表尾开始循环,直到找到该元素。
(2)关联式容器
与顺序性容器相比,关联式容器更注重快速和高效地检索数据的能力。这些容器是根据键值(key)来检索数据的,键可以是值也可以是容器中的某一成员。这一类中的成员在初始化后都是按一定顺序排好序的。关联式容器主要有 set、multiset、map、multimap。
set:集合,快速查找,不允许重复值。
multiset:集合,快速查找,允许重复值。
map:一对一映射,基于关键字快速查找,不允许重复值。
multimap:一对多映射,基于关键字快速查找,允许重复值。
(3)容器适配器
对已有的容器进行某些特性的再封装,不是一个真正的新容器,主要有 stack、queue。
stack:栈类,特点是后进先出。
queue:队列类,特点是先进先出。
容器共性
容器一般来说都有下列函数:
默认构造函数: 容器默认初始化。
拷贝构造函数: 容器初始化为同类容器副本。
析构函数: 容器关闭,资源释放。
empty(): 判断容器是否为空。
max_size(): 返回容器中最大元素个数。
size(): 返回容器中当前元素个数。
operator = () : 将一个容器赋给另一个容器。
operator < () : 容器比较。
operator <= ():
operator > ():
operator >= ():
operator == ():
operator != ():
swap(): 交换两个容器的元素
begin(): 返回第一个元素迭代器指针。
end(): 返回最后一个元素的后一位的迭代器指针。
rbegin(): 返回最后一个元素迭代器指针。
rend(): 返回第一个元素的前一位的迭代器指针。
erase(): 从容器中清除一个或几个元素。
clear(): 清除容器中所有元素。
vector 容器
使用 vector 容器时需要头文件 。
(1)构造函数
vector(); //创建一个空的 vector
vrctor(int nSize); //创建一个 vector,元素个数为 nSize
vrctor(int nSize const T& t); //创建一个 vector,元素个数为 nSize,且值均为 T
vector(const vector&); //拷贝构造函数
(2)添加函数
void push_back(const T& x); //在尾部添加元素 x
iterator insert(iterator it,const T& x); //在某一元素前添加元素 x
void insert(iterator it,int n,const T& x); //在某一元素前添加 n 个相同元素 x
void insert(iterator it,const_iterator first,const_iterator last); //在某一元素前插入另一个相同类型的[first,last)间的元素
(3)删除函数
iterator erase(iterator it); //删除某一元素
iterator erase(iterator first,iterator last); //删除[first,last)之间的元素
void pop_back(); //删除最后一个元素
void clear(); //删除所有元素
(4)遍历函数
reference at(int pos); //返回 pos 位置元素的引用
reference front(); //返回首元素的引用
reference back(); //返回尾元素的引用
iterator begin(); //返回首元素指针
iterator end(); //返回尾元素的后一个位置的指针
reverse_iterator rbegin(); //反向迭代器,返回尾元素迭代指针
reverse_iterator rend(); //反向迭代器,返回首元素的前一个位置的迭代指针
(5)判断函数
bool empty() const; //判断 vector 是否为空,为空返回 true
(6)大小函数
int size() const; //返回向量中元素个数
int capacity() const; //返回 vector 当前所能容纳元素数量的最大值
int max_size() const; //返回最大可允许的 vector 元素数量值
(7)其他函数
void swap(vector&); //交换两个同类型 vector 的元素
void assign(int n,const T& x); //设置容器大小为 n,每个元素为 x
void assign(const_iterator first,const_iterator last); //将容器中[first,last)间的元素设置成当前元素
vector 使用代码如下:
#include
#include
using namespace std;
//使用迭代器正序输出 vector 元素
void print(vector v,char s[])
{
vector::iterator it;
cout< v1; //默认构造函数,创建一个空的 vector
vector v2(2); //创建一个大小为2的 vector
vector v3(3,0); //穿件一个 vector,并填充3个0
vector v4(v3); //拷贝构造函数,创建v3的副本v4
v1.push_back(1); //在尾部添加1,此时v1:1
v1.insert(v1.end(),2); //在尾部添加2,此时v1:12
v1.insert(v1.begin(),2,0); //在首部添加2个0,此时v1:0012
v1.insert(v1.begin(),v1.begin(),v1.end()); //在首部添加v1所有内容,此时v1:00120012
print(v1,"v1");
cout<::reverse_iterator rt;
cout<<"v3倒序:";
for(rt=v3.rbegin();rt!=v3.rend();rt++) //使用反向迭代器逆序输出,输出210
{
cout<<*rt;
}
cout<::iterator it1=v3.begin();
vector::iterator it2=v3.end();
v4.assign(it1,it2); //将v3的内容复制给v4,此时v3:11111,v4:11111
print(v4,"v4");
return 0;
}
输出:
v1:00120012
v2 size 大小:4 v2 capacity 大小:4
v2 size 大小:5 v2 capacity 大小:6
v2 max_size 大小:1073741823
v2不为空!
v2:2
v3:012
v3倒序:210
v4:11111
v4:012
v4:11111
值得注意的是,v2初始大小设置为2,再添加两个元素"1"、"2"后,capacity 大小为4,再添加"3"后,capacity 大小为6,说明其大小动态在增加。另外,使用 max_size() 得知 v2 最多能有 1073741823 个元素,元素为 int 型,1073741823*4 个字节约为 4G(我的电脑内存为4G),是不是有点巧合呢?读者可以自行去深究。
deque 容器
使用与 vector 类似,多了如下操作:
(2)添加函数
void push_front(const T& x) //在首部添加元素 x
(3)删除函数
void pop_front(); //删除首元素
大部分功能与 vector 类似,因此不做代码演示了。主要记住 deque 是个双端队列,从在头尾进行操作即可。
list 容器
使用与 deque 类似,多了如下操作:
(3)删除函数
void remove(const T& x); //删除 list 中所有元素值等于 x 的元素
(7)其他函数
void sort(); //容器内所有元素排序,默认是升序
templatevoid sort(Pred pr); //容器内所有元素根据预定函数 pr 排序
void unique(); //list 内相邻元素若有重复的,则仅保留一个
void splice(iterator it,list& x); //队列合并函数,队列 x 所有元素插入迭代器指针 it 前,x 变成空队列
void splice(iterator it,list& x,iterator first,iterator last); //x 中移走[first,last)间的元素插入至 it 前
void reverse(); //反转容器中元素顺序
list 相关函数使用代码如下:
#include
#include
#include
using namespace std;
typedef class student
{
private:
string name;
int number;
public:
//构造函数
student(string name,int number)
{
this->name=name;
this->number=number;
}
//重载全局 operator <<
friend ostream& operator<<(ostream& out,student& s)
{
cout<number == s.number;
}
//重载 operator <
bool operator < (const student &s)
{
return this->number < s.number;
}
}S;
void print(list &l,string s)
{
cout<::iterator it;
for(it=l.begin();it!=l.end();it++)
{
cout<<*it< list1;
list1.push_back(S("乾隆",4));
list1.push_back(S("康熙",3));
list1.push_back(S("秦始皇",2));
list1.push_back(S("秦始皇",2));
//此时list1:乾隆4 康熙3 秦始皇2 秦始皇2
list list2;
list2.push_back(S("雍正",1));
list2.push_back(S("武则天",5));
list2.push_back(S("康熙",3));
//此时list2:雍正1 武则天5 康熙3
list1.swap(list2);
//此时list1:雍正1 武则天5 康熙3
//此时list2:乾隆4 康熙3 秦始皇2 秦始皇2
list1.splice(list1.end(),list2,list2.begin(),list2.end());
//此时list1:雍正1 武则天5 康熙3 乾隆4 康熙3 秦始皇2 秦始皇2
//此时list2:空
list1.sort();
//此时list1:雍正1 秦始皇2 秦始皇2 康熙3 康熙3 乾隆4 则天5
//此时list2:空
list1.unique();
//此时list1:雍正1 秦始皇2 康熙3 乾隆4 则天5
//此时list2:空
list1.reverse();
//此时list1:则天5 乾隆4 康熙3 秦始皇2 雍正1
//此时list2:空
print(list1,"list1:");
print(list2,"list2:");
return 0;
}
做几点说明:
1.编写 list
的 print() 函数时,想用 cout<<*it 必须先重载全局函数 ostream& operator<< ,具体见第2章。
2.使用容器的 sort() 函数时,若元素不是基本类型,必须先重载 operator < 函数以供 sort() 函数比较。
3.使用容器的 unique() 函数时,若元素不是基本类型,必须先重载 operator == 函数以供 unique() 函数比较。
queue和stack
队列和栈是常用而且重要的数据结构。队列先进先出,栈后进先出,STL 中将基本容器再次封装,实现这两个数据结构。
队列独有函数:
queue>; //构造函数,创建元素类型为T的空队列,默认容器是 deque
T& front(); //当队列非空情况下,返回队头元素引用
T& back(); //当队列非空情况下,返回队尾元素引用
栈独有函数:
stack>; //构造函数,创建元素类型为T的空栈,默认容器是 deque
T& top(); //当栈非空情况下,返回栈顶元素的引用
队列和栈共有函数:
bool empty(); //队列(栈)为空返回 true,否则返回 false
int size(); //返回队列(栈)中元素数值
void push(const T& t); //把 t 元素压入队尾(栈顶)
void pop(); //当队列(栈)非空时,返回队头(栈顶)元素
使用 queue 需要头文件,使用 stack 需要头文件,比如队列使用如下:
queue> q;
容器适配器
为了清除容器适配器的概念,先看一段 STL 中 stack 的源代码:
template >
class stack
{
protected:
_Container c; // the underlying container
public:
......
stack(): c()
{ // 使用容器类的默认构造函数
}
explicit stack(const _Container& _Cont): c(_Cont)
{ //使用容器类的拷贝构造函数
}
void push(value_type&& _Val)
{
c.push_back(_STD move(_Val));
}
size_type size() const
{
return (c.size());
}
......
};
可以发现构造 stack 时传入的容器对象即为 stack 类的成员函数 _Container c,栈的各元素都存储于 c 中。查看 push() 函数得知, stack 的 push() 实际上是调用了 c.push__back(),也就是说,stack 实际上只是调用了传入的容器对象的原有函数而已,它的各种操作函数只是起到一个适配器的作用,几乎没有自己独有的功能。广而言之,stack 类是对基础容器类的再封装,不是重新定义,queue 类也很相似。注意!由于 queue 类有 T& front() 函数,因此要求封装的容器必须有 pop_front()函数,因此 vector 不能被封装为 queue。
priority_queue
优先队列即 priority_queue 类,带优先权的队列,根据优先权决定出队顺序,默认的序列容器是 vector。
构造函数
//创建元素类型为 T 的空优先队列,Pred 是二员比较函数,默认是 less。
priority_queue(const Pred& pr=Pred(),const allocator_type& al=allocator_type());
//以迭代器[first,last)指向元素,创建元素类型为 T 的优先队列,Pred 是二员比较函数,默认是 less,可以传 greater。
priority_queue(const value_type* first,const value_type* last,const Pred& pr=Pred(),const allocator_type& al=allocator_type());
操作函数同 queue 类似。
priority_queue 使用代码如下:
#include
#include
#include
#include
using namespace std;
class student
{
public:
int number; //学号
string name; //姓名
int math; //数学成绩
int chinese; //语文成绩
public:
student(int number,const string &name,int math,int chinese)
{
this->number=number;
this->name=name;
this->math=math;
this->chinese=chinese;
}
friend ostream& operator << (ostream& out,const student& s)
{
cout<<"学号:"<number < s.number;
if(this->math < s.math) return true;
if(this->math == s.math && this->chinese < s.chinese) return true;
if(this->math == s.math && this->chinese == s.chinese && this->number < s.number) return true;
return false;
}
};
int main()
{
priority_queue,less > p;
p.push(student(3,"李世民",60,70));
p.push(student(1,"秦始皇",70,90));
p.push(student(2,"康熙",70,50));
while(!p.empty())
{
cout<
输出:
学号:1 姓名:秦始皇 数学: 70 语文:90
学号:2 姓名:康熙 数学: 70 语文:50
学号:3 姓名:李世民 数学: 60 语文:70
上述程序中有一个 student 类,主函数以 vector 为内置容器建立了一个优先队列,并使用 less 作为比较函数,为了使用该函数,必须重载 student 类的 operator< 操作符。程序中先比较数学成绩,相同再比较语文成绩,相同再比较学号,权值小的先输出。从输出中可以看到,"李世民"的数学最低,因此权值最小,所以最后才输出。如果想先输出权值大的该怎么办呢?很简单,只需要再建立优先队列时使用 gteater 参数,即 priority_queue,greater > p,并重载 student 类的 operator> 操作符即可。
bitset 容器
C 是一种"接近硬件"的语言,但 C 语言并没有固定的二进制表示法。bitset 可以看做是二进制位的容器,并提供了位的相关操作数。
bitset常用函数如下所示:
(1)构造函数
bitset(); //默认构造函数,Bits 为位最大长度
bitset(const bitset&); //拷贝构造函数
bitset(unsigned long val); //由无符号长整型数构建位容器
bitset(const string& str,size_t pos,size_t n=-1); //由字符串创建位容器,pos 为 str 起始位置,n 为根据 str 构建字符个数
bitset& operator= (const bitset&); //赋值操作
(2)逻辑运算操作
bitset& operator &= (const bitset&); //返回两个位容器 & 运算后的引用,并修改第一个位容器值
bitset& operator |= (const bitset&);
bitset& operator ^= (const bitset&);
bitset& operator << = (size_t n); //返回位容器左移 n 位后的引用,并修改第一个位容器值
bitset& operator >> = (size_t n);
bitset operator << (size_t n) const; //返回位容器左移 n 位后的备份
bitset operator >> (size_t n) const;
bitset operator & (const bitset&,const bitset&); //返回两个位容器 & 运算后的备份
bitset operator | (const bitset&,const bitset&);
bitset operator ^ (const bitset&,const bitset&);
(3)其他操作函数
string to_string(); //将位容器内容转换成字符串
size_t size() const; //返回位容器大小
size_t count() const; //返回设置成 1 的位个数
bool any() const; //是否有位设置 1
bool none() const; //是否没有为设置 1
bool test(size_t n)const; //测试某位是否为 1
bool operator[](size_t n)const; //随机访问位元素
unsigned long to_ulong() const; //若没有溢出异常,返回无符号长整型数
bitset& set(); //位容器所有位置 1
bitset& flip(); //位容器所有位翻转
bitset& reset(); //位容器所有位置 0
bitset& set(size_t n,int val=1); //设置某位为 1 或 0,默认为 1
bitset& reset(size_t n); //复位某位为 0
bitset& flip(size_t n); //翻转某位
bitset 容器的使用比较简单,这里直接给出一个它的简易应用。已知有 n 个整形数组,长度都是 10,元素都在[1,20]之间,且均递增排列,无重复数据。试利用 bitset 压缩这些数组,并存入文件中。
分析:一个数组大小=4*10=40字节,而每一位数字其实都能用一个20位大小的 bitset 容器存储,数字是几位容器的第几位就是几。如一个数n=5,则存储为 00000000000000010000。20位相当于2.5字节,与原先的40字节相比,压缩成了原来的1/16。但实际上文件操作的最小单位是字节,无法读写2.5字节,因此位容器需要选择24位大小,这样读写操作正好是3字节了。
使用 bitset 压缩数组代码如下:
#include
#include
#include
using namespace std;
template
class Mynum
{
public:
bitset b;
public:
void set(int array[],int nSize)
{
b.reset();
for(int i=0;i m;
for(int i=0;i<4;i++)
{
m.set(a[i],(sizeof(a[i])/sizeof(int)));
out.write((char*)&(m.b),3); //将bitset写入文件
}
out.close();
ifstream in("test.txt");
bitset<24> b;
if(!in) return 0;
else
{
for(int k=0;k<4;k++)
{
in.read((char*)(&b),3); //从文件中读出并存储到bitset中
for(int i=0;i<24;i++)
{
if(b.test(i))
{
cout<
输出:
1 2 3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20
2 4 6 8 10 12 14 16 18 20
1 3 5 7 9 11 13 15 17 19
实际中应该把 main 函数中的写入读出功能封装至 Mynum 类中,这里为了简便就这么写了。可以在程序目录下找到"test.txt",可以看到其大小为12字节,而正常存储40个int应该为160字节。
set和multiset
multiset 与 set 的区别就是 multiset 允许重复元素存在,两者都是有序集合。
大部分函数与之前介绍的容器的函数类似,这里就介绍下独有的函数。
首先需要介绍 pair 数据结构,定义如下:
template
struct pair : public _Pair_base
{
typedef T first_type;
typedef U second_type;
T first;
U second;
pair();
pair(const T&x,const U& y);
template
pair(const pair& pr);
}
在父结构 _Pair_base 中定义了 T first;U second; 简单地说 pair 是有着两个动态类型成员变量的数据结构。
(1)插入函数:
pairinsert(const value_type& x); //插入成功时,pair.first 为插入元素的位置迭代器,pair.second 为 true,插入成功时,pair.first 为与插入元素重复元素的位置迭代器,pair.second 为 false
(2)操作函数
const_iterator lower_bound(const Key& key); //返回容器元素大于等于 key 的迭代指针,否则返回 end()
const_iterator upper_bound(const Key& key); //返回容器元素大于 key 的迭代指针,否则返回 end()
pairequal_range(const Key& key) const; //返回容器中包含值等于 key 元素的最小范围[first,last)
可以发现,equal_range() 返回的 pair 的first 即是 lower_bound(),其 second 即是 upper_bound()。
map和multimap
在之前的容器中,仅保存着一样东西,但是在 map和multimap 中将会得到两样东西,关键字 Key 和以关键字查询得到的结果值 Value,即一对值,map 但映射中,Key 和 Value 是一对一的关系,而在 multimap 中,Key 和 Value 可以是一对多的关系。map和multimap 的函数使用基本与 set 类似,值得讲的是,map和multimap 可以用 [] 运算符来给映射添加键-值对,并返回值得引用。
map和multimap使用代码如下:
#include