标准模板库(STL)
标准模板库(STL)
ZEROKO14标准模板库(STL)
STL概述
长久以来,软件界一直希望建立一种可重复利用的东西,以及一种得以制造出”可重复运用的东西”的方法,让程序员的心血不止于随时间的迁移,人事异动而烟消云散,从函数(functions),类别(classes),函数库(function libraries),类别库(class libraries)、各种组件,从模块化设计,到面向对象(object oriented ),为的就是复用性的提升。
复用性必须建立在某种标准之上。但是在许多环境下,就连软件开发最基本的[[数据结构]] 和[[算法]]都未能有一套标准。大量程序员被迫从事大量重复的工作,竟然是为了完成前人已经完成而自己手上并未拥有的程序代码,这不仅是人力资源的浪费,也是挫折与痛苦的来源。
为了建立[[数据结构]]和[[算法]]的一套标准,并且降低他们之间的耦合关系,以提升各自的独立性、弹性、交互操作性(相互合作性,interoperability),诞生了STL。
STL基本概念
**STL(Standard Template Library,标准模板库)**,是惠普实验室开发的一系列软件的统
称。现在主要出现在 c++中,但是在引入 c++之前该技术已经存在很长时间了。
STL 从广义上分为: 容器(container) [[算法]]迭代器(iterator),容器和[[算法]]之间通过迭代器进行无缝连接。STL 几乎所有的代码都采用了模板类或者模板函数,这相比传统的由函数和类组成的库来说提供了更好的代码重用机会。STL(Standard Template Library)标准模板库,在我们 c++标准程序库中隶属于 STL 的占到了 80%以上。
STL六大组件简介
STL提供了六大组件,彼此之间可以组合套用,这六大组件分别是:容器、[[算法]]、迭代器、仿函数、适配器(配接器)、空间配置器。
- 容器:各种[[数据结构]],如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL容器是一种class template。
- [[算法]]:各种常用的[[算法]],如sort、find、copy、for_each。从实现的角度来看,STL算法是一种function tempalte.
- 迭代器:扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是一种将operator* , operator-> , operator++,operator–等指针相关操作予以重载的class template. 所有STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。
- 仿函数:行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是一种重载了operator()的class 或者class template
- 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
- 空间配置器:负责空间的配置与管理。从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class tempalte.
STL六大组件的交互关系,容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数。
STL优点
- STL 是 C++的一部分,因此不用额外安装什么,它被内建在你的编译器之内。
- STL 的一个重要特性是将数据和操作分离。数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作
- 程序员可以不用思考 STL 具体的实现过程,只要能够熟练使用 STL 就 OK 了。这样他们就可以把精力放在程序开发的别的方面。
- STL 具有高可重用性,高性能,高移植性,跨平台的优点。
- 高可重用性:STL 中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。关于模板的知
识,已经给大家介绍了。 - 高性能:如 map 可以高效地从十万条记录里面查找出指定的记录,因为 map 是采用红黑树的变体实现的。
- 高移植性:如在项目 A 上用 STL 编写的模块,可以直接移植到项目 B 上。
STL之父Alex Stepanov 亚历山大·斯特潘诺夫(STL创建者)
STL三大组件(全部为6大)
容器
容器,置物之所也。
研究数据的特定排列方式,以利于搜索或[[算法#排序算法|排序]]或其他特殊目的,这一门学科我们称为[[数据结构]]。大学信息类相关专业里面,与编程最有直接关系的学科,首推[[数据结构]]与[[算法]]。几乎可以说,任何特定的数据结构都是为了实现某种特定的算法。STL容器就是将运用最广泛的一些数据结构实现出来。
常用的[[数据结构]]:数组(array),链表(list),tree(树),栈(stack),队列(queue),集合(set),映射表(map),根据数据在容器中的排列特性,这些数据分为序列式容器和关联式容器两种。
- 序列式容器强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。
- 关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器
算法
[[算法]],问题之解法也。
以有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做[[算法]].
广义而言,我们所编写的每个程序都是一个算法,其中的每个函数也都是一个算法,毕竟它们都是用来解决或大或小的逻辑问题或数学问题。STL收录的算法经过了数学上的效能分析与证明,是极具复用价值的,包括常用的排序,查找等等。特定的算法往往搭配特定的数据结构,[[算法]]与[[数据结构]]相辅相成。
算法分为:质变算法和非质变算法。
- 质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等
- 非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等
$$
再好的编程技巧,也无法让一个笨拙的算法起死回生。
$$
e.g.标准模版库中sort函数包含在头文件
1 | default (1) |
- 默认情况下是对 [first,last)区间的元素采用由小到大的方式排列
- 可以自定义比较函数,也可以调用stl内提供的比较函数,less
() 、greater () - 排序的区间可以必须是通过迭代器遍历的(数组下标也算),迭代器的类型为随机迭代器;对于不支持随机访问迭代器的容器,内部会提供对应的算法接口,比如List
- 排序是通过多次内存的copy来实现的,所以链表不能使用stl 算法sort来对其排序(next指针修改问题);
迭代器
迭代器(iterator)是一种抽象的设计概念,现实程序语言中并没有直接对应于这个概念的实物。在<<Design Patterns>>一书中提供了23种设计模式的完整描述,其中iterator模式定义如下:提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
迭代器的设计思维-STL的关键所在,STL的中心思想在于将容器(container)和算法(algorithms)分开,彼此独立设计,最后再一贴胶着剂将他们撮合在一起。从技术角度来看,容器和算法的泛型化并不困难,c++的class template和function template可分别达到目标,如果设计出两这个之间的良好的胶着剂,才是大难题。
迭代器是一种智能指针(vector
1 |
|
迭代器的种类:
| 迭代器名称 | 描述 | 详细功能 |
|---|---|---|
| 输入迭代器 | 提供对数据的只读访问 | 只读,支持++、==、!= |
| 输出迭代器(output iterator) | 提供对数据的只写访问 | 只写,支持++ |
| 前/正向迭代器(forward iterator) | 提供读写操作,并能向前推进迭代器 | 读写,支持++、==、!= |
| 双向迭代器(bidirectional iterator) | 提供读写操作,并能向前和向后操作 | 读写,支持++、–, |
| 随机访问迭代器(random access iterator) | 提供读写操作,并能以跳跃的方式访问容器的任意数据,是功能最强的迭代器 | 读写,支持++、–、[n]、+-n、<、<=、>、>= |
p.s.上面的迭代器都可以支持++,但只有随机访问迭代器可以支持迭代器+数字的操作,不然会报错。因此可以以此作为判断是否随机访问迭代器的方式
- array: 随机访问迭代器
- vector: 随机访问迭代器
- string: 随机访问迭代器
- deque: 随机访问迭代器
- list: 双向迭代器
- set 和 multiset: 双向迭代器
- map 和 multimap: 双向迭代器
- unordered_set 和 unordered_multiset: 正向迭代器
- unordered_map 和 unordered_multimap: 正向迭代器
queue和stack没有迭代器访问元素,只能通过操作函数进行访问
案例:
1 |
|
$$
【重点理解】迭代器vector
$$
distance函数
直接使用两个迭代器相减就可以得到距离,但也可以使用这个函数
int distance( InputIt first1, InputIt first2 );
用于计算两个迭代器之间的距离,即两个迭代器所指向的元素之间的距离。
参数说明:
first1:起始迭代器,指向要计算距离的范围的起始位置。first2:结束迭代器,指向要计算距离的范围的结束位置。
返回值说明:
- 表示迭代器之间的距离。如果
first1与first2相等,则返回值为0;如果first1大于first2,返回值为负数。
迭代器语法注意点
stl中的end函数实际上是指向容器的最后一个元素之后的位置的迭代器。这个迭代器指向一个“不存在”的元素,它不表示任何实际的元素,只是用来表示容器的结尾。在使用end迭代器时,不能通过它来访问容器中的元素,但是可以使用它来判断迭代器是否指向容器的结尾。
因此要注意: 使用容器.end()--无法获得容器.rbegin()
常用容器
容器底层结构一览图
vector容器(动态数组)
也称为向量
vector容器基本概念
vector的数据安排以及操作方式,与array非常相似,两者的唯一差别在于空间的运用的灵活性。Array是静态空间,一旦配置了就不能改变,要换大一点或者小一点的空间,也可以,但一切琐碎得由自己来,首先配置一块新的空间,然后将旧空间的数据搬往新空间,再释放原来的空间。Vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新元素。因此vector的运用对于内存的合理利用与运用的灵活性有很大的帮助,我们再也不必害怕空间不足而一开始就要求一个大块头的array了。
头文件:#include<vector>
Vector的实现技术,关键在于其对大小的控制以及重新配置时的数据移动效率,一旦vector旧空间满了,如果客户每新增一个元素,vector内部只是扩充一个元素的空间,实为不智,因为所谓的扩充空间(不论多大),一如刚所说,是”配置新空间-数据移动-释放旧空间”的大工程,时间成本很高,应该加入某种未雨绸缪的考虑,稍后我们便可以看到vector的空间配置策略。
vector迭代器
Vector维护一个线性空间,所以不论元素的型别如何,普通指针都可以作为vector的迭代器,因为vector迭代器所需要的操作行为,如operaroe*, operator->, operator++, operator–, operator+, operator-, operator+=, operator-=, 普通指针天生具备。Vector支持随机存取,而普通指针正有着这样的能力。所以vector提供的是随机访问迭代器(Random Access Iterators).
根据上述描述,如果我们写如下的代码:
1 | Vector<int>::iterator it1; |
it1的型别其实就是Int*,it2的型别其实就是Teacher*.
1 |
|
由图可看出vector的内存空间的申请策略
vector的数据结构
Vector所采用的数据结构非常简单,线性连续空间,它以两个迭代器_Myfirst和_Mylast分别指向配置得来的连续空间中目前已被使用的范围,并以迭代器_Myend指向整块连续内存空间的尾端。
为了降低空间配置时的速度成本,vector实际配置的大小可能比客户端需求大一些,以备将来可能的扩充,这边是容量的概念。换句话说,一个vector的容量永远大于或等于其大小,一旦容量等于大小,便是满载,下次再有新增元素,整个vector容器就得另觅居所。
【注意】所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后尚有可配置的空间),而是一块更大的内存空间,然后将原数据拷贝新空间,并释放原空间。因此,对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就都失效了。这是程序员容易犯的一个错误,务必小心。
vector常用API操作
vector构造函数
1 | vector<T> v; //采用模板实现类实现,默认构造函数 |
vector常用赋值操作
1 | assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。 |
vector大小操作
1 | size();//返回容器中元素的个数 |
vector数据存取操作
1 | at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常 |
vector插入和删除操作
1 | insert(const_iterator pos, int count,ele);//迭代器指向位置pos插入count个元素ele. |
vector判断元素不存在是通过find函数来判断:find(v.begin(),v.end())==v.end()
vector小案例
巧用swap,收缩内存空间(重点)
1 |
|
reserve预留空间
1 |
|
逆序遍历用reverse_iterator
1 | //用iterator实现逆序遍历 |
只读迭代器const_iterator
1 | void myPrintf(const vector<int> v) |
vector bool的特殊
无法获取元素的真实地址
1 |
|
deque容器(双端队列)
头文件:#include <deque>
deque容器基本概念
Vector容器是单向开口的连续内存空间,deque则是一种双向开口的连续线性空间。所谓的双向开口,意思是可以在头尾两端分别做元素的插入和删除操作,当然,vector容器也可以在头尾两端插入元素,但是在其头部操作效率奇差,无法被接受。
Deque容器和vector容器最大的差异,一在于deque允许使用常数项时间(就是花费固定时间)对头端进行元素的插入和删除操作。二在于deque没有容量的概念,因为它是动态的以分段连续空间组合而成,随时可以增加一段新的空间并链接起来,换句话说,像vector那样,”旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque身上是不会发生的。也因此,deque没有必须要提供所谓的空间保留(reserve)功能.
虽然deque容器也提供了随机访问迭代器Random Access Iterator,但是它的迭代器并不是普通的指针,其复杂度和vector不是一个量级,这当然影响各个运算的层面。因此,除非有必要,我们应该尽可能的使用vector,而不是deque。对deque进行的排序操作,为了最高效率,可将deque先完整的复制到一个vector中,对vector容器进行排序,再复制回deque.
deque容器实现原理
Deque容器是连续的空间,至少逻辑上看来如此,连续现行空间总是令我们联想到array和vector,array无法成长,vector虽可成长,却只能向尾端成长,而且其成长其实是一个假象,事实上(1)申请更大空间 (2)原数据复制新空间 (3)释放原空间 三步骤,如果不是vector每次配置新的空间时都留有余裕,其成长假象所带来的代价是非常昂贵的。
Deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或者尾端。Deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
既然deque是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。Deque代码的实现远比vector或list都多得多。
Deque采取一块所谓的map(注意,不是STL的map容器)作为主控,这里所谓的map是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是deque的存储空间的主体。
deque常用API
以下的pos都是迭代器
deque构造函数
1 | deque<T> deqT;//默认构造形式 |
deque赋值操作
1 | assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。 |
deque大小操作
1 | deque.size();//返回容器中元素的个数 |
deque双端插入和删除操作
1 | push_back(elem);//在容器尾部添加一个数据 |
deque数据存取
1 | at(idx);//返回索引idx所指的数据,如果idx越界,抛出out_of_range。 |
deque插入操作
1 | insert(const_iterator pos,elem);//在pos位置插入一个elem元素的拷贝,返回新数据的位置。 |
deque删除操作
1 | clear();//移除容器的所有数据 |
list容器(双向链表)
头文件:#include<list>
list容器基本概念
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
相较于vector的连续线性空间,list就显得负责许多,它的好处是每次插入或者删除一个元素,就是配置或者释放一个元素的空间。因此,list对于空间的运用有绝对的精准,一点也不浪费。而且,对于任何位置的元素插入或元素的移除,list永远是常数时间。
List和vector是两个最常被使用的容器。
1 | //验证List容器为双向循环链表 |
List容器是一个双向循环链表。(下图展示的首尾并不是null,实际上List是首尾相接的)
- 采用动态存储分配,不会造成内存浪费和溢出
- 链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素
- 链表灵活,但是空间和时间额外耗费较大
list容器的迭代器
List容器不能像vector一样以普通指针作为迭代器,因为其节点不能保证在同一块连续的内存空间上。List迭代器必须有能力指向list的节点,并有能力进行正确的递增、递减、取值、成员存取操作。所谓”list正确的递增,递减、取值、成员取用”是指,递增时指向下一个节点,递减时指向上一个节点,取值时取的是节点的数据值,成员取用时取的是节点的成员。
由于list是一个双向链表,迭代器必须能够具备前移、后移的能力,所以list容器提供的是双向迭代器(Bidirectional Iterators).
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效。这在vector是不成立的,因为vector的插入操作可能造成记忆体重新配置,导致原有的迭代器全部失效,甚至List元素的删除,也只有被删除的那个元素的迭代器失效,其他迭代器不受任何影响。
list常用API
list构造函数
1 | list<T> lstT;//list采用采用模板类实现,对象的默认构造形式: |
list数据元素插入和删除操作
1 | push_back(elem);//在容器尾部加入一个元素 |
【注意】若利用remove删除自定义数据类型,需要重载该自定义类的==运算符,remove函数才知道如何判断是否相等。
list大小操作
1 | size();//返回容器中元素的个数 |
list赋值操作
1 | assign(beg, end);//将[beg, end)区间中的数据拷贝赋值给本身。 |
list数据的存取
1 | front();//返回第一个元素。 |
list反转和排序
1 | reverse();//反转链表,比如lst包含1,3,5元素,运行此方法后,lst就包含5,3,1元素。 |
forward_list容器(单向链表)
C++11才引入
与list容器不同,forward_list是单向的,每个元素只有一个指向下一个元素的指针。它的底层实现通常是由单向链表结构构成,而不是像list那样的双向链表。
既然已经有了 list,为什么 C++ STL 又设计了 forward_list 这一容器呢?设计 forward_list 的目的是为了达到不输于任何一个C风格手写链表的极值效率!为此,forward_list 是一个最小链表设计,它甚至没有
size()接口,因为内部维护一个size变量会降低增删元素的效率。如果想要获取 forward_list 的 size,一个通常的做法是,用std::distance 计算 begin 到 end 的距离得出 size。一句话总结:list 兼顾了接口丰富性牺牲了效率,而 forward_list 舍弃了不必要的接口只为追求极致效率。效率高是选用 forward_list 而弃用 list 容器最主要的原因,换句话说,只要是 list 容器和 forward_list 容器都能实现的操作,应优先选择 forward_list 容器。
相关函数
| 成员函数 | 功能 |
|---|---|
| before_begin() | 返回一个前向迭代器,其指向容器中第一个元素之前的位置。 |
| begin() | 返回一个前向迭代器,其指向容器中第一个元素的位置。 |
| end() | 返回一个前向迭代器,其指向容器中最后一个元素之后的位置。 |
| cbefore_begin() | 和 before_begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
| cbegin() | 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
| cend() | 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 |
| empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
| max_size() | 返回容器所能包含元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。 |
| front() | 返回第一个元素的引用。 |
| assign() | 用新元素替换容器中原有内容。 |
| push_front() | 在容器头部插入一个元素。 |
| emplace_front() | 在容器头部生成一个元素。该函数和 push_front() 的功能相同,但效率更高。 |
| pop_front() | 删除容器头部的一个元素。 |
| emplace_after() | 在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。和 insert_after() 的功能相同,但效率更高。 |
| insert_after() | 在指定位置之后插入一个新元素,并返回一个指向新元素的迭代器。 |
| erase_after() | 删除容器中某个指定位置或区域内的所有元素。 |
| swap() | 交换两个容器中的元素,必须保证这两个容器中存储的元素类型是相同的。 |
| resize() | 调整容器的大小。 |
| clear() | 删除容器存储的所有元素。 |
| splice_after() | 将某个 forward_list 容器中指定位置或区域内的元素插入到另一个容器的指定位置之后。 |
| remove(val) | 删除容器中所有等于 val 的元素。 |
| remove_if() | 删除容器中满足条件的元素。 |
| unique() | 删除容器中相邻的重复元素,只保留一个。 |
| merge() | 合并两个事先已排好序的 forward_list 容器,并且合并之后的 forward_list 容器依然是有序的。 |
| sort() | 通过更改容器中元素的位置,将它们进行排序。 |
| reverse() | 反转容器中元素的顺序。 |
案例
1 |
|
set/multiset容器(集合)
两者都包含在#include<set>头文件中
红黑树(原理)
二叉树就是任何节点最多只允许有两个字节点。分别是左子结点和右子节点。
二叉搜索树,是指二叉树中的节点按照一定的规则进行排序,使得对二叉树中元素访问更加高效。二叉搜索树的放置规则是:任何节点的元素值一定大于其左子树中的每一个节点的元素值,并且小于其右子树的值。因此从根节点一直向左走,一直到无路可走,即得到最小值,一直向右走,直至无路可走,可得到最大值。那么在二叉搜索树中找到最大元素和最小元素是非常简单的事情。下图为二叉搜索树:
平衡二叉树(AVL):任意节点左、右子树高度差(平衡因子 )不超过1
最先发明的自平衡二叉查找树
上面我们介绍了二叉搜索树,那么当一个二叉搜索树的左子树和右子树不平衡的时候,那么搜索依据上图表示,搜索9所花费的时间要比搜索17所花费的时间要多,由于我们的输入或者经过我们插入或者删除操作,二叉树失去平衡,造成搜索效率降低。
所以我们有了一个平衡二叉树的概念,所谓的平衡不是指的完全平衡。
平衡与否的区别
上图图(b)中是二叉树最糟糕的情况,刚好退化成链表
$$
h(层数)=n
\
\
ASL(平均查找长度)=\frac{n+1}{2}
$$上图图(a)中为平衡二叉树
$$
h(层数) = \lfloor log_2n \rfloor +1
\
\
ASL(平均查找长度) = \lfloor log_2n \rfloor +1
$$
RB-tree(红黑树)也为自平衡的二叉查找树,但比AVL要调整次数更少,因此如果插入删除很频繁则应该使用红黑树.
【红黑树逻辑意义如下】
线性查找性能低—>二叉树—>二叉树会出现退化成链表的问题(如上图b)—>出现平衡二叉树—>数据变化有频繁更新节点问题—>出现红黑树(自平衡)
红黑树定义和性质
红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:
- 每个节点要么是黑色,要么是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。
- 每个红色结点的两个子结点一定都是黑色。
- 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
从性质5又可以推出:
- 如果一个结点存在黑子结点,那么该结点肯定有两个子结点
红黑树的自平衡
它靠的是三种操作
- 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
- 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。
- 变色:结点的颜色由红变黑或由黑变红。
由图可见,旋转操作是局部的。另外可以看出旋转操作能保持红黑树的中序遍历的顺序(实际上就是由小到大的顺序)。
但要保持红黑树的性质,结点不能乱挪,还得靠变色
set容器
Set的特性是。所有元素都会根据元素的键值自动被排序。Set的元素不像map那样可以同时拥有实值和键值,set的元素即是键值又是实值。Set不允许两个元素有相同的键值。
我们可以通过set的迭代器改变set元素的值吗?不行,因为set元素值就是其键值,关系到set元素的排序规则。如果任意改变set元素值,会严重破坏set组织。换句话说,set的iterator是一种const_iterator.
set拥有和list某些相同的性质,当对容器中的元素进行插入操作或者删除操作的时候,操作之前所有的迭代器,在操作完成之后依然有效,被删除的那个元素的迭代器必然是一个例外。
multiset容器
multiset特性及用法和set完全相同,唯一的差别在于它允许键值重复。set和multiset的底层实现是红黑树,红黑树为平衡二叉树的一种。
set/multiset常用API
set构造函数
1 | set<T> st;//set默认构造函数: |
set赋值操作
1 | set& operator=(const set &st);//重载等号操作符 |
set大小操作
1 | size();//返回容器中元素的数目 |
set插入和删除操作
1 | insert(elem);//在容器中插入元素。(返回pair<iterator,bool>),bool表示是否插成功 |
set查找操作
1 | find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end(); |
对组(pair)
不需要任何头文件就可以直接使用
对组(pair)将一对值组合成一个值,这一对值可以具有不同的数据类型,两个值可以分别用pair的两个公有属性first和second访问。
两种对组创建的案例:
1 | //1 |
set/multiset注意事项
重复值插入
- set:重复插入同样的值,第一次插入成功,第二次插入无效,编译不会报错。
- multiset:他的insert函数返回的不是对组,仅迭代器表示插入位置(不需要类似set的insert一样返回包含插入成功与否的bool标志位的对组)
排序规则
可以指定set/multiset的排序规则,但是必须在插入前指定。指定排序规则利用仿函数的技术
对于自定义类型,必须指定出排序规则,不然没有意义
调整排序规则案例:
1 | //set从大到小 |
map/multimap容器(映射)
二者的头文件均为:#include<map>
Map的特性是,所有元素都会根据元素的键值自动排序。Map所有的元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值,map不允许两个元素有相同的键值。
我们可以通过map的迭代器改变map的键值吗?答案是不行,因为map的键值关系到map元素的排列规则,任意改变map键值将会严重破坏map组织。如果想要修改元素的实值,那么是可以的。
Map和list拥有相同的某些性质,当对它的容器元素进行新增操作或者删除操作时,操作之前的所有迭代器,在操作完成之后依然有效,当然被删除的那个元素的迭代器必然是个例外。
Multimap和map的操作类似,唯一区别multimap键值可重复。
Map和multimap都是以红黑树为底层实现机制。
map/multimap常用API
map构造函数
1 | map<T1, T2> mapTT;//map默认构造函数: |
map赋值操作
1 | map& operator=(const map &mp);//重载等号操作符 |
map大小操作
1 | size();//返回容器中元素的数目 |
map插入数据元素操作
1 | map.insert(...); //往容器插入元素,返回pair<iterator,bool> |
通过数组的方式插入值不推荐的原因
形如cout<<mapStu[3]<<enedl;这样的代码,若3是未指定的键值,会生成一个key为3,value为0的数据插入到容器中。因此此代码输出0。
使用情况:常用于查询已知键值的实值。
map删除操作
1 | clear();//删除所有元素 |
map查找操作
1 | find(key);//查找键key是否存在,若存在,返回该键的元素的迭代器;/若不存在,返回map.end(); |
map/multimap同样可设定排序规则,方式与set一致。
multimap案例
1 | //multimap中一个key对应多个键值的查询处理 |
unordered_map/unordered_multimap(无序映射)
用于存储键值对的容器,他使用哈希表来实现快速查找和插入,他是无序的
unordered_map:键值唯一unordered_multimap:键值不唯一
二者头文件: <unordered_map>
优势:
- 查询速度快,平均性能接近于常数时间O(1)
- 无论从查找、插入上来说,
unordered_map的效率都优于map
在大多数情况下,都应该使用
unordered_map而不是map。因为unordered_map的查找和插入操作的平均时间复杂度为**O(1),而map的这些操作的时间复杂度为O(logn)**。但是,如果你需要按顺序遍历键,或者需要键自动排序,那么应该使用map
API与map一致,参考map的api
map与unordered_map等的结构默认值均为0,如果直接访问一个没赋值的map[3],得到的结果直接是0
unordered_set/unordered_multiset(无序集合)
哈希表作为底层实现
API与set一致,参考set的api
由于哈希表需要对键计算哈希值,因此当视图定义
unordered_set<vector<int>>这种复合类型的时候,由于unordered_set并未针对vector提供特化版本,因此会导致该结构报错:the specified hash does not meet the Hash requirements,即没有满足C++标准库对哈希函数的要求
下面给出一个例子,提供unordered_set对vector的特化版本
1 |
|
hash_combine算法是一种用于将多个哈希值合并成一个新的哈希值的算法。它的思想是通过对每个输入哈希值进行位运算和异或操作,将它们组合成一个新的哈希值。这样做的目的是为了保持合并后的哈希值的唯一性和散列性。
seed ^= i + 0x9e3779b9 + (seed << 6) + (seed >> 2); 在这个算法中,使用0x9e3779b9作为调整因子可以帮助扩散输入哈希值的位分布,增加合并后的哈希值的随机性和散列性。它的选择是基于对黄金比例的一种经验性的观察和研究。
bitset容器
底层是一个固定大小的size_t数组
#include <bitset>
bitset比size_t数组更优秀的优点是:节约空间,节约时间,支持基本的位运算
声明一个100000次方位的bitset
1 | bitset<100000> s;//构造的时候默认初始化为全0 |
bitset常用函数
size_t count():计数1的个数size_t size(): 返回bitset可以容纳的位数bool any():检查是否存在1bool none():检查是否全0bitset& set():所有位设置为1bitset& set(size_t pos,bool value = true): 指定位置的位设置为给定值(默认为1)bitset& reset():所有位重置为0bitset& reset(size_t pos): 将指定位置的位重置为0bitset& flip():翻转所有位bitset& flip(size_t pos);:翻转指定位string to_string(): 返回bitset的字符串表示
位运算符在bitset中的符号操作
~:按位取反&:按位与|:按位或^:按位异或<</>>:左/右移==/!=:比较两个bitset是否相等。
容器适配器
适配器是一种将一个类的接口转换成另一个类的接口的设计模式
容器适配器是一种特殊的容器,它们使用底层容器来提供特定的接口和功能。这些容器适配器包装了底层容器,并通过限制其功能来提供不同的行为。
array容器(数组)
array是 C++ 中的标准模板库(STL)之一。它提供了一个固定大小的数组容器,类似于原生数组,但提供了更多的功能和安全性。你可以使用
array来存储和操作固定大小的元素集合。
1 | // 创建一个包含 5 个整数的数组 |
常用操作
size_t size() const:返回数组中元素的数量。bool empty() const:检查数组是否为空,如果为空则返回 true,否则返回 false。T& operator[](size_t pos):访问指定位置的元素,并返回对应的引用。const T& operator[](size_t pos) const:以只读方式访问指定位置的元素,并返回对应的常量引用。T& at(size_t pos):访问指定位置的元素,并返回对应的引用,如果越界则抛出异常。const T& at(size_t pos) const:以只读方式访问指定位置的元素,并返回对应的常量引用,如果越界则抛出异常。T& front():返回首个元素的引用。const T& front() const:以只读方式返回首个元素的常量引用。T& back():返回最后一个元素的引用。const T& back() const:以只读方式返回最后一个元素的常量引用。void fill(const T& value):用指定的值填充整个数组。void swap(array& other):交换两个数组的内容。T* data():返回指向数组内存中第一个元素的指针。const T* data() const:以只读方式返回指向数组内存中第一个元素的常量指针。
string容器(字符串)
string容器基本概念
C风格字符串(以空字符结尾的字符数组)太过复杂难于掌握,不适合大程序的开发,所以C++标准库定义了一种string类,定义在头文件**<string>**。
String和c风格字符串对比:
Char*是一个指针,String是一个类
string封装了char*,管理这个字符串,是一个char*型的容器。
String封装了很多实用的成员方法
查找find,拷贝copy,删除delete 替换replace,插入insert
不用考虑内存释放和越界
string管理char*所分配的内存。每一次string的复制,取值都由string类负责维护,不用担心复制越界和取值越界等。
string和unicode的关系
在C++中,
string类型的字符串实际上是一个字节序列,每个字符占用一个字节。然而,Unicode字符(包括中文字符)通常需要多个字节来表示。这就引出了一个问题:string如何存储和显示Unicode字符,特别是中文字符呢?实际上,
string可以存储任何字节序列,包括表示Unicode字符的字节序列。当你通过cin >> str输入中文字符时,这些字符会被转换为一个字节序列,然后存储在string中。当你打印这个string时,这个字节序列会根据编码来解码并显示在屏幕上在C++中,
std::string和std::wstring的行为会受到当前区域设置(locale查询)的影响。例如,当你使用std::cin或std::cout读取或输出字符串时,这些字符串会被转换为当前区域设置中指定的字符编码
string和wstring的区别
功能上的区别:
std::string主要用于存储单字节的字符(ASCII字符集),但是也可以用来保存UTF-8编码的字符。std::string内部是char单字节字符,String 数据类型中的每个字节都可以是从 0x00 到0xFF 的任意值。std::wstring主要用于UTF-16编码的字符。std::wstring内部是WCHAR宽字符。WString数据类型中的每个字可以是 0x0000 - 0xFFFF 之间的任意值。即使是ASCII字符,也要占用1个字UTF-16编码是2个字节或4个字节表示一个字符:对于那些需要4个字节的UTF-16字符(也被称为代理对),它们会被分成两个2字节的部分,然后分别存储在
std::wstring的两个连续的元素中
string容器常用操作
在C++标准库中,
std::string::npos是std::string类的一个静态成员常量,它代表了一个特殊值,通常表示一个不存在的位置或超出字符串长度的索引。当使用std::string的查找、替换等函数时,如果找不到指定的子串或字符,这些函数会返回std::string::npos。
string 构造函数
1 | string();//创建一个空的字符串 例如: string str; |
string基本赋值操作
1 | string& operator=(const char* s);//char*类型字符串 赋值给当前的字符串 |
string存取字符操作
1 | char& operator[](int n);//通过[]方式取字符 |
【注意】[]和at区别?
[]访问越界,直接挂掉;而at会抛出out_of_range异常
【注意】 为了修改string字符串的内容,下标操作符[]和at都会返回字符的引用。但当字符串的内存被重新分配之后,可能发生错误.
1 | string s = "abcdefg"; |
string拼接操作
1 | string& operator+=(const string& str);//重载+=操作符 |
string查找和替换
1 | int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找 |
p.s. find,rfind函数找不到就返回-1,但实际上是返回的无符号int类型,即4294967295,但依然只需要直接和-1做比较即可
string比较操作
1 | /* |
string子串
1 | string substr(int pos = 0, int n = npos) const;//返回由pos开始的n个字符组成的字符串 |
string插入和删除操作
1 | string& insert(int pos, const char* s); //插入字符串 |
string和c-style字符串转换
1 | //string 转 char* |
在c++中存在一个从const char*到string的隐式类型转换,却不存在从一个string对象到C_string的自动类型转换。对于string类型的字符串,可以通过c_str()函数返回string对象对应的C_string.(C_string就是const char*)
通常,程序员在整个程序中应坚持使用string类对象,直到必须将内容转化为char*时才将其转换为C_string.
大小写转换
- 大写转小写toupper
- 小写转大写tolower
string其他相关算法
各种类型转string
1 | _LIBCPP_FUNC_VIS string to_string(int __val); |
string转其他类型
1 | _LIBCPP_FUNC_VIS int stoi (const string& __str, size_t* __idx = nullptr, int __base = 10); |
string操作utf8案例
一个mac上基于utf8和string的用户多行输入代码(ctrl+d结束输入)
1 |
|
stack容器(栈)
头文件:#include<stack>
基于deque、list或vector实现
stack容器基本概念
stack是一种**先进后出(First In Last Out,FILO)**的数据结构,它只有一个出口,形式如图所示。stack容器允许新增元素,移除元素,取得栈顶元素,但是除了最顶端外,没有任何其他方法可以存取stack的其他元素。换言之,stack不允许有遍历行为。
有元素推入栈的操作称为:push,将元素推出stack的操作称为pop.
stack没有迭代器
Stack所有元素的进出都必须符合”先进后出”的条件,只有stack顶端的元素,才有机会被外界取用。Stack不提供遍历功能,也不提供迭代器。
stack常用API
stack构造函数
1 | stack<T> stkT;//stack采用模板类实现, stack对象的默认构造形式: |
stack赋值操作
1 | stack& operator=(const stack &stk);//重载等号操作符 |
stack数据存取操作
1 | push(elem);//向栈顶添加元素 |
stack大小操作
1 | empty();//判断堆栈是否为空 |
queue容器(队列)
头文件:#include<queue>
基于deque或list实现的容器适配器
queue容器基本概念
Queue是一种**先进先出(First In First Out,FIFO)**的数据结构,它有两个出口,queue容器允许从一端新增元素,从另一端移除元素。
queue没有迭代器
Queue所有元素的进出都必须符合”先进先出”的条件,只有queue的顶端元素,才有机会被外界取用。Queue不提供遍历功能,也不提供迭代器。
queue常用API
queue构造函数
1 | queue<T> queT;//queue采用模板类实现,queue对象的默认构造形式: |
queue存取、插入和删除操作
1 | push(elem);//往队尾添加元素 |
queue赋值操作
1 | queue& operator=(const queue &que);//重载等号操作符 |
queue大小操作
1 | empty();//判断队列是否为空 |
可以使用
queue::resize()方法来固定队列的大小。该方法可以将队列的大小调整为指定的大小.但是如果当前大小已经超过了要设置的大小,则无效
priority_queue容器(优先队列)
优先队列
底层为:在vector上实现的一个[[数据结构#堆|堆]]数据结构
头文件: #include <queue>
STL容器使用时机(重点)
| vector | deque | list | set | multiset | map | multimap | |
|---|---|---|---|---|---|---|---|
| 典型内存结构 | 单端数组 | 双端数组 | 双向链表 | 二叉树 | 二叉树 | 二叉树 | 二叉树 |
| 可随机存取 | 是 | 是 | 否 | 否 | 否 | 对key而言:不是 | 否 |
| 元素搜寻速度 | 慢 | 慢 | 非常慢 | 快 | 快 | 对key而言:快 | 对key而言:快 |
| 元素安插移除 | 尾端 | 头尾两端 | 任何位置 | - | - | - | - |
vector的使用场景:比如软件历史操作记录的存储,我们经常要查看历史记录,比如上一次的记录,上上次的记录,但却不会去删除记录,因为记录是事实的描述。
deque的使用场景:比如排队购票系统,对排队者的存储可以采用deque,支持头端的快速移除,尾端的快速添加。如果采用vector,则头端移除时,会移动大量的数据,速度慢。
vector与deque的比较:
1 | 1. `vector.at()比deque.at()效率高,比如vector.at(0)是固定的,deque的开始位置 却是不固定的。` |
- list的使用场景:比如公交车乘客的存储,随时可能有乘客下车,支持频繁的不确实位置元素的移除插入。
- set的使用场景:比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的顺序排列。
- map的使用场景:比如按ID号存储十万个用户,想要快速要通过ID查找对应的用户。二叉树的查找效率,这时就体现出来了。如果是vector容器,最坏的情况下可能要遍历完整个容器才能找到该用户。
常用算法
算法概述
[[算法]]主要是由头文件<algorithm> <functional> <numeric>组成。
<algorithm>是所有STL头文件中最大的一个,其中常用的功能涉及到比较,交换,查找,遍历,复制,修改,反转,排序,合并等…
<numeric>体积很小,只包括在几个序列容器上进行的简单运算的模板函数.
<functional> 定义了一些模板类,用以声明函数对象。
全排列算法
next_permutation函数
只要容器支持双向迭代器(Bidirectional Iterator)都支持该函数
但对于
std::set和std::map,std::next_permutation函数不适用,因为它们是有序容器,没有意义
1 | template<class BidirectionalIterator> |
next_permutation函数接受两个迭代器参数,表示要生成排列的范围。它会将范围内的元素重新排列为下一个字典序更大的排列,并返回true。如果已经是最大的排列,则将范围内的元素重新排列为最小的排列,并返回false。next_permutation会确保每个生成的排列都是唯一的。
以下是一个示例,展示如何使用 next_permutation 函数生成排列:
1 |
|
相临去重算法
iterator unique(iterator it_1,iterator it_2,bool MyFunc);
iterator unique(iterator it_1,iterator it_2
参数列表中前两个参数都是迭代器,表示对区间[it_1,it_2)内进行去重。(区间为左闭右开,很多STL函数都是这样)
功能描述:移除给定范围内相邻的重复元素,只保留一个副本(注意一定是相邻元素,且只保留一个)。本质上没有将重复的元素删除,而是通过把不重复的元素移到前面来,导致重复的元素放到数组的最后面了
只可用于支持随机访问迭代器的容器
std::vectorstd::dequestd::arraystd::string
可以理解实现为:
1 | template <class ForwardIterator> |
unique函数并不会真正删除元素,而是用替代的方法来实现。容器的大小是不会变得,那么我们如果想真正删除元素我们该怎么办呢?我们想想,unique函数的返回值是指向最后一个不相等的元素的下一个元素的迭代器,我们只要接收这个返回值,利用erase函数把后面元素都给直接删除即可。
整个流程是: sort->unique->erase
1 | //排序后 |
如果需要去重的同时复制序列:可以参考 unique_copy
常用遍历算法
- for_each—遍历
- transform—搬运(目标容器要有容量)
1 | /* |
【注意】 : transform 不会给目标容器分配内存,所以需要我们提前分配好内存
【注意】:map和for_each的使用如下:
1 | void fun(map<int,int>::reference i)//reference引用 |
常用查找算法
- find—查找
- find_if—按条件查找
- adjacent_find—查找相邻的重复元素
- binary_search—二分查找法(在无序序列中不可使用)
- count—统计元素出现的次数
- count_if—按条件进行统计
- upper_bound — 针对有序容器,查找第一个大于给定值的元素的迭代器
1 | /* |
常用排序算法
- merge—合并(合并两个有序序列到一个有预留空间的目标容器)
- sort—排序(底层通常是[[算法#快速排序|快速排序]]或[[算法#归并排序|归并排序]])
- random_shuffle—洗牌
- reverse—反转
1 | /* |
常用拷贝和替换算法
- copy—拷贝(也可用于打印)
- replace—替换
- replace_if—按条件替换
- swap—交换
1 | /* |
常用算数生成算法
- accumulate—计算容器元素累积总和
- fill—向容器添加元素
1 | /* |
常用集合算法
- set_intersection—求两个set集合的交集
- set_union—求两个set集合的并集
- set_difference—求两个set集合的差集
注意:上面都必须是针对两个有序序列
1 | /* |
堆相关算法
可以先参考[[数据结构#堆|堆的知识点]]
1 | void make_heap(RandomAccessIterator first, RandomAccessIterator last) |
案例:
1 | //24个人参加。比赛共三轮,前两轮为淘汰赛,第三轮为决赛。 |
stl包含的其他标准头文件
numeric头文件
#include <numeric>
属于stl的一部分,提供了一些用于数值计算的函数和算法,用于处理数值型数据
常用函数
- T accumulate(InputIterator first, InputIterator last, T init) 用于计算指定范围内元素的累加值,从init开始累加
- T inner_product(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2, T init) 计算两个序列的内积(即返回两序列对应位置相乘,最后相加的总和)
- void iota(ForwardIterator first, ForwardIterator last, T value) 生成一个序列,从指定的起始值开始,递增一个固定的步长
- OutputIterator partial_sum(InputIterator first, InputIterator last, OutputIterator result) 计算指定范围内元素的部分和
- OutputIterator adjacent_difference(InputIterator first, InputIterator last, OutputIterator result) 计算相邻元素之间的差值,返回一个序列
1 | //adjacent_difference使用案例参考 |
numeric头文件中还包含了一些模板类
- valarray:用于表示和处理数值数组。
- complex:用于处理复数运算。















