这里写目录标题
- STL定义
- 一、容器
- 概念
- (1)vector
- 如何避免扩容导致效率低
- 为什么是1.5或2扩容
- 怎么找某vector或者list的倒数第二个元素
- vector如何释放空间
- [] 下标检查
- (2)deque
- (3)stack
- (4)queue
- (5)list
- (6)set 、multiset、unordered_set
- (7)map、multimap、unordered_map
- map与unordered_map扩容机制
- map插入方式有哪几种?
- 容器内删除元素
- 二、迭代器
- 容器对应的迭代器
- STL中迭代器失效的情况
- traits
- 三、算法
- 二叉树
- 哈希表
- hashtable中解决哈希冲突方法?
- 常用的遍历算法
- 常用查找算法
- 常用排序算法
- 常用拷贝和替换算法
- 常用算数生成算法
- 常用集合算法
- 四、STL空间配置器
- 五、仿函数
- 六、适配器
STL定义
STL(Standard Template Library),即标准模板库。它是94年被正式纳入C++标准,是 C++ 标准库的重要组成部分。它不仅仅是一个可复用的组件库,而且是一个包含了许多常用的数据结构和算法的软件框架。
STL的第一个比较重要的特点是数据结构和算法的分离。
STL构成
STL提供了六大组件,彼此之间可以组合套用,这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
一、容器
概念
谈到容器的话,其实任何特定的数据结构都是为了实现某种特定的算法,而STL容器就是把运用最广泛的一些数据结构实现出来,那么实现了之后就可以使用由容器里定义的迭代器来为了访问容器中的数据;
从实现角度来看,STL容器是一种class template。
容器分类
根据数据在容器中的排列特性,可以把一些常用的数据结构分为序列式容器和关联式容器两种。
序列式容器强调数据的排序,容器中的每个元素均有固定的位置,常见的序列式容器有vector、deque、list等等;
关联式容器里元素的位置取决于特定的排序准则,和插入顺序无关。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。常见的关联式容器有set、multiset、map、multimap等;
容器 | 特性 |
---|---|
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入和删除元素很慢 |
deque | 双端队列。支持快速随机访问。在头尾位置插入和删除速度很快 |
list | 双向链表。只支持双向顺序访问。在list中任何位置进行插入和删除操作速度都很快 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入和删除操作速度都很快 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素 |
string | 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快 |
容器 | 使用场景 |
---|---|
vector | 比如软件历史操作记录的存储,我们经常要查看历史记录,比如上一次的记录,上上次的记录,但却不会去删除记录,因为记录是事实的描述。 |
deque | 比如排队购票系统,对排队者的存储可以采用deque,支持头端的快速移除,尾端的快速添加。如果采用vector,则头端移除时,会移动大量的数据,速度慢。 |
list | 比如公交车乘客的存储,随时可能有乘客下车,支持频繁的不确实位置元素的移除插入。 |
set | 比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的顺序排列。 |
map | 比如按ID号存储十万个用户,想要快速要通过ID查找对应的用户。二叉树的查找效率,这时就体现出来了。如果是vector容器,最坏的情况下可能要遍历完整个容器才能找到该用户。 |
(1)vector
vector概念
在讲vector之前我想先说一下array这个数据结构,因为vector和与array非常相似,两者的重要差别在于空间的运用的灵活性。
Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,可以,一切工作得由自己来,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。
而vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素,也就是重新配置、元素搬移、释放原空间的过程。因此相比于array来说,vector对于内存的运用更灵活。
vector数据结构
vector所采用的数据结构非常简单:线性连续空间。它用两个迭代器start和finish分别指向连续空间中目前已被使用的范围,并以迭代器end_of_storage指向整块连续空间(含备用空间)的尾端。它有一个容量(capacity)的观念,vector实际配置的大小可能比实际的需求量更大一些,为将来可能的扩充作准备。所以一个 vector 的容量永远大于或等于它实际配置的大小。一旦容量等于大小,就是满载,下次再有新增元素,整个vector就得重新找空间安置。
初始化
//定义具有10个整型元素的向量,不具有初值,其值不确定
vector<int>a(10);
常用函数
#include<vector>
size() //返回返回容器中元素个数
begin() //返回头部迭代器
end() //返回尾部+1迭代器
rbegin() //返回逆首部迭代器
rend()//返回逆尾部-1迭代器
front() //返回首个元素
back() //返回尾部元素
push_back() //在末尾添加一个函数
emplace_back() //和push_back()是一样的作用
pop_back() //弹出最后一个元素
empty() //判断是否为空
insert() //在指定位置插入元素
erase() //在指定位置删除元素
clear() //清空容器
int a[6]={1,2,3,4,5,6};
vector<int>b(a,a+4);
for(int i=0;i<=b.size()-1;++i){cout<<b[i]<<endl;}
int a[6]={1,2,3,4,5,6};
vector<int>b(a,a+4);
for(vector<int>::iterator it=b.begin();it!=b.end();it++){cout<<*it<<" ";}
// 左闭右开
sort(a.begin(),a.end());
reverse(a.begin(),a.end());
copy(a.begin(),a.end(),b.begin()+1); //把a的元素复制到b中
find(a.begin(),a.end(),10); //在a中的元素中查找10,找到返回在向量中的位置
如何避免扩容导致效率低
如果在插入之前,可以预估vector存储元素的个数,提前将底层容量开辟好即可。也就是在插入元素之前进行reserve,只要空间给足,则插入时不会扩容。
vet.reserve(100);
// 先将vector底层空间扩增到100个,然后插入。
为什么是1.5或2扩容
首先vector的扩容原理是申请新空间,拷贝元素,释放旧空间。
理想的分配方案是在第n次扩容时,前面的n-1次释放的空间还能够使用。
如果按照2倍方式扩容:1、2、4、8…,如果8字节还不够继续进行扩容,那之前释放的空间1+2+4=7字节,还是小于需要的至少8字节,所以按2倍方式扩容时,每次扩容前释放的旧空间都不能复用,而且2倍扩容可能也用不了那么多,空间浪费太高。
怎么找某vector或者list的倒数第二个元素
int mySize = vec.size();vec.at(mySize -2);
list不提供随机访问,所以不能用下标直接访问到某个位置的元素,要访问list里的元素只能遍历,不过你要是只需要访问list的最后N个元素的话,可以用反向迭代器来遍历
vector如何释放空间
首先vector的内存空间是只增不减,如果里面有100字节,erase掉99个,留下一个有效元素,但是它的内存占用仍然是100个。它的内存空间是在析构的时候才能被系统回收。empty()用来检测容器是否为空的,clear()可以清空所有元素,但不会真正释放内存。也就是size()会清零,但capacity()不会改变。
可以用swap释放内存,swap的用法有以下两种:
1.释放vector中多余的空间:vector<int> (vec).swap(vec);
2.释放整个vector空间:vector<int> ().swap(vec);
释放思想:vector()使用默认构造函数建立临时vector对象,再对该临时对象上调用swap()成员,swap调用之后对象vector占用的空间就等于一个默认构造的对象的大小,临时对象就具有原来对象vector的大小,而临时对象随机被析构,从而其占用的空间被释放。
[] 下标检查
vector:通过下标访问vector中的元素时会做边界检查,确保不会数组越界。
map:map的下标运算符[]是把key作为下标去执行查找,并返回相应的值;如果不存在这个key,就创建一个新的键值对,值进行值初始化,对于内置类型,值将被初始化为默认值,然后把键 key 插入到 map 中。
(2)deque
deque是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,它在头部和尾部插入元素很快。但在中间插入元素比较费时,因为必须移动中间其他的元素。
虽然deque是连续线性空间,但其实我更愿意说它是连续分段空间。它实际上是由一段一段的连续空间构成的。一旦有必要在deque前端或者尾端增加新的空间,就再配置一段连续的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
Deque用一小块连续的内存空间叫做map作为主控(注意,不是STL的map容器),里面的每一个元素,也就是节点都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。SGI STL允许我们指定缓冲区大小,默认使用 512 字节的缓冲区。
(3)stack
stack是一种先进后出(First In Last Out)的数据结构,它只有一个出口。stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不能遍历,也没有迭代器。
初始化
stack<int>s1;
//定义一个储存数据类型为int的stack容器s1
empty() //判断堆栈是否为空
pop() //弹出堆栈顶部的元素
push() //向堆栈顶部添加元素
size() //返回堆栈中元素的个数
top() //返回堆栈顶部的元素
遍历
s.top();
s.pop();
(4)queue
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。
(5)list
list概念
list容器的内部其实就是一个双向链表,相比于vector的连续线性空间,list采用动态存储分配,不会造成内存浪费和溢出。它执行插入和删除操作十分方便,修改指针就行,不需要移动大量元素。所以,list对于空间的掌握更精准。
常用操作
list<int> lt1;
// 构造int类型的空容器
list<int> lt;
// 头插与头删
lt.push_front(1);
lt.pop_front();
// 尾插与尾删数据
lt.push_back(1);
lt.pop_back();
list<int>::iterator pos = find(lt.begin(), lt.end(), 2);
lt.insert(pos, 4); //在2的位置插入9
lt.erase(pos); // 删除2
empty()
size()
front() 返回list的第一个节点中值的引用
back()
push_front() 在list首元素前插入值为val的元素
pop_front() 删除list中第一个元素
push_back() 在list尾部插入值为val的元素
pop_back() 删除list中最后一个元素
insert()
erase()
swap
clear()
list优势
注意: list不能像vector一样以普通指针作为迭代器,因为它的节点不能保证在同一块连续的内存空间上。
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。而vector会失效。至于List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受影响。
list容器提供的是Bidirectional Iterators.
(6)set 、multiset、unordered_set
概念
set 是一个内部有序且不含重复元素的容器,它底层实现的是红黑树。
set中的元素即是键也是值,所以我们不能通过set的迭代器改变set的值,这关系到set元素的排序规则,如果改变set的元素值,就会破坏set的组织。
set的iterator是一种const_iterator.
当对容器中的元素进行插入操作或者删除操作的时候,只会影响当前元素的迭代器,其他元素的迭代器不受影响。
除开 vector 和 string 之外的 STL 容器都不支持 *(it+i) 的访问方式
基本函数
insert()//插入元素
count()//判断容器中是否存在某个元素
size()//返回容器的尺寸,也可以元素的个数
erase()//删除集合中某个元素
clear()//清空集合
empty()//判断是否为空
begin()//返回第一个节点的迭代器
end()//返回最后一个节点加1的迭代器
rbegin()//反向迭代器
rend()//反向迭代器
//功能函数(进阶)
find()//查找某个指定元素的迭代器
lower_bound()//二分查找第一个不小于某个值的元素的迭代器
get_allocator()//返回集合的分配器
swap()//交换两个集合的变量
max_size()//返回集合能容纳元素的最大限值
set遍历
#include<set>
int main(){
set<int> s;//定义
s.insert(3);//插入元素3
s.insert(2);//插入元素2
set<int>::iterator it;
for(it=s.begin(); it != s.end(); it++){
cout << *it << ' ';
}
// 或者
for(auto it:s){
cout<< it <<' ';
}
}
multiset
multiset特性及用法和set完全相同,唯一的差别在于它允许键值重复。
unordered_set的底层是哈希表,也就是散列表,它不再以键值对的形式存储数据,而是直接存储数据的值 ;它的值不能被修改,也不会对元素进行排序。
(7)map、multimap、unordered_map
概念
map是关联式容器中的一种,里面的元素是由键key和值value组成的键值对,这个键值对也叫做队组,所谓队组就是把键和对应的值组合成一组数据,pair的第一个元素是键,第二个元素是值。。
map的底层实现是红黑树,它不允许键重复,值可以重复。而且键不能被修改,因为map的键值关系到map元素的排列规则,修改map的键将会破坏map的组织。
Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,只会影响当前元素的迭代器,其他元素的迭代器不受影响。
基本函数
begin() 返回指向map头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数, (帮助评论区理解: 因为key值不会重复,所以只能是1 or 0)
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
swap() 交换两个map
upper_bound() 返回键值>给定元素的第一个位置
value_comp() 返回比较元素value的函数
大小:int nSize = mapStudent.size();
插入
// 定义map
map<int, string> mapStudent;
// 第一种 用insert函數插入pair
mapStudent.insert(pair<int, string>(000, "student_zero"));
// 第二种 用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type(001, "student_one"));
// 第三种 用"array"方式插入
mapStudent[123] = "student_first";
用insert插入时,如果map中已经有了对应关键字,insert无效。而数组方式插入会覆盖键对应的值。
查找
// find 返回迭代器指向当前查找元素的位置,找不到就返回map::end()位置
iter = mapStudent.find("123");
if(iter != mapStudent.end())
cout<<"Find, the value is"<<iter->second<<endl;
删除和清空
//迭代器刪除
iter = mapStudent.find("123");
mapStudent.erase(iter);
//用关键字刪除
int n = mapStudent.erase("123"); //刪除成功返回1,否則返回0
//用迭代器范围刪除 : 把整个map清空
mapStudent.erase(mapStudent.begin(), mapStudent.end()); //等同于mapStudent.clear()
multimap和map的操作类似,唯一区别multimap键值可重复。
map与unordered_map扩容机制
map使用红黑树实现,它是一种自平衡的二叉搜索树,保持元素的有序性。unordered_map使用哈希表实现,哈希表里的元素称为桶,由桶所链接的元素叫做节点,哈希表里是用vector容器来存桶元素的,因为vector容器本身具有动态扩容能力,无需人工干预。
map的扩容是基于表格的使用情况。当插入新元素时,如果表格已满或接近满,std::map会触发扩容,map会重新分配更大的内存空间和将现有的键值对重新散列到新的表格中,并且保持元素的顺序性和平衡性。
unordered_map的扩容是基于负载因子。负载因子是当前元素数量与桶数量的比值。当负载因子超过容器设置的最大负载因子时,unordered_map会触发扩容。unordered_map会创建一个更大的桶数组,并将旧桶数组中的元素重新哈希到新桶数组中。它的目标是保持较低的哈希冲突,提高性能。
map插入方式有哪几种?
1.用数组方式插入数据
mapStudent[1] = "student_one";
2.用insert函数插入pair数据,
mapStudent.insert(pair<int, string>(1, "student_one"));
3.在insert函数中使用make_pair()函数
mapStudent.insert(make_pair(1, "student_one"));
4.用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type (1, "student_one"));
容器内删除元素
顺序容器(序列式容器,比如vector、deque)
erase之后,后边每个元素都往前移动一位。所以erase迭代器会使当前元素和后面元素的迭代器都失效(list除外),所以不能使用erase(it++)的方式,但是erase的返回值是下一个有效迭代器;
It = c.erase(it);
关联容器(关联式容器,比如map、set、multimap、multiset等)
erase迭代器只是被删除元素的迭代器失效,其他元素的迭代器不受影响。它的返回值是void,所以要采用erase(it++)的方式删除迭代器;
c.erase(it++)
二、迭代器
迭代器是一种抽象的设计概念。 在<<设计模式>>里面有对iterator模式的定义:提供一种方法,使用这种方法就能按序遍历某个容器中的所有元素,而且不会暴露这个容器的内部表示方式。
因为STL的中心思想在于将容器和算法分开,彼此独立设计,最后再把它们撮合在一起,这个粘的东西就是容器。所以我理解它其实就相当于容器和操纵容器的算法之间的中介。
迭代器的作用就是提供一个遍历容器内部所有元素的接口,所以迭代器内部有一个保存和容器相关联的指针,然后重载各种运算操作来遍历,其中比较重要的是*运算符与->运算符,以及++、–等可能需要重载的运算符重载。这和C++中的智能指针很像,智能指针也是将一个指针封装,然后通过引用计数或是其他方法完成自动释放内存的功能。
迭代器分类
迭代器 | 功能 |
---|---|
输入迭代器 | 提供对数据的只读访问,支持++、==、!= |
输出迭代器 | 提供对数据的只写访问,支持++ |
前向迭代器 | 提供读写操作,并能向前推进迭代器,支持++、==、!= |
双向迭代器 | 提供读写操作,并能向前和向后操作,支持++、–- |
随机访问迭代器 | 提供读写操作,并能以跳跃的方式访问容器的任意数据,是功能最强的迭代器,支持++、–-、[n]、-n、<、<=、>、>= |
容器对应的迭代器
容器 | 迭代器 |
---|---|
vector、deque | 随机访问迭代器 |
stack、queue、priority_queue | 无 |
list、(multi)set/map | 双向迭代器 |
unordered_(multi)set/map、forward_list | 前向迭代器 |
STL中迭代器失效的情况
vector插入元素
1、尾后插入:size < capacity时,只有尾迭代失效(未重新分配空间),size == capacity时,所有迭代器均失效(需要重新分配空间)。
2、中间插入:size < capacity时,插入元素之后所有迭代器失效,size == capacity时,所有迭代器均失效。
traits
traits是一种特性萃取技术,经常用来针对不同类型提供相同或不同的实现,实际上就是通过模板中的类型推导机制,获取到变量的类型。比如在 STL 中,容器与算法是分开的,彼此独立设计,容器与算法之间通过迭代器联系在一起。算法就是通过 traits 从迭代器类中萃取出容器元素的类型的。
Traits编程技法对迭代器加以规范,利用C++重载机制和参数推导机制将运行期决议问题提前到编译期决议,获得了对象的类型相关信息,从而使得算法可以通过类型信息来优化效率。
Traits技法的优点是在编译时进行类型检查和类型推断,避免了运行时的开销,并提供了灵活的编程方式。它可以帮助在泛型代码中针对不同类型进行定制化的操作,提高代码的可重用性和可扩展性。
三、算法
算法是用来操作容器中的数据的模板函数。例如,STL用sort()来对一个vector中的数据进行排序,用find()来搜索一个list中的对象,函数本身与他们操作的数据的结构和类型无关,因此他们可以在从简单数组到高度复杂容器的任何数据结构上使用;从实现的角度来看,STL算法是一种function tempalte.
二叉树
二叉树的子树有左右之分,每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
满二叉树
每个分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上
完全二叉树
叶子结点只能出现在最下面两层,且最下层的叶子结点集中在树的左边
二叉搜索树
若它的左子树不空,则左子树上所有结点的值均小于它根结点的值。
若它的右子树不空,则右子树上所有结点的值均大于它根结点的值。
中序遍历就是有序数组
平衡二叉树(AVL)
任意节点的子树的高度差都小于等于 1
红黑树
红黑树是一种二叉搜索树,但在它在每个节点上增加了一个存储位,用于表示结点的颜色,这个颜色可以是红色或者黑色,所以我们叫它红黑树。
红黑树性质
每个结点不是红色就是黑色,根结点是黑色的。
如果一个结点是红色的,那它的两个孩子结点就是黑色的。
对于每个结点,从这个结点到它所有后代叶子结点的路径上都包含相同数目的黑色结点。
每个叶子结点都是黑色的(此处的叶子结点指定是空结点)。
红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O ( logN ) 。
AVL树是严格平衡的二叉搜索树,左右子树高度不超过1;而红黑树是近似平衡的,它的最长路径不超过最短路径的二倍.
相对于AVL树来说,红黑树降低了插入结点时需要进行的旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,实际运用时也大多用的是红黑树。
底层是红黑树,实现map的红黑树的节点数据类型是key+value,而实现set的节点数据类型是value
红黑树可以确保从根到叶子的最长可能路径不会超过最短路径的两倍
假设在红黑树中,从根到叶子的所有路径上包含的黑色结点的个数都是N个,那么最短路径就是全部由黑色结点构成的路径,即长度为N,最长可能路径就是由一黑一红结点构成的路径,即长度为 2N 。所以红黑树从根到叶子的最长路径不会超过最短路径的两倍。
哈希表
概念
哈希表也叫"散列表",它是根据键去直接访问内存存储位置的一种数据结构。它通过计算一个关于键值的函数,把所需查询的数据映射到表中的一个位置来访问记录,这样就加快了查找的速度,这个映射函数就叫做哈希函数,存放记录的数组叫做哈希表。其实哈希表的本质上就是一个数组,哈希表就是通过把key用一个哈希函数加工处理之后得到一个值,这个值就是数据存放的位置,我们就可以根据这个值快速的找到我们想要的数据。
构造哈希表
直接定址法,取关键字或关键字的某个线性函数值为哈希地址。
随机数法:选择一个随机函数,取关键字的随机函数值为它的哈希地址
哈希冲突
处理哈希冲突有两种主要方法:
开放寻址法: 换个位置
开链法: 当哈希函数将不同的键映射到相同的桶时,发生哈希冲突。链地址法的基本思想是将冲突的元素存储在同一个桶中,并使用链表将它们链接起来。
创建一个桶数组,每个桶初始化为空。定义一个哈希函数,将键映射到桶索引。当插入键值对时,首先使用哈希函数计算键的哈希值,并将其映射到对应的桶索引。如果该桶中已经有元素存在,发生了哈希冲突。在链地址法中,新元素将被插入到链表的头部或尾部(取决于具体的实现)。可以使用单向链表、双向链表或其他数据结构来存储冲突的元素。当查找特定键的值时,使用哈希函数计算键的哈希值,并定位到对应的桶。然后遍历该桶中的链表,找到与给定键匹配的元素。当删除键值对时,使用哈希函数计算键的哈希值,并定位到对应的桶。然后在该桶的链表中搜索并删除与给定键匹配的元素。
hashtable中解决哈希冲突方法?
线性探测
使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位
开链
每个表格维护一个list,如果hash函数计算出的格子相同,则按顺序存在这个list中
再散列
发生冲突时使用另一种hash函数再计算一个地址,直到不冲突
常用的遍历算法
for_each(iterator beg, iterator end, _callback); //遍历容器元素
transform(iterator beg1, iterator end1, iterator beg2, _callbakc) //将指定容器区间元素搬运到另一容器中
常用查找算法
find(iterator beg, iterator end, value)
adjacent_find(iterator beg, iterator end, _callback); //查找相邻重复元素
bool binary_search(iterator beg, iterator end, value); // 二分查找
count(iterator beg, iterator end, value); // 统计元素出现次数
常用排序算法
merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest) //容器元素合并,并存储到另一容器中
sort(iterator beg, iterator end, _callback) //容器元素排序
random_shuffle(iterator beg, iterator end) //对指定范围内的元素随机调整次序
reverse(iterator beg, iterator end) // 反转指定范围的元素
常用拷贝和替换算法
copy(iterator beg, iterator end, iterator dest) //将容器内指定范围的元素拷贝到另一容器中
replace(iterator beg, iterator end, oldvalue, newvalue) //将容器内指定范围的旧元素修改为新元素
swap(container c1, container c2) //互换两个容器的元素
常用算数生成算法
accumulate(iterator beg, iterator end, value) //计算容器元素累计总和
fill(iterator beg, iterator end, value) // 向容器中填充元素
常用集合算法
set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest) //求两个set集合的交集
set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest) //求两个set集合的并集
set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest) //求两个set集合的差集
四、STL空间配置器
设计初衷:首先讲一下为什么会出现空间配置器,我们知道当STL容器保存的的数据量超过容器容量时,需要进行扩容。如果频繁扩容,就需要不断的向操作系统申请和释放空间,那这样太影响操作系统的效率了,为了效率考虑就出现了空间配置器。
空间配置器:是操作系统开辟出的一大块内存空间,当STL需要扩容申请内存时,就从空间配置器中申请,不需要再经过操作系统,那么只有当空间适配器的空间不够时,才会向操作系统申请。
结构:空间配置器有两级结构,一级空间配置器是用来处理大块内存,二级空间配置器处理小块内存。SGI-STL规定以128字节作为小块内存和大块内存的分界线。
使用流程:容器进行扩容,如果申请的空间是大于128字节,直接向一级空间配置器申请。如果小于128字节,先查找哈希桶对应大小位置是否为空,不为空,直接从该位置申请空间,如果该位置为空,向内存池申请。当内存池空间不够了会直接向OS申请一大块空间。
一级空间配置器:一级空间配置器原理比较简单,直接是对malloc和free进行了封装,并且增加了C++中的申请空间失败抛异常机制,也就是如果向操作系统申请内存失败就会抛异常。
为什么不直接用C++的new和delete,因为这里并不需要调用构造函数和析构函数。
二级空间配置器:二级空间配置器专门负责处理小于128字节的小块内存,耳机空间配置器主要采用了内存池和哈希桶两个技术。
内存池:其中内存池的技术用来提高申请空间的速度以及减少额外空间的浪费,采用哈希桶的方式来提高用户获取空间的速度和高效管理。
有个问题就是用户申请空间很简单,但是释放时,并不知道这块空间应该放在内存池的什么位置。使用哈希桶的技术来解决归问题。
哈希桶:哈希桶也可以叫做自由链表,它是一个指针数组,里面每个元素代表所挂的区块大小,如果用户申请小于等于128的区块,就到自由链表中取。如果自由链表中没有对应的内存块,就向内存池里申请,内存池也没有,就先把内存池残余的零头给挂在自由链表上,然后向操作系统申请空间。
deallocate() :空间释放函数deallocate(),该函数首先判断区块大小,大于128bytes时,直接调用一级配置器,小于128bytes就找到对应的free-list然后释放内存。
五、仿函数
仿函数:行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是一种重载了operator()的class 或者class template。
六、适配器
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。