这里写目录标题
- STL定义
- 两大特点
- 两个层次
- STL构成
- 容器
- 容器概念
- 容器分类
- vector
- vector概念
- vector数据结构
- 常用操作
- deque
- stack
- 常用操作
- queue
- list
- list概念
- 常用操作
- list优势
- set 与 multiset容器
- 概念
- 基本函数
- set遍历
- multiset
- unordered_set
- map/multimap容器
- 概念
- 基本函数
- multimap
- 算法
- 二叉树
- 满二叉树
- 完全二叉树
- 二叉搜索树
- 平衡二叉树(AVL)
- 红黑树
- 红黑树性质
- 红黑树可以确保从根到叶子的最长可能路径不会超过最短路径的两倍
- 哈希表
- 概念
- 哈希表的构造方法
- 哈希冲突
- 常用的遍历算法
- 常用查找算法
- 常用排序算法
- 常用拷贝和替换算法
- 常用算数生成算法
- 常用集合算法
- 迭代器
- 仿函数
- 适配器
- 空间配置器
- traits
STL定义
STL(Standard Template Library),即标准模板库。它在1994年被正式纳入C++标准,是 C++ 标准库的重要组成部分。它不仅仅是一个可复用的组件库,而且是一个包含了许多在计算机科学领域里常用的基本数据结构和基本算法的软件框架。
两大特点
STL的第一个比较重要的特点是数据结构和算法的分离。
虽然这只是个简单的概念,但这种分离确实使得STL变得非常通用。例如,由于STL的sort()函数是完全通用的,你可以用它来操作几乎任何数据集合,包括链表,容器和数组;
STL的另一个重要特性是它不是面向对象而是面向泛型编程的。
在STL是中找不到任何明显的类继承关系,这好像是一种倒退,但这正好是使得STL的组件具有广泛通用性的底层特征。另外,由于STL是基于模板,内联函数的使用使得生成的代码短小高效;
两个层次
首先从逻辑层次来看,在STL中体现了泛型化程序设计的思想,引入了诸多新的名词,比如像容器(container),算法(algorithmn),迭代器等等。与面向对象中的多态一样,泛型也是一种软件的复用技术;
从实现层次看,整个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容器,最坏的情况下可能要遍历完整个容器才能找到该用户。 |
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,找到返回在向量中的位置
deque
deque是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,它不论在尾部或头部插入元素,都十分迅速。而在中间插入元素则会比较费时,因为必须移动中间其他的元素。
虽然deque是连续线性空间,但其实我更愿意说它是连续分段空间。它实际上是由一段一段的连续空间构成的。一旦有必要在deque前端或者尾端增加新的空间,就再配置一段连续的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。SGI STL允许我们指定缓冲区大小,默认值 0表示将使用 512 bytes缓冲区。
stack
stack是一种先进后出(First In Last Out)的数据结构,它只有一个出口。stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为(也就是没有迭代器)。
常用操作
初始化
stack<int>s1;
//定义一个储存数据类型为int的stack容器s1
empty() //判断堆栈是否为空
pop() //弹出堆栈顶部的元素
push() //向堆栈顶部添加元素
size() //返回堆栈中元素的个数
top() //返回堆栈顶部的元素
遍历
s.top();
s.pop();
queue
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。
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的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。
list容器提供的是Bidirectional Iterators.
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效,甚至List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。
set 与 multiset容器
概念
set 是一个内部有序且不含重复元素的容器,它底层实现的是红黑树。
set中的元素即是键也是值,所以我们不能通过set的迭代器改变set的值,这关系到set元素的排序规则,如果改变set的元素值,就会破坏set的组织。
set的iterator是一种const_iterator.
set拥有和list某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器必然是一个例外。
除开 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
unordered_set的底层是哈希表,也就是散列表,它不再以键值对的形式存储数据,而是直接存储数据的值 ;值不能被修改,也不会对元素进行排序。
map/multimap容器
概念
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
Multimap和map的操作类似,唯一区别multimap键值可重复。
算法
算法是用来操作容器中的数据的模板函数。例如,STL用sort()来对一个vector中的数据进行排序,用find()来搜索一个list中的对象,函数本身与他们操作的数据的结构和类型无关,因此他们可以在从简单数组到高度复杂容器的任何数据结构上使用;从实现的角度来看,STL算法是一种function tempalte.
二叉树
二叉树的子树有左右之分,每个结点最多有两棵子树,即二叉树不存在度大于2的结点。
满二叉树
每个分支结点都存在左子树和右子树,并且所有叶子结点都在同一层上
完全二叉树
叶子结点只能出现在最下面两层,且最下层的叶子结点集中在树的左边
二叉搜索树
若它的左子树不空,则左子树上所有结点的值均小于它根结点的值。
若它的右子树不空,则右子树上所有结点的值均大于它根结点的值。
中序遍历就是有序数组
平衡二叉树(AVL)
任意节点的子树的高度差都小于等于 1
红黑树
红黑树是一种二叉搜索树,但在它在每个节点上增加了一个存储位,用于表示结点的颜色,这个颜色可以是红色或者黑色,所以我们叫它红黑树。
红黑树性质
每个结点不是红色就是黑色,根结点是黑色的。
如果一个结点是红色的,那它的两个孩子结点就是黑色的。
对于每个结点,从这个结点到它所有后代叶子结点的路径上都包含相同数目的黑色结点。
每个叶子结点都是黑色的(此处的叶子结点指定是空结点)。
红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O ( logN ) O(logN)O(logN)。
AVL树是严格平衡的二叉搜索树,左右子树高度不超过1;而红黑树是近似平衡的,它的最长路径不超过最短路径的二倍.
相对于AVL树来说,红黑树降低了插入结点时需要进行的旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,实际运用时也大多用的是红黑树。
红黑树可以确保从根到叶子的最长可能路径不会超过最短路径的两倍
假设在红黑树中,从根到叶子的所有路径上包含的黑色结点的个数都是N个,那么最短路径就是全部由黑色结点构成的路径,即长度为N,最长可能路径就是由一黑一红结点构成的路径,即长度为 2N 。所以红黑树从根到叶子的最长路径不会超过最短路径的两倍。
哈希表
概念
哈希表也叫"散列表",它是根据键去直接访问内存存储位置的一种数据结构。它通过计算一个关于键值的函数,把所需查询的数据映射到表中的一个位置来访问记录,这样就加快了查找的速度,这个映射函数就叫做哈希函数,存放记录的数组叫做哈希表。其实哈希表的本质上就是一个数组,哈希表就是通过把key用一个哈希函数加工处理之后得到一个值,这个值就是数据存放的位置,我们就可以根据这个值快速的找到我们想要的数据。
哈希表的构造方法
直接定址法,取关键字或关键字的某个线性函数值为哈希地址。
随机数法:选择一个随机函数,取关键字的随机函数值为它的哈希地址
哈希冲突
处理哈希冲突有两种主要方法:
开放寻址法: 换个位置
拉链法: 同一位置的冲突对象组织在一起,链地址法就是将相应位置上冲突的所有关键词存储在同一个单链表中
常用的遍历算法
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集合的差集
迭代器
迭代器是一种抽象的设计概念。 在<<设计模式>>这本书里有对iterator模式的定义:提供一种方法,使用这种方法就能按序遍历某个容器中的所有元素,而且不会暴露这个容器的内部表示方式。我理解它其实就相当于容器和操纵容器的算法之间的中介。
因为STL的中心思想在于将容器和算法分开,彼此独立设计,最后再把它们撮合在一起,这个粘的东西就是容器。
迭代器就如同一个指针。事实上,C++的指针也是一种迭代器。但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符地方法的类对象;
从实现角度来看,迭代器是一种将operator* , operator++,operator–等这些指针相关操作都重载了的class template. 所有STL容器都附带有自己专属的迭代器。
迭代器 | 功能 |
---|---|
输入迭代器 | 提供对数据的只读访问,支持++、==、!= |
输出迭代器 | 提供对数据的只写访问,支持++ |
前向迭代器 | 提供读写操作,并能向前推进迭代器,支持++、==、!= |
双向迭代器 | 提供读写操作,并能向前和向后操作,支持++、–- |
随机访问迭代器 | 提供读写操作,并能以跳跃的方式访问容器的任意数据,是功能最强的迭代器,支持++、–-、[n]、-n、<、<=、>、>= |
仿函数
仿函数:行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是一种重载了operator()的class 或者class template
适配器
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
空间配置器
空间配置器:负责空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class tempalte.
traits
traits是一种特性萃取技术,经常用来针对不同类型提供相同或不同的实现,实际上就是通过模板中的类型推导机制,获取到变量的类型。比如在 STL 中,容器与算法是分开的,彼此独立设计,容器与算法之间通过迭代器联系在一起。算法就是通过 traits 从迭代器类中萃取出容器元素的类型的。