容器
string
vector
list
deque
map
set
multimap
mutilset
string
专门存放字符的动态的顺序表。
1.string类对象的常见构造
(constructor)函数名称 | 功能说明 |
---|---|
string() | 构造空的string类对象,即空字符串 |
string(const char* s) | 用C-string来构造string类对象 |
string(size_t n, char c) | string类对象中包含n个字符c |
string(const string&s) | 拷贝构造函数 |
2.string类对象的容量操作
函数名称 | 功能说明 |
---|---|
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
empty | 检测字符串释放为空串,是返回true,否则返回false |
clear | 清空有效字符。只清空元素,不清理空间,不改变空间大小 |
reserve | 为字符串预留空间预留空间,只改变容量,不改变有效元素个数。扩大增容,底层容量大小改变。缩小不变 |
resize | 将有效字符的个数该成n个,多出的空间用字符c填充。1. 增大:n<s.capacity()-----直接填充。n>s.capacity()-----string类会自己扩容 2. 减小:直接改变size值 3. c不赋值默认为0 |
3.访问及遍历
- perator[ ]:返回pos位置的字符,const string类对象调用
- begin+end:begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器。左闭右开
- rbegin+rend:begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器
4.修改操作
- operator+=:在字符串后追加字符串str
- c_str:返回C格式字符串
- find+npos:从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
5.非成员函数
- operator>>:输入运算符重载
- operator<< :输出运算符重载
- getline:获取一行字符串
- relational operators:大小比较
6.浅拷贝
概念:编译器只是将对象中的值拷贝过来值的拷贝
- 如果对象中管理资源,就会导致多个对象共享同一份资源,当一个对象销毁时会将该资源释放掉,此时另一些对象不知道该资源已经被释放,所以当继续对资源进项操作时,就会发生访问违规。
- 会引起一块资源销毁多次
- 会导致内存泄漏
7.深拷贝
概念:给每个对象独立分配资源,保证多个对象之间不会因共享资源而造成多次释放程序崩溃问题
8.写时拷贝
浅拷贝+引用计数+在修改一个对象时分离对象。线程安全问题。
vector
存放任意类型的动态顺序表。
1.概念
表示可变大小数组的序列容器,连续的存储空间来存储元素
2.构造
- 空构造函数
- 区间构造
- n个值data元素的构造
- 拷贝构造
3.vector (Input Iterator first, Input Iterator last);
:使用迭代器进行初始化构造
4.容量操作
- size:获取数据个数
- capacity:获取容量大小
- empty:判断是否为空
- resize:
resize(newSize,data)
: 将vector中有效元素个数更新到newSize,假设vector中有效元素个数为oldSize。- newSize>oldSize
- newSize<=capacity:增多的元素使用data来进行填充
- newSize>capacity:
- 开辟新空间
- 拷贝元素
- 释放旧空间
- newSize<=oldSize
- 直接改变oldSize,不会缩小底层空间大小
- newSize>oldSize
- data:
- 内置类型:默认为0
- 自定义类型:调用无参的构造函数
- reserve:
reserve(newCapacity)
:为vector预留空间,不会改变有效元素个数。- newCapacity<=oldCapacity:返回
- newCapacity>oldCapacity:真正的进行扩容
5.增删改查
- push_back:尾插
- pop_back:尾删
- find:查找
- insert:在position之前插入val
- erase:删除position位置的数据
- clear:清空
- swap:交换两个vector的数据空间
- operator[]
T&operator[](size_t index)
const T& operatr[](size_t index) const
- 头部元素:
T& front
const T&front() const
- 尾部元素:
T& back()
const T&back() const
- ++/–:顺序往后移动
6.迭代器
- 旧空间释放,重新赋值指向新空间即可:erase返回删除位置的下一个位置
- 主要是提供给算法来使用,让算法可以透明化(不用关心底层的数据结构)的操作数据
- begin+end
- 获取第一个数据位置的iterator/const_iterator, 获取最后一个数据的下一个位置的iterator/const_iterator。
- 左闭右开
- 从begin–>end移动
- rbegin+rend
- 从rend–>rbegin移动
- begin+end
- 迭代器失效
- typedef T* iterator
auto it=v.begin();v.push_back(data);
:it实际已经指向vector底层空间的起始位置,通过it迭代器访问vector中的元素,代码会崩溃
- 底层空间改变
- push_back
- insert resize
- reserve
- swap
- assign
- erase(pos):pos迭代器失效
- 解决方式:给迭代器重新赋值
- typedef T* iterator
7.list与vector的对比
本质:顺序表和链表区别。
方面 | vector | list |
---|---|---|
底层 | 是一段连续空间 | 是链式结构(带头结点的双向循环链表) |
元素访问 | 支持随机访问 O(1) | 不支持随机访问,访问任意位置的元素时必须要遍历 O(N) |
插入、删除 | 任意位置插入或者删除元素,需要搬移大量的元素,效率比较低 O(N) | 任意位置插入、删除元素,只需要改变指针的指向,效率比较高 O(1) |
扩容 | 在插入时,可能需要扩容:1. 申请新空间 2. 拷贝元素 3. 释放旧空间。因此,在插入时,如果没有提取将容量给出,效率会更低 | 不需要扩容 |
使用场景 | 高效存储+频繁的访问的情况 | 在任意位置插入、删除操作比较多的情况 |
迭代器 | 原生态指针,原生态的指针可以去遍历空间中的每个元素 | 对原生态指针进行封装,可以将该迭代器按照指针的方式进行使用。达到方便遍历的目的 |
迭代器失效 | 只要底层空间发生改变,所有的迭代器都会失效(push_back/insert/resize/reserve)插入、删除时,不重新赋值会导致失效 | 插入不会导致失效,删除只会影响当前迭代器 |
空间利用率 | 底层为连续空间,不容易造成内存碎片,空间利用率高,缓存利用率高 | 底层节点动态开辟,小节点容易造成内存碎片,空间利用率低,缓存利用率低 |
接口不同 | 支持随机访问(operator[]),与容量相关的接口 | 有特殊的接口:merage、sort… |
针对整型定义了一个 vector,插入 6 个元素,然后打印所有元素:
#include <iostream>
#include <vector>
using namespace std;
int main(int argc, char* argv[])
{
vector<int> vecTemp;
for (int i = 0; i<6; i++)
vecTemp.push_back(i);
for (int i = 0; i<vecTemp.size(); i++)
cout << vecTemp[i] <<" "; // 输出:0 1 2 3 4 5
return 0;
}
list
存放任意类型的数据。带头节点的双向循环链表
- capacity相关
- 头删
- 头插
- 尾删
- 尾插
- access相关
- modify相关
- 迭代器
- 让算法对容器透明化(让算法对于任意的容器都可以操作)
- 不能给成原生态指针
- 如果给成原生态指针,没有办法++让迭代器往后移动到下一个节点的位置。需要对原生态的指针进行封装
- 管理原生态指针 _pNode
- 构造、拷贝构造
- 让迭代器具有指针一样的操作
operator*() /operator ->()
正向迭代器重新封装。 - 支持移动操作
- operator++() /operator++(int)
- operator–() /operator–(int)
- 与正向迭代器相反
- 比较操作
operator!=() /operator==()
与正向迭代器相同
- 分类
vector/string
typedef T* iterator;
底层是一段连续空间 - ++/–:重载 operator++/–
typedef listlterator <> iterator
产生一个空 list,准备放置字符,然后将 'a' 至 'z' 的所有字符插入其中,利用循环每次打印并移除集合的第一个元素,从而打印出所有元素:
#include <iostream>
#include <list>
using namespace std;
int main(int argc, char* argv[])
{
list<char> listTemp;
for (char c = 'a'; c <= 'z'; ++c)
listTemp.push_back(c);
while (!listTemp.empty())
{
cout <<listTemp.front() << " ";
listTemp.pop_front();
}
return 0;
}
优缺点:
优点:
- 不使用连续内存完成动态操作。
- 在内部方便的进行插入和删除操作
- 可在两端进行push、pop
缺点:
- 不能进行内部的随机访问,即不支持[ ]操作符和vector.at()
- 相对于verctor占用内存多
deque
是由一段一段的定量连续空间构成。
一旦要在 deque 的前端和尾端增加新空间,便配置一段定量连续空间,串在整个 deque 的头端或尾端。因此不论在尾部或头部安插元素都十分迅速。 在中间部分安插元素则比较费时,因为必须移动其它元素。deque 的最大任务就是在这些分段的连续空间上,维护其整体连续的假象,并提供随机存取的接口。
优点:
- 支持随机访问,即 [] 操作和 .at(),查询效率高;
- 可在双端进行 pop,push。
- 在内部方便的进行插入和删除操作
缺点:
- 不适合中间插入删除操作;
- 占用内存多。
适用场景:适用于既要频繁随机存取,又要关心两端数据的插入与删除的场景。
声明了一个浮点类型的 deque,并在容器尾部插入 6 个元素,最后打印出所有元素。
#include <iostream>
#include <deque>
using namespace std;
int main(int argc, char* argv[])
{
deque<float> dequeTemp;
for (int i = 0; i<6; i++)
dequeTemp.push_back(i);
for (int i = 0; i<dequeTemp.size(); i++)
cout << dequeTemp[i] << " "; // 输出:0 1 2 3 4 5
return 0;
}
vector VS. list VS. deque:
a、若需要随机访问操作,则选择vector;
b、若已经知道需要存储元素的数目,则选择vector;
c、若需要随机插入/删除(不仅仅在两端),则选择list
d、只有需要在首端进行插入/删除操作的时候,还要兼顾随机访问效率,才选择deque,否则都选择vector。
e、若既需要随机插入/删除,又需要随机访问,则需要在vector与list间做个折中-deque。
f、当要存储的是大型负责类对象时,list要优于vector;当然这时候也可以用vector来存储指向对象的指针,同样会取得较高的效率,但是指针的维护非常容易出错,因此不推荐使用。
map
map 由红黑树实现,其元素都是 “键值/实值” 所形成的一个对组(key/value pairs)。每个元素有一个键,是排序准则的基础。每一个键只能出现一次,不允许重复。
map 主要用于资料一对一映射的情况,map 内部自建一颗红黑树,这颗树具有对数据自动排序的功能,所以在 map 内部所有的数据都是有序的。比如一个班级中,每个学生的学号跟他的姓名就存在着一对一映射的关系。
特点:
自动建立 Key - value 的对应。key 和 value 可以是任意你需要的类型。
根据 key 值快速查找记录,查找的复杂度基本是 O(logN),如果有 1000 个记录,二分查找最多查找 10次(1024)。
增加和删除节点对迭代器的影响很小,除了那个操作节点,对其他的节点都没有什么影响。
对于迭代器来说,可以修改实值,而不能修改 key。
- 优点:使用平衡二叉树实现,便于元素查找,且能把一个值映射成另一个值,可以创建字典。
- 缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
- 适用场景:适用于需要存储一个数据字典,并要求方便地根据key找value的场景。
#include "stdafx.h"
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main(int argc, char* argv[])
{
map<int, string> mapTemp;
mapTemp.insert({ 5,"张三" });
mapTemp.insert({ 3, "李四"});
mapTemp.insert({ 4, "隔壁老王" });
map<int, string>::iterator it;
for (it = mapTemp.begin(); it != mapTemp.end(); it++)
{
printf("学号:%d 姓名:%s\n", (*it).first, (*it).second.c_str());
}
return 0;
}
/*
输出结果:
学号:3 姓名:李四
学号:4 姓名:隔壁老王
学号:5 姓名:张三
*/
set
set(集合)由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复。
set 中的元素都是排好序的,集合中没有重复的元素;
map 和 set 的插入删除效率比用其他序列容器高,因为对于关联容器来说,不需要做内存拷贝和内存移动。
- 优点:使用平衡二叉树实现,便于元素查找,且保持了元素的唯一性,以及能自动排序。
- 缺点:每次插入值的时候,都需要调整红黑树,效率有一定影响。
- 适用场景:适用于经常查找一个元素是否在某群集中且需要排序的场景。
下面子演示 set(集合)的两个特点:
#include <iostream>
#include <set>
using namespace std;
int main(int argc, char* argv[])
{
set<int> setTemp;
setTemp.insert(3);
setTemp.insert(1);
setTemp.insert(2);
setTemp.insert(1);
set<int>::iterator it;
for (it = setTemp.begin(); it != setTemp.end(); it++)
{
cout << *it << " ";
}
return 0;
}
输出结果:1 2 3。一共插入了 4 个数,但是集合中只有 3 个数并且是有序的。可见之前说过的 set 集合的两个特点,有序和不重复。
当 set 集合中的元素为结构体时,该结构体必须实现运算符 ‘<’ 的重载:
#include <iostream>
#include <set>
#include <string>
using namespace std;
struct People
{
string name;
int age;
bool operator <(const People p) const
{
return age < p.age;
}
};
int main(int argc, char* argv[])
{
set<People> setTemp;
setTemp.insert({"张三",14});
setTemp.insert({ "李四", 16 });
setTemp.insert({ "隔壁老王", 10 });
set<People>::iterator it;
for (it = setTemp.begin(); it != setTemp.end(); it++)
{
printf("姓名:%s 年龄:%d\n", (*it).name.c_str(), (*it).age);
}
return 0;
}
/*
输出结果
姓名:王二麻子 年龄:10
姓名:张三 年龄:14
姓名:李四 年龄:16
*/
可以看到结果是按照年龄由小到大的顺序排列。另外 string 要使用c_str()
转换一下,否则打印出的是乱码。
如何设置迭代器
- 应该根据该数据结构的特性,实现一个迭代器类
- 在该类中给迭代器类型取别名:iterator
- 使用iterator将不同容器迭代器类型统一,可以降低用户使用成本
- begin()/end()
- 左闭右开
- 正向迭代器
- ++:从begin–>end
- –:从end–>begin
- 反向迭代器
- ++:从end–>begin
- –:从begin–>end
各容器的特点总结
vector 头部与中间插入和删除效率较低,在尾部插入和删除效率高,支持随机访问。
deque 是在头部和尾部插入和删除效率较高,支持随机访问,但效率没有 vector 高。
list 在任意位置的插入和删除效率都较高,但不支持随机访问。
set 由红黑树实现,其内部元素依据其值自动排序,每个元素值只能出现一次,不允许重复,且插入和删除效率比用其他序列容器高。
map 可以自动建立 Key - value 的对应,key 和 value 可以是任意你需要的类型,根据 key 快速查找记录。
在实际使用过程中,到底选择这几种容器中的哪一个,应该根据遵循以下原则:
1、如果需要高效的随机存取,不在乎插入和删除的效率,使用 vector。
2、如果需要大量的插入和删除元素,不关心随机存取的效率,使用 list。
3、如果需要随机存取,并且关心两端数据的插入和删除效率,使用 deque。
4、如果打算存储数据字典,并且要求方便地根据 key 找到 value,一对一的情况使用 map,一对多的情况使用 multimap。
5、如果打算查找一个元素是否存在于某集合中,唯一存在的情况使用 set,不唯一存在的情况使用 multiset。
各容器的时间复杂度分析
vector 在头部和中间位置插入和删除的时间复杂度为 O(N),在尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
deque 在中间位置插入和删除的时间复杂度为 O(N),在头部和尾部插入和删除的时间复杂度为 O(1),查找的时间复杂度为 O(1);
list 在任意位置插入和删除的时间复杂度都为 O(1),查找的时间复杂度为 O(N);
set 和 map 都是通过红黑树实现,因此插入、删除和查找操作的时间复杂度都是 O(log N)。
各容器的共性
各容器一般来说都有下列函数:默认构造函数、复制构造函数、析构函数、empty()、max_size()、size()、operator=、operator<、operator<=、operator>、operator>=、operator==、operator!=、swap()。
顺序容器和关联容器都共有下列函数:
begin() :返回容器第一个元素的迭代器指针;
end():返回容器最后一个元素后面一位的迭代器指针;
rbegin():返回一个逆向迭代器指针,指向容器最后一个元素;
rend():返回一个逆向迭代器指针,指向容器首个元素前面一位;
clear():删除容器中的所有的元素;
erase(it):删除迭代器指针it处元素。