[C++ 系列] 88. STL进阶及简单总结

1. STL的本质

通过前面的学习以及使用,我们对 STL 已经有了一定的认识。

通俗说:STLStandard Template Library (标准模板库),是高效的 C++ 程序库,其采用泛型编程思想对常见数据结构(顺序表,链表,栈和队列,堆,二叉树,哈希)和算法(查找、排序、集合、数值运算…)等进行封装,里面处处体现着泛型编程程序设计思想以及设计模式,已被集成到 C++ 标准程序库中。具体说:STL 中包含了容器、适配器、算法、迭代器、仿函数以及空间配置器。

STL 设计理念:追求代码高复用性以及运行速度的高效率,在实现时使用了许多技术,因此熟悉 STL 不仅对我们正常使用有很大帮助,而且对自己的知识也有一定的提高。

2. STL的六大组件

2.1 容器

STL 中的容器,可以划分为两大类:序列式容器和关联式容器。
在这里插入图片描述
必备技能:

  1. 熟悉每个容器的常用接口以及帮助文档查阅,并能熟练使用,建议再刷题以及写项目时多多应用,熟能生巧
  2. 熟悉每个容易的底层结构、实现原理以及应用场景,比如:红黑树、哈希桶
  3. 熟悉容器之间的区别:比如vector和list区别?map和set区别?map和unordered_map的区别?

2.2 算法

算法:问题的求解步骤,以有限的步骤,解决数学或逻辑中的问题。 STL 中的算法主要分为两大类:与数据结构相关算法(容器中的成员函数)和通用算法(与数据结构不相干)。

STL 中通用算法总共有70多个,为我们提供了一个 algorithm 库,主要包含:排序,查找,排列组合,数据移动,拷贝,删除,比较组合,运算等。 以下只列出了部分常用的算法:

算法名称
算法功能
accumulate 元素统计
binary_search 二分查找
copy 拷贝
copy_backward 逆向拷贝
copy_n 拷贝n个元素
count 计数
count_if 在特定条件下计数
equal 判断两个区间相等与否
fill 填充元素
fill_n 填充元素n次
find 循环查找
find_if 循环查找符合特定条件元素
find_end 查找某个子序列的最后一次出现点
find_first_of 查找某个元素首次出现点
for_each 对区间内的每隔一元素实行某种操作
is_heap 判断某区间是否为一个heap
is_sorted 判断某区间是否已排序
lexicographical_compare 以字典顺序进行比较
max 获取最大值
max_element 最大值所在位置
merge 合并两个序列
min 获取最小值
min_element 最小值所在位置
next_permutation 获取下一个排列组合
pre_permutation 获取前一个排列组合
partial_sort 局部排序
partial_sum 局部求和
partition 分割
remove 删除某类元素
remove_copy 删除某类元素并将结果拷贝到另一个容器中
remove_if 有条件的删除某类元素
replace 替换某类元素
replace_if 有条件的替换
reverse 反转序列
sort 排序(不稳定)
stable_partition 分割并保持元素的相对次序
stable_sort 分割并保持相等元素的相对位置(稳定排序算法)
unique 取出重复性元素
make_heap 创建堆
push_heap 堆插入
pop_heap 堆删除
sort_heap 堆排序

在刷题过程中,sort、reverse、unique、remove、max、min、find 等函数用着是相当的舒服,甚至有些黑科技如 next_permutation、pre_permutation 排列组合函数能达到秒杀题目的效果。但在 STL 的算法库中这些仅仅是九牛一毛罢了,顶多就是比较常见而已。

就问一个简单的问题,STL 中最为常见的 sort 函数底层是怎么实现的?可能一部分人知道他是个混合排序,由快排和插排组成,但是再精确点呢?为什么我们写的排序没有 sort 快呢?这些都是 STL 算法封装的魅力所在,高效且优雅。

必备技能:

  1. 熟悉常用算法的作用,并熟练使用
  2. 熟悉房间算法时间复杂&空间复杂度求解方式

2.3 迭代器

2.3.1 什么是迭代器

迭代器是一种设计模式,让用户通过特定的接口访问容器的数据,不需要了解容器内部的底层数据结构。

C++ 中迭代器本质:是一个指针,让该指针按照具体的结构去操作容器中的数据。

2.3.2 为什么需要迭代器

通过前面算法的学习了解到:STL 中算法分为容器相关联与通用算法。所谓通用算法,即与具体的数据结构无关,比如:

参见代码如下:

template<class InputIterator, class T>
InputIterator find ( InputIterator first, InputIterator last, const T& value )
{
    for ( ;first!=last; first++) 
        if ( *first==value )
            break;
    
    return first;
}

find 算法在查找时,与具体的数据结构无关,只要给出待查找数据集合的范围,find 就可在该范围中查找,找到返回该元素在区间中的位置,否则返回 end

来自大佬博文的图片: LLZK_ :STL迭代器的"特性萃取机"-----Traits

在这里插入图片描述
在这里也能够发现,上面的 InputIterator 即为只读迭代器,在这里做个引子,后面会详细介绍。

问题:对于 vector、list、deque、map、unordered_set 等容器,其底层数据结构均不相同,那 find 算法是怎么统一向后遍历呢?

  • 只要是容器的迭代器,就一定有 ++ 算法,* 解引用算法,这是由它底层迭代器封装方式决定的。
2.3.3 迭代器应该由谁负责提供

每个容器的底层结构都不同,为了降低算法使用容器时的复杂度,底层结构应该对于算法透明,迭代器就充当了算法与容器之间的转接层,因此:每个容器的迭代器应该由容器设计者负责提供,然后容器按照约定给出统一的接口即可。

参见代码如下:

// vector中:
typedef T* iterator;
iterator begin();
iterator end();
find(v.beign(), v.end(), 5);
 
// list中
typedef list_iterator<T, T&, T*> iterator;
iterator begin();
iterator end();
find(l.begin(), l.end(), 5);
2.3.4 迭代器实现原理

容器底层结构不同,导致其实现原理不同,容器迭代器的设计,必须结合具体容器的底层数据结构。比如:

  1. vector
  • 因为 vector 底层结构为一段连续空间,迭代器前后移动时比较容易实现,因此 vector 的迭代器实际是对原生态指针的封装,即:typedef T* iterator
  1. list
  • list 底层结构为带头结点的双向循环链表,迭代器在移动时,只能按照链表的结构前后依次移动,因此链表的迭代器需要对原生态的指针进行封装,因为当对迭代器 ++ 时,应该通过节点中的 next 指针域找到下一个节点。可参考:[C++系列] 56. list深度剖析及模拟实现

如果迭代器不能直接使用原生态指针操作底层数据时,必须要对指针进行封装,在封装时需要提供以下方法:

  1. 迭代器能够像指针一样方式进行使用:重载 reference operator*() / pointer operator->()
  2. 能够让迭代器移动
  • 向后移动:self& operator++() / self operator++(int)
  • 向前移动:self& operator--() / self operator--(int) (注意:有些容器不能向前移动,比如 forward_list)
  1. 支持比较-因为在遍历时需要知道是否移动到区间的末尾 bool operator!=(const self& it)const / bool operator==(const self& it)const
2.3.5 迭代器与类的融合
  1. 定义迭代器类
  2. 在容器类中统一迭代器名字

参见代码如下:

// 比如list:
template <class T, class Alloc = alloc>
class list 
{
    // ...
    typedef __list_iterator<T, T&, T*>             iterator;
    // ...
};
  1. 在容器类中添加获取迭代器范围的接口:

参见代码如下:

template <class T, class Alloc = alloc>
class list 
{
    // ...
    iterator begin(){ return (link_type)((*node).next);}
    iterator end(){ return node;}
    // ...
};
2.3.6 反向迭代器

反向迭代器:正向迭代器的适配器,即正向迭代器 ++end 方向移动,--begin 方向移动,而反向迭代器 ++ 则往 begin 方向移动,-- 则向 end 方向移动。

可参考:[C++系列] 56. list深度剖析及模拟实现list 的反向迭代器模拟实现。

2.3.7 迭代器萃取(了解)

可参考侯捷大佬的:《 STL 源码剖析》
可参考来自大佬博文: LLZK_ :STL迭代器的"特性萃取机"-----Traits 但是博文需要有前置知识,是默认了解类型萃取 Traits 原理机制。在此作简单介绍:

参见代码如下:

#include <iostream>
#include <vector>
#include <typeinfo>
using namespace std;

class A{};

// 希望
template <class T>
struct unknown_class
{
	// 待萃取类型
	typedef T return_type;
};

/****************************************/

// 容器萃取机
template <class unknown_class>
struct unknown_class_traits
{
	// 在此必须加上typename
	// 因为unknown_class不知道其为类型、函数、变量
	// 更不知道return_value为类型、函数、变量
	// 所以显示加上typename否则编译器不知道后面为类型
	typedef typename unknown_class::return_type return_type;

	// 萃取vector传入value_type即可
	// typedef typename unknown_class::value_type return_type;
};

// 模板与模板函数
// 加入typename显示声明其为类型
template <class unknown_class>
typename unknown_class_traits<unknown_class>::return_type
func(unknown_class u)
{
	typedef typename unknown_class_traits<unknown_class>::return_type return_type;
	return return_type();
}

int main()
{
	// 需要函数将unknown_class中的类型萃取出来定义变量
	unknown_class<A> a;
	// unknown_class<int> a;
	// vector<int *> a;

	// 若b的类型为能与a相同那么类型萃取成功
	auto b = func(a);

	cout << typeid(b).name() << endl;
	return 0;
}

2.4 适配器

适配器:又接着配接器,是一种设计模式,简单的说:需要的东西就在眼前,但是由于接口不对而无法使用,需要对其接口进行转化以方便使用。即:将一个类的接口转换成用户希望的另一个类的接口,使原本接口不兼容的类可以一起工作。

STL 中适配器总共有三种类型:

  • 容器适配器 stackqueue
    stack 的特性是后进先出,queue 的特性为先进先出,该种特性 deque 的接口完全满足,因此 stackqueue 在底层将 deque 容器中的接口进行了封装。

参见代码如下:

template < class T, class Container = deque<T> >
class stack { ... };
 
template < class T, class Container = deque<T> > 
class queue { ... };

2.5 仿函数

仿函数:一种具有函数特征的对象,调用者可以像函数一样使用该对象 ,为了能够“行为类似函数”,该对象所在类必须自定义函数调用运算符 operator(),重载该运算符后,就可在仿函数对象的后面加上一对小括号,以此调用仿函数所定义的operator()操作,就其行为而言,“仿函数”一次更切贴。

仿函数一般配合算法,作用就是:提高算法的灵活性。

参见代码如下:

#include <vector>
#include <algorithm>
class Mul2
{
public:
    void operator()(int& data)
    { data <<= 1;}
};

void mul2(int & val) 
{
	val *= 2;
}

class Mod3
{
public:
    bool operator()(int data)
    { return 0 == data % 3;}
};
 
int main()
{
    // 给容器中每个元素乘2
    vector<int> v{1,2,3,4,5,6,7,8,9,0};
    // 临时对象
    for_each(v.begin(), v.end(), Mul2());
	
	// 也可显示定义对象
	Mul2 m2;
	for_each(v.begin(), v.end(), m2);

	// 也可直接传函数
    for_each(v.begin(), v.end(), mul2);
    
    // 上三者等价,将扩大2*2*2=8倍
    for (auto e : v)
        cout << e << " ";
    cout << endl;
 
    // 删除容器中3的倍数
    auto pos = remove_if(v.begin(), v.end(), Mod3());
    v.erase(pos, v.end());
 
    // 将容器中的元素打印出来
    // 注意:对于功能简单的操作,可以使用C++11提供的lambda表达式来代替
    // lambda表达式实现简单,其在底层与仿函数的原理相同,编译器会将lambda表达式转换为仿函数
    for_each(v.begin(), v.end(), [](int data){cout << data << " "; });
    cout << endl;
    return 0;
}

2.6 空间配置器

参考博主上篇博文:[C++ 系列] 87. 简要剖析SGI-STL空间配置器

3. STL框架

一张爆炸的图,学习时老师给做的,比较完善,有少量单词打错及错行现象…很详细很详细…
在这里插入图片描述

发布了391 篇原创文章 · 获赞 329 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yl_puyu/article/details/104955656