0%

CPP设计-STL

一、总论

STL提供了一组表示容器、迭代器、函数对象(没看懂,不会)和算法的模板。

  • 容器是一个与数组类似的单元,可以存储若干个值。STL的容器是同质的,即存储的值的类型相同;
  • 算法是完成特定任务的方法;
  • 迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;
  • 函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。

STL是一种泛型编程(generic programming)模板使得算法独立于存储的数据类型,而迭代器使算法的独立于使用的容器类型。


二、迭代器

迭代器其实就是我理解的游标,因为很多的算法都需要遍历一组数据的集合(比如说排序算法),所以我们就需要一个游标一样的东西在这组集合上面移动。这组集合可以是一个数组,也可以是一个链表,还可以是一个树,当然,也可以是容器。所以迭代器是很重要的。

同时迭代器也是不同的,比如说我们操作数组的时候,一般用索引值(数组下标) i,j,k 这类东西来当做迭代器。而我们在操作链表的时候,一般都用一个链表节点的指针当做迭代器,比如说 p,q,cur,pre 之类的。

当我们提出迭代器的概念的时候,希望的是忽略容器的类型,比如说从前到后遍历一个数据集合。无论这个集合是数组形式实现的,还是链表形式实现的,我们在遍历的过程中都有一个“让迭代器等于迭代器后继”这样的一个思想,在数组中表现为 i = i + 1 在链表中表现为 cur = cur->next 所以如果我们把这个过程抽象出来,那么就可以不考虑具体的容器是啥样的,而达到设计算法的目的,也就是说,实现了上面提到的“迭代器使算法的独立于使用的容器类型。”

但是也需要注意到,不是所有的容器都有完美的迭代器的,比如说链表的迭代器指针,他是没有固定时间复杂度来进行随机访问的,也就是类似这种操作 *(a + n) ,指针必须线性时间复杂度完成这个功能。这种功能就可以说是不能理想实现的。依照功能的实现,我们将迭代器进行分类,分类如下

迭代器功能 输入 输出 正向 双向 随机访问
解除引用读取 *i Y - Y Y Y
解除引用写入 *i = - Y Y Y Y
固定和可重复排序(按照相同顺序去遍历,允许多趟算法) - - Y Y Y
i++, ++i Y Y Y Y Y
i--, --i - - - Y Y
i[n] - - - - Y
i + n - - - - Y
i - n - - - - Y
i += n - - - - Y
i -= n - - - - Y
i - j - - - - Y
i compare j - - - - Y

之所以要有这样一个清晰的认识,是因为如果功能不足,很可能有些算法是实现不了的(比如说快排就没办法在链表的迭代器上实现)。我们在编写算法的时候尽可能使用要求最低的迭代器,并让它适用于容器的最大区间。

不过有一些看起来很没有道理,比如说双向迭代器没法支持 i + n 操作,但是可以支持 i++ 操作,但是只要将 i++ 重复执行 n 次,就可以完成这种操作,这可能是上面说的操作都是可以在 $O(1)$ 时间复杂度内完成。

不同类型的迭代器本质不是类型,而是一系列要求。STL算法可以使用任何满足其要求的迭代器实现。STL文献使用术语概念(concept)来描述一系列要求。概念可以具有类似继承的关系。比如双向迭代器继承了正向迭代器的功能。STL文献使用术语改进(refinement)来表示这种概念上的继承,因此,双向迭代器是对正向迭代器概念改进。概念的具体实现被叫做模型(model)


三、容器

3.1 容器

容器是存储其他对象的对象,被存储的对象必须是同一种类型的,它们可以是OOP意义上的对象,也可以是内置类型。容器可以分成三大类:

  • 序列 sequence:线性容器,包括 vector, deque, list, queue, priority_queue, stack, forward_list
  • 关联容器 associative container:树形键值对容器,包括 set, multiset, map, multimap
  • 无序关联容器 unordered associative container:哈希键值对容器,包括 unordered_set, unordered_multiset, unordered_map, unordered_multimap,是 C++11 的新标准。

容器具有一些基本的方法:

表达式 返回类型 说明
a.begin() iterator 返回指向容器第一个元素的迭代器
a.end() iterator 指向超尾迭代器
a.size() unsigned int 返回元素个数
a.swap(b) void 交换 a 和 b 的内容
a == b bool 如果 a 和 b 的长度相同,且元素均相同,则为真
a != b bool 返回 !(a == b)

同时容器是支持“插入 insert 和删除 erase ”的,显然如果不支持这两个基本的编辑操作,这个数据结构也没啥意义了,只不过对于不同的容器,其功能有所区别,比如说对于 sequence 来说,insert 可以指定插入的位置,但是关联容器就没有这个功能,这是因为关联容器的顺序是排序关系决定的,无法指定。

3.2 序列

3.2.1 序列总论

可以通过添加要求来改进基本的容器概念。序列(sequence)是一种重要的改进。序列概念增加了“迭代器至少是正向迭代器”。而且序列还要求其元素按严格的线性顺序排列。

序列的种类虽然多,但是其底层只有 vector, deque, list, forward_list 4 中,其他的如 stack, queue, priority_queue 之类的只是建立在前面的容器上的适配器(adapter)又称包装类,他们进一步封装了这些容器,然后提供了一些更加独特的 API。

序列中有一些方法针对头部和尾部的插入删除,有些序列可以实现,有些没有实现,在了解了其底层实现结构后,就可以很轻松的理解。

函数 含义 容器
front() 头部元素 均可
back() 尾部元素 均可
push_front() 插入头部 vector 不可
pop_front() 删除头部 vector 不可
push_back() 插入尾部 均可
pop_back() 删除尾部 均可

3.2.2 vector

vector 是最基础的类,其底层是一个数组,所以用于随机访问的迭代器,所以基本上 a[i] 这种操作是支持的。

但是因为底层是数组,所以对于头部的删除和增加无能为力。

3.2.3 deque

deque 是“双端队列(double end queue)”的意思,其底层实现是一个在数组上实现的循环队列,所以他既具有随机访问的迭代器,又可以有在头部插入的快速操作。

但是 deque 的实现是比 vector 更加复杂的,所以 vector 在随机访问和中段的编辑中要优于 deque

3.2.4 list

底层实现是双向链表,不具有随机访问的迭代器。正因为不具有随机访问的迭代器,所以有一些功能会受到限制,比如说 STL 算法库中的 sort 需要随机访问的迭代器,如果没有的话,那么就无法进行。

为了更好地发挥链表的优势,所以考虑一些特定的方法,如下所示

方法 说明
void merge(list other) 合并两个已经排好序的链表,合并后 other 链表为空,复杂度为 $O(n)$
void remove(const T &val) 删除所有的 val 元素,复杂度为 $O(n)$
void sort() 使用 < 运算符对链表排序,时间复杂度为 $O(NlogN)$
void splice(iterator pos, list other) 将链表 other 的内容插入到 pos 之前,other 的内容为空,时间复杂度为 $O(1)$
void unique() 连续的相同元素压缩为单个元素,时间复杂度为 $O(n)$

3.2.5 forward_list

是一个单项的链表,只有正向迭代器,相比于 list 要更加简单紧凑,但是功能也受到了限制。

3.2.6 queue, stack

两者都是适配器类,queue 的底层是 dequestack 的底层是 vector ,他们多出了三个方法,如下所示:

表达式 含义
a.empty() 一个布尔函数,为空时返回 true
a.push() 压入元素,是 void 返回值
a.pop() 弹出元素,是 void 返回值

3.2.7 priority_queue

优先队列,支持的操作与 queue 类似,本质是一个利用数组实现的堆。默认是一个小根堆,堆顶的元素是最大的元素。

因为涉及到了比较操作,所以 STL 允许我们定义比较函数,示例如下

#include <bits/stdc++.h>
#include <queue>
using namespace std;

struct Node
{
public:
    int x, y;
    Node(int x, int y) :
        x(x), y(y)
    {
    }
    void show() const
    {
        cout << "x: " << x << ", y: " << y << endl;
    }

	// attention to the second 'const'
	bool operator< (const Node &other) const
	{
		if (other.x == x)
		{
			return y > other.y;
		}
		else 
		{
			return x > other.x;
		}
	}
};

// wrong
bool cmp(const Node &a, const Node &b)
{
    if (a.x == b.x)
    {
        return a.y < b.y;
    }
    else
    {
        return a.x < b.y;
    }
}

struct COMPARE
{
    bool operator()(const Node &a, const Node &b)
    {
        if (a.x == b.x)
        {
            return a.y < b.y;
        }
        else
        {
            return a.x < b.x;
        }
    }
};

int main()
{
	// three type arguments
    priority_queue<Node, vector<Node>, COMPARE> q;
    q.push(Node(3, 2));
    q.push(Node(1, 2));
    q.push(Node(1, 1));
    q.push(Node(5, 1));

    while (!q.empty())
    {
		q.top().show();
        q.pop();
    }

	cout << "==========" << endl;
	// one type argument
    priority_queue<Node> q1;
    q1.push(Node(3, 2));
    q1.push(Node(1, 2));
    q1.push(Node(1, 1));
    q1.push(Node(5, 1));

    while (!q1.empty())
    {
		q1.top().show();
        q1.pop();
    }
    return 0;
}

比较函数有三种形式,正如上所示,对于比较函数,是行不通的,只有比较函数对象内置的重载运算符可以实现,在自定义的时候,有两种声明形式,一种是三参数的,分别指明了元素类型、底层容器类型和比较函数对象,如下所示

priority_queue<Node, vector<Node>, COMPARE> q;

如果是重载运算符,那么用一个单参数的就好了

priority_queue<Node> q1;

如果希望排序的是一个基础类型,可以利用 greater<> 函数对象,如下所示

priority_queue<int, vector<int>, greater<int>> q;

3.3 关联容器

关联容器一般采用树结构进行实现(应该是红黑树)将值与键关联在一起,并使用键来查找值(感觉就是 python 中的字典),包括 set,multiset, map, multimap。

3.3.1 map

只有一个双向迭代器,不支持 iter + n 这样的操作,会自动根据 less<Key> 进行排序,所以利用迭代器访问的时候是有序的。当然也可以自定义排序规则,如下所示

std::map<std::string, int, std::greater<std::string> >myMap{ {"C语言教程",10},{"STL教程",20} };

此时用迭代器访问的结果是这样的

<"STL教程", 20>
<"C语言教程", 10>

虽然迭代器并不是随机访问的,但是却支持根据 key 值查找,如下所示

smap["A"] = 1;
smap["B"] = 2;

其中比较相等用到的是重载的 ==

对于map的方法,有

begin()         //返回指向map头部的迭代器

clear()        //删除所有元素

count()         //返回指定元素出现的次数

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的函数

四、算法

4.1 sort()

sort算法使用如下,可以看到实现了对序列的升降序排列:

#include<bits/stdc++.h>

using namespace std;
int a[5] = {1, 5, 3, 2, 4};

bool cmp(int a, int b)
{
    if(a > b) return true;
    else return false;
}

int main()
{
    sort(a, a + 5);
    for(int i = 0; i < 5; i++)
    {
        cout<<a[i]<<" ";
    }
    cout<<endl;
    
	// sort(a, a + 5, greater<int>()); 这种写法也可以
    sort(a, a + 5, cmp);
     for(int i = 0; i < 5; i++)
    {
        cout<<a[i]<<" ";
    }
    cout<<endl;   

}

4.2 lower_bound() 和 upper_bound()

从小到大的数组中,lower_bound() 会在规定的范围内找到大于等于指定 val 的第一个元素,并且返回指向这个元素的迭代器。采用的是二分查找,所以时间复杂度是 $\log_2 n$ 。

upper_bound() 会在规定的范围内找到大于指定 val 的第一个元素,其他要素与lower_bound()相同,用法如下

lower_bound(iterator begin, iterator end, ValueType val)

此外,lower_bound() 还接受自定义的比较过程,比如重载函数

lower_bound(iterator begin, iterator end, ValueType val, cmp)

有如下示例

#include<bits/stdc++.h>

using namespace std;

int a[5] = {1, 7, 5, 2, 4};

bool cmp(int a, int b)
{
    if(a > b) return true;
    else return false;
}

int main()
{
    sort(a, a + 5);

    int pos = lower_bound(a, a + 5, 2) - a;
    cout << "The lower_bound() result is " << pos << endl;
    pos = upper_bound(a, a + 5, 2) - a;
    cout << "The upper_bound() result is " << pos << endl;


    sort(a, a + 5, greater<int>());
    pos = lower_bound(a, a + 5, 2, greater<int>()) - a;
    cout << "The lower_bound() result is " << pos << endl;
    pos = upper_bound(a, a + 5, 2, greater<int>()) - a;
    cout << "The upper_bound() result is " << pos << endl;

}