Skip to content

Latest commit

 

History

History

3-标准模板库

STL 概述

STL,英文全称 standard template library,中文可译为标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能。

从根本上说,STL 是一些容器、算法和其他一些组件的集合,所有容器和算法都是总结了几十年来算法和数据结构的研究成果,汇集了许多计算机专家学者经验的基础上实现的,因此可以说,STL 基本上达到了各种存储方法和相关算法的高度优化。

vector 的底层为顺序表(数组),list 的底层为双向链表,deque 的底层为循环队列,set 的底层为红黑树,hash_set 的底层为哈希表

STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的

组件 描述
容器 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。
算法 STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 <algorithm> 中,少部分位于头文件 <numeric> 中。
迭代器 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。
函数对象 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。
适配器 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。
内存分配器 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。

C++ STL头文件

<iterator> <functional> <vector> <deque>
<list> <queue> <stack> <set>
<map> <algorithm> <numeric> <memory>
<utility>

一、序列式容器

STL序列式容器,其共同的特点是不会对存储的元素进行排序,元素排列的顺序取决于存储它们的顺序

  • array<T,N>(数组容器):表示可以存储 N 个 T 类型的元素。此类容器一旦建立,其长度就是固定不变的,不能增加或删除元素,只能改变某个元素的值;
  • vector<T>(向量容器,常用):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高 O(1) ,在其它位置插入或删除元素效率较差 O(n)
  • deque<T>(双端队列容器,常用):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效 O(1) ,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶
  • list<T>(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素O(1),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list<T> 必须从第一个元素或最后一个元素开始访问,需要沿着链表移动,直到到达想要的元素。
  • forward_list<T>(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

二、关联式容器

关联式容器的存储的元素为一个一个的“键值对”( <key,value> ),它的功能是在使用关联式容器的过程中,如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式。

关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序

关联式容器所具备的这些特性,归咎于 STL 标准库在实现该类型容器时,底层选用了 「红黑树」这种数据结构来组织和存储各个键值对

  • map,定义在 <map> 头文件中,使用该容器存储的数据,其各个元素的键必须是唯一的(即不能重复),该容器会根据各元素键的大小,默认进行升序排序(调用 std::less<T>
  • set,定义在 <set> 头文件中,使用该容器存储的数据,各个元素键和值完全相同,且各个元素的值不能重复(保证了各元素键的唯一性)。该容器会自动根据各个元素的键(其实也就是元素值)的大小进行升序排序(调用 std::less<T>
  • multimap常用,定义在 <map> 头文件中,和 map 容器唯一的不同在于,multimap 容器中存储元素的键可以重复
  • multiset常用,定义在 <set> 头文件中,和 set 容器唯一的不同在于,multiset 容器中存储元素的值可以重复(一旦值重复,则意味着键也是重复的)

三、无序关联式容器

无序容器具有以下 2 个特点

  1. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键
  2. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器

本质上的不同,无序容器的底层实现采用的是「哈希表」的存储结构,关联式容器的底层实现采用的「红黑树」

实际场景中如果涉及大量遍历容器的操作,建议首选关联式容器;反之,如果更多的操作是通过键获取对应的值,则应首选无序容器

  • unordered_map常用,存储键值对 <key, value> 类型的元素,其中各个键值对键的值不允许重复,且是无序的
  • unordered_set常用,不再以键值对的形式存储数据,而是直接存储数据元素本身(当然也可以理解为,该容器存储的全部都是键 key 和值 value 相等的键值对,正因为它们相等,因此只存储 value 即可)。另外,该容器存储的元素不能重复,且是无序的
  • unordered_multimap,和 unordered_map 唯一的区别在于,该容器允许存储多个键相同的键值对
  • unordered_multiset,和 unordered_set 唯一的区别在于,该容器允许存储值相同的元素

四、容器适配器

容器适配器是一个封装了序列容器的类模板,它在一般序列容器的基础上提供了一些不同的功能。之所以称作适配器类,是因为它可以通过适配容器现有的接口来提供不同的功能。

  • stack<T>常用,是一个封装了 deque<T> 容器的适配器类模板,默认实现的是一个后入先出(Last-In-First-Out,LIFO)的压入栈。定义在头文件 stack 中
  • queue<T>常用,是一个封装了 deque<T> 容器的适配器类模板,默认实现的是一个先入先出(First-In-First-Out,LIFO)的队列。可以为它指定一个符合确定条件的基础容器。定义在头文件 queue 中
  • priority_queue<T>常用,是一个封装了 vector<T> 容器的适配器类模板,默认实现的是一个会对元素排序,从而保证最大元素总在队列最前面的队列。定义在头文件 queue 中
容器适配器 成员函数 基础容器
stack empty()、size()、back()、push_back()、pop_back() 默认 deque,支持 vector、list
queue empty()、size()、front()、back()、push_back()、pop_front() 默认 deque,支持 list
priority_queue empty()、size()、front()、push_back()、pop_back() 默认 vector,支持 deque

五、迭代器

无论是序列容器还是关联容器,最常做的操作无疑是遍历容器中存储的元素,而实现此操作,多数情况会选用“迭代器(iterator)”来实现

我们知道,尽管不同容器的内部结构各异,但它们本质上都是用来存储大量数据的,换句话说,都是一串能存储多个数据的存储单元。因此,诸如数据的排序、查找、求和等需要对数据进行遍历的操作方法应该是类似的。

既然类似,完全可以利用泛型技术,将它们设计成适用所有容器的通用算法,从而将容器和算法分离开。但实现此目的需要有一个类似中介的装置,它除了要具有对容器进行遍历读写数据的能力之外,还要能对外隐藏容器的内部差异,从而以统一的界面向算法传送数据。

这是泛型思维发展的必然结果,于是迭代器就产生了。简单来讲,迭代器和 C++指针非常类似,它可以是需要的任意类型,通过迭代器可以指向容器中的某个元素,如果需要,还可以对该元素进行读/写操作。

常用的迭代器按功能分为输入迭代器、输出迭代器、前向迭代器、双向迭代器、随机访问迭代器 5 种,功能越来越强

1、前向迭代器(forward iterator)

假设 p 是一个前向迭代器,则 p 支持 ++p,p++,*p 操作,还可以被复制或赋值,可以用 == 和 != 运算符进行比较。此外,两个正向迭代器可以互相赋值。

2、双向迭代器(bidirectional iterator)

双向迭代器具有正向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p 或者 p-- 操作(即一次向后移动一个位置)

3、随机访问迭代器(random access iterator)

随机访问迭代器具有双向迭代器的全部功能。除此之外,假设 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作:

  • p+=i:使得 p 往后移动 i 个元素。
  • p-=i:使得 p 往前移动 i 个元素。
  • p+i:返回 p 后面第 i 个元素的迭代器。
  • p-i:返回 p 前面第 i 个元素的迭代器。
  • p[i]:返回 p 后面第 i 个元素的引用。

此外,两个随机访问迭代器 p1、p2 还可以用 <、>、<=、>= 运算符进行比较。另外,表达式 p2-p1 也是有定义的,其返回值表示 p2 所指向元素和 p1 所指向元素的序号之差(也可以说是 p2 和 p1 之间的元素个数减一)

不同容器的迭代器

容器 对应的迭代器类型
array 随机访问迭代器
vector 随机访问迭代器
deque 随机访问迭代器
list 双向迭代器
set / multiset 双向迭代器
map / multimap 双向迭代器
forward_list 前向迭代器
unordered_map / unordered_multimap 前向迭代器
unordered_set / unordered_multiset 前向迭代器
stack 不支持迭代器
queue 不支持迭代器

迭代器的定义方式

迭代器定义方式 具体格式
正向迭代器 容器类名::iterator 迭代器名;
常量正向迭代器 容器类名::const_iterator 迭代器名;
反向迭代器 容器类名::reverse_iterator 迭代器名;
常量反向迭代器 容器类名::const_reverse_iterator 迭代器名;

用法

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

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    vector<int> vec(arr, arr + 5); //vec被初始化成有 5 个元素
    vector<int>::iterator iter;    //创建一个正向迭代器,当然也支持其他 3 种定义迭代器的方式

    // 第一种遍历方法:vec[i]
    for (int i = 0; i < vec.size(); ++i)
        cout << vec[i] << " ";
    cout << endl; // 1 2 3 4 5

    //第二种遍历方法:用 != 比较两个迭代器
    for (iter = vec.begin(); iter != vec.end(); ++iter)
        cout << *iter << " ";
    cout << endl; // 1 2 3 4 5

    //第三种遍历方法:用 < 比较两个迭代器
    for (iter = vec.begin(); iter < vec.end(); ++iter)
        cout
            << *iter << " ";
    cout << endl; // 1 2 3 4 5

    //第四种遍历方法:随机访问迭代器支持 "+= 整数"  的操作
    iter = vec.begin();
    while (iter < vec.end())
    {
        cout << *iter << " ";
        iter += 2; // 1 3 5
    }
}

六、迭代适配器

完全可以将迭代器适配器视为普通迭代器。之所以称为迭代器适配器,是因为这些迭代器是在输入迭代器、输出迭代器、正向迭代器、双向迭代器或者随机访问迭代器这些基础迭代器的基础上实现的。

1、反向迭代器(reverse_iterator)

又称“逆向迭代器”,其内部重新定义了递增运算符(++)和递减运算符(--),专门用来实现对容器的逆序遍历。

2、安插型迭代器(inserter或者insert_iterator)

通常用于在容器的任何位置添加新的元素,需要注意的是,此类迭代器不能被运用到元素个数固定的容器(比如 array)上。

3、流迭代器(istream_iterator / ostream_iterator)

输入流迭代器用于从文件或者键盘读取数据;相反,输出流迭代器用于将数据输出到文件或者屏幕上。

4、流缓冲区迭代器(istreambuf_iterator / ostreambuf_iterator)

输入流缓冲区迭代器用于从输入缓冲区中逐个读取数据;输出流缓冲区迭代器用于将数据逐个写入输出流缓冲区。

5、移动迭代器(move_iterator)

此类型迭代器是 C++ 11 标准中新添加的,可以将某个范围的类对象移动到目标范围,而不需要通过拷贝去移动。

来源

http://c.biancheng.net/stl/