C++ STL 基础及应用 容器
Posted 哈士奇超帅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ STL 基础及应用 容器相关的知识,希望对你有一定的参考价值。
读者可能有这样的经历,自己编写了动态数组类、链表类、集合类和映射类等程序,然后小心地维护着。其实 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 容器时需要头文件 <vector>。(1)构造函数
vector<Elem>(); //创建一个空的 vector
vrctor<Elem>(int nSize); //创建一个 vector,元素个数为 nSize
vrctor<Elem>(int nSize const T& t); //创建一个 vector,元素个数为 nSize,且值均为 T
vector<Elem>(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 <iostream> #include <vector> using namespace std; //使用迭代器正序输出 vector 元素 void print(vector<int> v,char s[]) { vector<int>::iterator it; cout<<s<<":"; for(it=v.begin();it!=v.end();it++) { cout<<*it; } cout<<endl; } int main () { vector<int> v1; //默认构造函数,创建一个空的 vector vector<int> v2(2); //创建一个大小为2的 vector vector<int> v3(3,0); //穿件一个 vector,并填充3个0 vector<int> 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<<endl; v2.push_back(1); //在尾部添加1,此时v2:001 v2.push_back(2); //在尾部添加1,此时v2:0012 cout<<"v2 size 大小:"<<v2.size()<<" v2 capacity 大小:"<<v2.capacity()<<endl; v2.push_back(3); //在尾部添加3,此时v2:00123 cout<<"v2 size 大小:"<<v2.size()<<" v2 capacity 大小:"<<v2.capacity()<<endl; cout<<"v2 max_size 大小:"<<v2.max_size()<<endl; v2.erase(v2.begin()); //删除首元素,此时v2:0123 v2.erase(v2.begin(),v2.begin()+2); //删除第1、2个元素,此时v2:23 v2.pop_back(); //删除v2尾元素,此时v2:2 if(!v2.empty()) cout<<"v2不为空!"<<endl; print(v2,"v2"); cout<<endl; v3.clear(); //清空v3,此时v3: v3.push_back(0); //在尾部添加0,此时v3:0 v3.push_back(1); //在尾部添加1,此时v3:01 v3.push_back(2); //在尾部添加2,此时v3:012 cout<<"v3:"<<v3.front()<<v3.at(1)<<v3.back()<<endl; vector<int>::reverse_iterator rt; cout<<"v3倒叙:"; for(rt=v3.rbegin();rt!=v3.rend();rt++) //使用反向迭代器逆序输出,输出210 { cout<<*rt; } cout<<endl<<endl; v4.assign(5,1); //分配v4为5个1,此时v4:11111 print(v4,"v4"); swap(v3,v4); //交换v3、v4元素,此时v3:11111,v4:012 print(v4,"v4"); vector<int>::iterator it1=v3.begin(); vector<int>::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(); //容器内所有元素排序,默认是升序
template<class Pred>void 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 <iostream> #include <string> #include <list> 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<<s.name<<" "<<s.number; return out; } //重载 operator == bool operator == (const student &s) { return this->number == s.number; } //重载 operator < bool operator < (const student &s) { return this->number < s.number; } }S; void print(list<S> &l,string s) { cout<<s<<endl; list<S>::iterator it; for(it=l.begin();it!=l.end();it++) { cout<<*it<<endl; } } int main() { list<student> 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<student> 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<student> 的 print() 函数时,想用 cout<<*it 必须先重载全局函数 ostream& operator<< ,具体见第2章。
2.使用容器的 sort() 函数时,若元素不是基本类型,必须先重载 operator < 函数以供 sort() 函数比较。
3.使用容器的 unique() 函数时,若元素不是基本类型,必须先重载 operator == 函数以供 unique() 函数比较。
queue和stack
队列和栈是常用而且重要的数据结构。队列先进先出,栈后进先出,STL 中将基本容器再次封装,实现这两个数据结构。队列独有函数:
queue<class T,class Container=deque<T>>; //构造函数,创建元素类型为T的空队列,默认容器是 deque
T& front(); //当队列非空情况下,返回队头元素引用
T& back(); //当队列非空情况下,返回队尾元素引用
栈独有函数:
stack<class T,class Container=deque<T>>; //构造函数,创建元素类型为T的空栈,默认容器是 deque
T& top(); //当栈非空情况下,返回栈顶元素的引用
队列和栈共有函数:
bool empty(); //队列(栈)为空返回 true,否则返回 false
int size(); //返回队列(栈)中元素数值
void push(const T& t); //把 t 元素压入队尾(栈顶)
void pop(); //当队列(栈)非空时,返回队头(栈顶)元素
使用 queue 需要头文件<queue>,使用 stack 需要头文件<stack>,比如队列使用如下:
queue<int,list<int>> q;
容器适配器
为了清除容器适配器的概念,先看一段 STL 中 stack 的源代码:template<class _Ty,class _Container = deque<_Ty> > 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<T>。
priority_queue(const Pred& pr=Pred(),const allocator_type& al=allocator_type());
//以迭代器[first,last)指向元素,创建元素类型为 T 的优先队列,Pred 是二员比较函数,默认是 less<T>,可以传 greater<T>。
priority_queue(const value_type* first,const value_type* last,const Pred& pr=Pred(),const allocator_type& al=allocator_type());
操作函数同 queue 类似。
priority_queue 使用代码如下:
#include <iostream> #include <queue> #include <string> #include <vector> 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<<"学号:"<<s.number<<"\t姓名:"<<s.name<<"\t数学:\t"<<s.math<<"\t语文:"<<s.chinese<<endl; return out; } bool operator < (const student& s) const { //先比较数学成绩,相同再比较语文成绩,相同再比较学号 //return this->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<student,vector<student>,less<student> > p; p.push(student(3,"李世民",60,70)); p.push(student(1,"秦始皇",70,90)); p.push(student(2,"康熙",70,50)); while(!p.empty()) { cout<<p.top(); p.pop(); } return 0; }输出:
学号:1 姓名:秦始皇 数学: 70 语文:90
学号:2 姓名:康熙 数学: 70 语文:50
学号:3 姓名:李世民 数学: 60 语文:70
上述程序中有一个 student 类,主函数以 vector 为内置容器建立了一个优先队列,并使用 less<student> 作为比较函数,为了使用该函数,必须重载 student 类的 operator< 操作符。程序中先比较数学成绩,相同再比较语文成绩,相同再比较学号,权值小的先输出。从输出中可以看到,"李世民"的数学最低,因此权值最小,所以最后才输出。如果想先输出权值大的该怎么办呢?很简单,只需要再建立优先队列时使用 gteater<T> 参数,即 priority_queue<student,vector<student>,greater<student> > p,并重载 student 类的 operator> 操作符即可。
bitset 容器
C 是一种"接近硬件"的语言,但 C 语言并没有固定的二进制表示法。bitset 可以看做是二进制位的容器,并提供了位的相关操作数。bitset常用函数如下所示:
(1)构造函数
bitset<int Bits>(); //默认构造函数,Bits 为位最大长度
bitset<int Bites>(const bitset&); //拷贝构造函数
bitset<int Bites>(unsigned long val); //由无符号长整型数构建位容器
bitset<int Bites>(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 <bitset> #include <iostream> #include <fstream> using namespace std; template <size_t N> class Mynum { public: bitset<N> b; public: void set(int array[],int nSize) { b.reset(); for(int i=0;i<nSize;i++) { b.set(array[i]-1,1); } } }; int main() { int a[4][10]={ {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}}; fstream out("test.txt"); Mynum<24> 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<<i+1<<"\t"; } } cout<<endl; } } in.close(); return 0; }输出:
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 <class T,class U> struct pair : public _Pair_base<T, U> { typedef T first_type; typedef U second_type; T first; U second; pair(); pair(const T&x,const U& y); template <class V,class W> pair(const pair<V,W>& pr); }在父结构 _Pair_base 中定义了 T first;U second; 简单地说 pair 是有着两个动态类型成员变量的数据结构。
(1)插入函数:
pair<iterator,bool>insert(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()
pair<const_iterator,cons_iterator>equal_range(const Key& key) const; //返回容器中包含值等于 key 元素的最小范围[first,last)
可以发现,equal_range() 返回的 pair 的first 即是 lower_bound(),其 second 即是 upper_bound()。
map和multimap
在之前的容器中,仅保存着一样东西,但是在 map和multimap 中将会得到两样东西,关键字 Key 和以关键字查询得到的结果值 Value,即一对值<Key,Value>,map 但映射中,Key 和 Value 是一对一的关系,而在 multimap 中,Key 和 Value 可以是一对多的关系。map和multimap 的函数使用基本与 set 类似,值得讲的是,map和multimap 可以用 [] 运算符来给映射添加键-值对,并返回值得引用。map和multimap使用代码如下:
#include <map> #include <string> #include <iostream> using namespace std; int main() { map<string,string> m; m["1-1"]="元旦节"; //使用赋值形式添加映射,键在[]内,值在赋值号右侧 m["5-1"]="劳动节"; pair<string,string> p("8-1","建军节"); //使用键值对通过 insert() 添加 m.insert(p); cout<<m["8-1"]; return 0; }
以上是关于C++ STL 基础及应用 容器的主要内容,如果未能解决你的问题,请参考以下文章