C++ STL demon
参考:https://www.w3cschool.cn/cpp/cpp-stl-tutorial.html
在前面的章节中,我们已经学习了 C++ 模板的概念。C++ STL(标准模板库)是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈。
C++ 标准模板库的核心包括以下三个组件:
这三个组件都带有丰富的预定义函数,帮助我们通过简单的方式处理复杂的任务。
下面的程序演示了向量容器(一个 C++ 标准的模板),它与数组十分相似,唯一不同的是,向量在需要扩展大小的时候,会自动处理它自己的存储需求:
1 | #include <iostream> |
当上面的代码被编译和执行时,它会产生下列结果:
1 | vector size = 0 |
关于上面实例中所使用的各种函数,有几点要注意:
- push_back( ) 成员函数在向量的末尾插入值,如果有必要会扩展向量的大小。
- size( ) 函数显示向量的大小。
- begin( ) 函数返回一个指向向量开头的迭代器。
- end( ) 函数返回一个指向向量末尾的迭代器。
STL 讲解
参考:https://blog.csdn.net/u010183728/article/details/81913729
https://www.cnblogs.com/yifengs/p/15189874.html
https://blog.csdn.net/boke_fengwei/article/details/90647423
http://c.biancheng.net/view/6860.html
STL是什么
STL(Standard Template Library),即标准模板库,是一个具有工业强度的,高效的C++ 程序库。该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。为广大C++ 程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。
- STL的一个重要特点是数据结构和算法的分离。egg: 由于STL的sort()函数是完全通用的,你可以用它来操作几乎任何数据集合,包括链表,容器和数组。
- STL另一个重要特性是它不是面向对象的。为了具有足够通用性,STL主要依赖于模板而不是封装,继承和虚函数(多态性)——OOP的三个要素。你在STL中找不到任何明显的类继承关系。这好像是一种倒退,但这正好是使得STL的组件具有广泛通用性的底层特征。另外,由于STL是基于模板,内联函数的使用使得生成的代码短小高效。
STL的内容
STL是由六大组件构成的:容器,迭代器,算法,仿函数,适配器,分配器。
容器
容器(Container),是一种数据结构,如list,vector,和deques ,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。
STL中的容器有队列容器和关联容器,容器适配器(congtainer adapters:stack,queue,priority queue),位集(bit_set),串包(string_package)等等。
序列式容器(Sequence containers)
每个元素都有固定位置--取决于插入时机和地点,和元素值无关,vector、deque、list;
vector
vector是一个动态数组,可以随机存取元素(用索引直接存取), 数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时.
- 特点:
-
(1) 一个动态分配的数组(当数组空间内存不足时,都会执行:分配新空间-复制元素-释放原空间);
-
(2) 当删除元素时,不会释放限制的空间,所以向量容器的容量(capacity)大于向量容器的大小(size);
-
(3) 对于删除或插入操作,执行效率不高,越靠后插入或删除执行效率越高;
-
(4) 高效的随机访问的容器。
- vector的常用操作
- vector构造函数
1 |
|
常用:egg vector
- vector大小操作:
1 | size();//返回容器中元素的个数 |
- vector数据存取操作
1 | at(int idx); //返回索引idx所指的数据,如果idx越界,抛出out_of_range异常。 |
- vector插入和删除操作
1 |
|
- 巧用swap,收缩删除内存空间
1 | #define _CRT_SECURE_NO_WARNINGS |
运行结果:
vector
(v).swap(v); 拷贝构造一个临时对象 其分配了v.size()个元素的内存空间,即capacity为v.size()
vector
().swap(v); // 可以清除v的内存空间
- 关于vector的size和capacity的区别:
对于抽象的问题,只要我们把它们同我们的生活实际相结合,将问题具象化,自然就会很好的理解。
就拿我们的办公室举例,假设,我们部门的办公地点位于公司大楼的六楼。在我们的办公室里面,放置了100套办公桌椅(工位),公司说按照一个萝卜一个坑来算,你们部门最多只能招这么多人,那么,这时我们可以说,我们部门的容量(即capacity)就是100人,如果我们部门是公司刚成立的部门,正处于发展壮大的阶段,目前只有40为员工,也就是说,办公室里只坐了40个人,另外60个工位是空着的,那么,我们可以说,我们部门当前的大小(即size)是40人。这实际上就是size和capacity的区别。类比到vector,size和capacity的概念自然就很清楚了。
deque容器
- 概述:
deque 是 double-ended queue的缩写,又称双端队列容器。前面小节中,我们已经系统学习了 vector 容器,值得一提的是,deque 容器和 vecotr 容器有很多相似之处,比如:
- deque 容器也擅长在序列尾部添加或删除元素(时间复杂度为O(1)),而不擅长在序列中间添加或删除元素。
- deque 容器也可以根据需要修改自身的容量和大小。
和 vector 不同的是:
- deque 还擅长在序列头部添加或删除元素,所耗费的时间复杂度也为常数阶O(1)。
- 并且更重要的一点是,deque 容器中存储元素并不能保证所有元素都存储到连续的内存空间中。
当需要向序列两端频繁的添加或删除元素时,应首选 deque 容器。
使用deque 容器,代码中需要包含下面两行代码:
1 | #include <deque> |
- 创建deque容器的几种方式:
创建 deque 容器,根据不同的实际场景,可选择使用如下几种方式。
- deque
d; //创建一个没有任何元素的空 deque 容器。 - deque
d(10); //deque d(10); - deque
d(10, 5); //创建一个具有 10 个元素的 deque 容器,并为每个元素都指定初始值5。 - 在已有 deque 容器的情况下,可以通过拷贝该容器创建一个新的 deque 容器,例如:
1 | deque<int> d1(5); |
注意,采用此方式,必须保证新旧容器存储的元素类型一致。
- 通过拷贝其他类型容器中指定区域内的元素(也可以是普通数组),可以创建一个新容器,例如:
1 |
|
函数成员 | 函数功能 |
---|---|
begin() | 返回指向容器中第一个元素的迭代器。 |
end() | 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用 |
size() | 返回实际元素个数 |
max_size() | 返回容器所能容纳元素个数的最大值。这通常是一个很大的值,我们很少会用到这个函数。 |
resize() | 改变实际元素的个数 |
empty() | 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。 |
shrink _to_fit() | 将内存减少到等于当前元素实际所使用的大小。 |
at() | 使用经过边界检查的索引访问元素。 |
front() | 返回第一个元素的引用。 |
back() | 返回最后一个元素的引用。 |
push_back() | 在序列的尾部添加一个元素。 |
push_front() | 在序列的头部添加一个元素。 |
pop_back() | 移除容器尾部的元素。 |
pop_front() | 移除容器头部的元素。 |
swap() | 交换两个容器的所有元素。 |
和 vector 相比,额外增加了实现在容器头部添加和删除元素的成员函数,同时删除了 capacity()、reserve() 和 data() 成员函数。
List
双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针。
- list构造函数
1 | list<int> L0; // 空链表 |
循环输出链表所有值:
1 | void showlist(list<int> a) |
- assign()分配值,有两个重载
1 |
|
- front()返回第一个元素的引用
1 | int nRet = list1.front() // nRet = 1 |
- back()返回最后一元素的引用
1 | int nRet = list1.back() // nRet = 3 |
- begin()返回第一个元素的指针(iterator)
1 | it = list1.begin(); // *it = 1 |
- end()返回最后一个元素的下一位置的指针(list为空时end()=begin())
1 | it = list1.end(); |
- push_back()增加一元素到链表尾
1 | list1.push_back(4) // list1(1,2,3,4) |
- push_front()增加一元素到链表头
1 | list1.push_front(4) // list1(4,1,2,3) |
- pop_back()删除链表尾的一个元素
1 | list1.pop_back() // list1(1,2) |
- pop_front()删除链表头的一元素
1 | list1.pop_front() // list1(2,3) |
- empty()判断是否链表为空
1 | bool bRet = L1.empty(); //若L1为空,bRet = true,否则bRet = false。 |
- size()返回链表中元素个数
1 | list<int>::size_type nRet = list1.size(); // nRet = 3 |
关联式容器(Associated containers)
概述: 元素位置取决于特定的排序准则,和插入顺序无关,set、multiset、map、multimap等。map/multimap,set/multiset都为c++中的标准容器,它们的底层都是用红黑树实现的,因此在进行查询,修改,删除等操作上具有很高的效率,可以达到O(logN)。
Set/Multiset
概述
- 内部的元素依据其值自动排序,Set内的相同数值的元素只能出现一次,Multisets内可包含多个数值相同的元素,内部由二叉树实现,便于查找。
和所有的标准关联容器类似,sets和multisets通常以平衡二叉树完成。
自动排序的主要优点在于使二叉树搜寻元素具有良好的性能,在其搜索函数算法具有对数复杂度。但是自动排序也造成了一个限制,不能直接改变元素值,因为这样会打乱原有的顺序,要改变元素的值,必须先删除旧元素,再插入新元素。所以sets和multisets具有以下特点:
- 不提供直接用来存取元素的任何操作元素
- 通过迭代器进行元素的存取。
- set demon:
1 | #include <iostream> |
- multiset demon
1 |
|
操作函数
- 构造
函数 | 功能 |
---|---|
set c | 产生一个空的set/multiset,不含任何元素 |
set c(op) | 以op为排序准则,产生一个空的set/multiset |
set |
产生一个set,以(operator <)为排序准则 |
set<Elem,0p> | 产生一个set,以op为排序准则 |
- 拷贝
函数 | 功能 |
---|---|
set c1(c2) | 产生某个set/multiset的副本,所有元素都被拷贝 |
set c(beg,end) | 以区间[beg,end)内的所有元素产生一个set/multiset |
set c(beg,end,op) | 以op为排序准则,区间[beg,end)内的元素产生一个set/multiset |
- 销毁
c.~set(): 销毁所有元素,释放内存
- 非变动性操作
函数 | 功能 |
---|---|
c.size() | 返回当前的元素数量 |
c.max_size() | 返回能容纳的元素最大数量 |
c.empty () | 判断大小是否为零,等同于0 == size(),效率更高 |
c1 == c2 | 判断c1是否等于c2 |
c1 != c2 | 判断c1是否不等于c2(等同于!(c1==c2)) |
- 该数据结构自带的特殊的搜索函数
函数 | 功能 |
---|---|
count (elem) | 返回元素值为elem的个数 |
find(elem) | 返回元素值为elem的第一个元素,如果没有返回end() |
lower _bound(elem) | 返回元素值为elem的第一个可安插位置,也就是元素值 >= elem的第一个元素位置
upper _bound (elem) |返回元素值为elem的最后一个可安插位置,也就是元素值 > elem 的第一个元素位置
equal_range (elem)| 返回elem可安插的第一个位置和最后一个位置,也就是元素值==elem的区间
- 赋值
函数 | 功能
—|---
c1 = c2|将c2的元素全部给c1
c1.swap(c2) |将c1和c2 的元素互换
swap(c1,c2)|同上,全局函数
- 迭代器相关函数
sets和multisets的迭代器是双向迭代器,对迭代器操作而言,所有的元素都被视为常数,可以确保你不会人为改变元素值,从而打乱既定顺序,所以无法调用变动性算法,如remove()。
函数 | 功能 |
---|---|
c.begin() | 返回一个随机存取迭代器,指向第一个元素 |
c.end() | 返回一个随机存取迭代器,指向最后一个元素的下一个位置 |
- 安插和删除元素
必须保证参数有效,迭代器必须指向有效位置,序列起点不能位于终点之后,不能从空容器删除元素。
函数 | 功能 |
---|---|
c.insert(elem) | 插入一个elem副本,返回新元素位置,无论插入成功与否。 |
c.insert(pos,elem) | 安插一个elem元素副本,返回新元素位置,pos为收索起点,提升插入速度。 |
c.insert(beg,end) | 将区间[beg,end)所有的元素安插到c,无返回值。 |
c.erase(elem) | 删除与elem相等的所有元素,返回被移除的元素个数。 |
c.erase(pos) | 移除迭代器pos所指位置元素,无返回值。 |
c.erase(beg,end) | 移除区间[beg,end)所有元素,无返回值。 |
c.clear() | 移除所有元素,将容器清空 |
Map/Multimap
Map的元素是成对的键值/实值,内部的元素依据其值自动排序,Map内的相同数值的元素只能出现一次,Multimap内可包含多个数值相同的元素,内部由二叉树实现,便于查找。Map不允许key值重复,而Multimap允许。
demon
1 |
|
它的make_pair(k,v)方法便是用来生成pair类型的变量的。
Map.insert(make_pair(“fjt”, “香港科技大学计算机科学全奖博士”));。
如果直接使用Map.insert(“fjt”,“香港科技大学计算机科学全奖博士”)程序无法通过编译。
STL迭代器
Iterator(迭代器)模式又称Cursor(游标)模式。Iterator模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的方法)访问聚合对象中的各个元素。
迭代器的作用:能够让迭代器与算法不干扰的相互发展,最后又能无间隙的粘合起来,重载了*,++,==,!=,=运算符。用以操作复杂的数据结构,容器提供迭代器,算法使用迭代器;常见的一些迭代器类型:==iterator、const_iterator、reverse_iterator和const_reverse_iterator==.
迭代器使用方法:
1 | 容器::iterator iter; |
算法
函数库对数据类型的选择对其可重用性起着至关重要的作用。举例来说,一个求方根的函数,在使用浮点数作为其参数类型的情况下的可重用性肯定比使用整型作为它的参数类性要高。而C++通过模板的机制允许推迟对某些类型的选择,直到真正想使用模板或者说对模板进行特化的时候,STL就利用了这一点提供了相当多的有用算法。它是在一个有效的框架中完成这些算法的——你可以将所有的类型划分为少数的几类,然后就可以在模版的参数中使用一种类型替换掉同一种类中的其他类型。
STL提供了大约100个实现算法的模版函数,比如算法for_each将为指定序列中的每一个元素调用指定的函数,stable_sort以你所指定的规则对序列进行稳定性排序等等。只要我们熟悉了STL之后,许多代码可以被大大的化简,只需要通过调用一两个算法模板,就可以完成所需要的功能并大大地提升效率。
算法部分主要由头文件
-
是所有STL头文件中最大的一个(尽管它很好理解),它是由一大堆模版函数组成的,可以认为每个函数在很大程度上都是独立的,其中常用到的功能范围涉及到比较、交换、查找、遍历操作、复制、修改、移除、反转、排序、合并等等。 -
体积很小,只包括几个在序列上面进行简单数学运算的模板函数,包括加法和乘法在序列上的一些操作。 -
中则定义了一些模板类,用以声明函数对象。
STL中算法大致分为四类:
- 非可变序列算法:指不直接修改其所操作的容器内容的算法。
- 可变序列算法:指可以修改它们所操作的容器内容的算法。
- 排序算法:对序列进行排序和合并的算法、搜索算法以及有序序列上的集合操作。
- 数值算法:对容器内容进行数值计算。
查找算法:判断容器中是否包含某个值
-
adjacent_find:在iterator对标识元素范围内,查找一对相邻重复元素,找到则返回指向这对元素的第一个元素的Forward Iterator。否则返回last。重载版本使用输入的二元操作符代替相等的判断。
-
binary_search: 在有序序列中查找value,找到返回true。
-
count: 利用等于操作符,把标志范围内的元素与输入值比较,返回相等元素个数。
-
count_if: 利用输入的操作符,对标志范围内的元素进行操作,返回结果为true的个数。
-
equal_range: 功能类似equal,返回一对iterator,第一个表示lower_bound,第二个表示upper_bound。
-
find: 利用底层元素的等于操作符,对指定范围内的元素与输入值进行比较。当匹配时,结束搜索,返回该元素的一个InputIterator。
-
search: 给出两个范围,返回一个ForwardIterator,查找成功指向第一个范围内第一次出现子序列(第二个范围)的位 置,查找失败指向last1。重载版本使用自定义的比较操作。
-
search_n: 在指定范围内查找val出现n次的子序列。重载版本
使用自定义的比较操作。
排序和通用算法:提供元素排序策略
-
inplace_merge: 合并两个有序序列,结果序列覆盖两端范围
-
merge: 合并两个有序序列,存放到另一个序列。重载版本使用自定义的比较。
-
nth_element: 将范围内的序列重新排序,使所有小于第n个元素的元素都出现在它前面,而大于它的都出现在后面。重载版本使用自定义的比较操作。
-
partial_sort: 对序列做部分排序,被排序元素个数正好可以被放到范围内。重载版本使用自定义的比较操作。
-
partial_sort_copy: 与partial_sort类似,不过将经过排序的序列复制到另一个容器。
-
partition: 对指定范围内元素重新排序,使用输入的函数,把结果为true的元素放在结果为false的元素之前。
-
random_shuffle: 对指定范围内的元素随机调整次序。重载版本输入一个随机数产生操作。
-
reverse: 将指定范围内元素重新反序排序。
-
reverse_copy: 与reverse类似,不过将结果写入另一个容器。
-
rotate: 将指定范围内元素移到容器末尾,由middle指向的元素成为容器第一个元素。
-
rotate_copy: 与rotate类似,不过将结果写入另一个容器。
-
sort: 以升序重新排列指定范围内的元素。重载版本使用自定义的比较操作。
-
stable_sort: 与sort类似,不过保留相等元素之间的顺序关系。
-
stable_partition: 与partition类似,不过不保证保留容器中的相对顺序。
删除和替换算法
-
copy: 复制序列
-
copy_backward: 与copy相同,不过元素是以相反顺序被拷贝。
-
iter_swap: 交换两个ForwardIterator的值。
-
remove: 删除指定范围内所有等于指定元素的元素。注意,该函数不是真正删除函数。内置函数不适合使用remove和 remove_if函数:
-
remove_copy: 将所有不匹配元素复制到一个制定容器,返回Output Iterator指向被拷贝的末元素的下一个位置。
-
remove_if: 删除指定范围内输入操作结果为true的所有元素。
-
remove_copy_if: 将所有不匹配元素拷贝到一个指定容器。
-
replace: 将指定范围内所有等于vold的元素都用vnew代替。
-
replace_copy: 与replace类似,不过将结果写入另一个容器。
-
replace_if: 将指定范围内所有操作结果为true的元素用新值代替。
-
replace_copy_if: 与replace_if,不过将结果写入另一个容器。
swap: 交换存储在两个对象中的值。 -
swap_range: 将指定范围内的元素与另一个序列元素值进行交换。
-
unique: 清除序列中重复元素,和remove类似,它也不能真正删除元素。重载版本使用自定义比较操作。
-
unique_copy: 与unique类似,不过把结果输出到另一个容器。
排列组合算法:提供计算给定集合按一定顺序的所有可能排列组合
- next_permutation: 取出当前范围内的排列,并重新排序为下一个排列。重载版本使用自定义的比较操作。
- prev_permutation: 取出指定范围内的序列并将它重新排序为上一个序列。如果不存在上一个序列则返回false。重载版本使用自定义的比较操作。
算术算法(4个)
- accumulate:iterator对标识的序列段元素之和,加到一个由val指定的初始值上。重载版本不再做加法,而是传进来的二元操作符被应用到元素上。
- partial_sum:创建一个新序列,其中每个元素值代表指定范围内该位置前所有元素之和。重载版本使用自定义操作代替加法。
- inner_product: 对两个序列做内积(对应元素相乘,再求和)并将内积加到一个输入的初始值上。重载版本使用用户定义的操作。
- adjacent_difference:创建一个新序列,新序列中每个新值代表当前元素与上一个元素的差。重载版本用指定二元操作计算相邻元素的差。
生成和异变算法(6个)
关系算法(8个)
集合算法(4个)
堆算法(4个)
仿函数
概述:
仿函数(functor),就是使一个类的使用看上去象一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。有些功能的的代码,会在不同的成员函数中用到,想复用这些代码。
- 公共的函数: 这是一个解决方法,不过函数用到的一些变量,就可能成为公共的全局变量,再说为了复用这么一片代码,就要单立出一个函数,也不是很好维护。
- 仿函数:写一个简单类,除了那些维护一个类的成员函数外,就只是实现一个operator(),在类实例化时,就将要用的,非参数的元素传入类中。求–函数指针无法和STL其他组件搭配,产生更灵活变化。
自定义仿函数:
1 | #include <iostream> |
STL内建的仿函数
要使用STL内建的仿函数,必须包含
算术类仿函数
1 | #include <iostream> |
关系运算类仿函数
从大到小排序:
1 | #include <iostream> |