【算法】常用STL容器

概述

这是我对写算法题中常用的STL容器的笔记。

概述

STL中容器是被使用的最多的部分,因此需要记住常用的容器的各种方法。这样在写算法题的时候就不需要关系各种常用数据结构的实现。因为标准库的容器已经帮我们做了这一件事情。但是标准库的所有容器都是基于模板技术的。所以需要知道如何实例化模板,也就是如何使用模板。

vector

vector,直接翻译的含义是向量,但是实际上,其实现的是一个动态扩容的数组。也就是说,我们无需关心数组中元素放不下的时候需要考虑的扩容问题。我们只要把元素放进去。这个时候的vector就可以当成是一个数组来使用了。vector根据元素的个数来分配内存,每一次扩容会根据扩容因子来进行扩容,一般是二倍扩容。

vector是一个模板类,我们使用vector<类型>来实例化一个模板的类型。比如下面这一行代码,我们就可以定义一个叫做vvector动态数组,并且数组的元素类型是int

vector<int> v;

首先要解决的是访问vector中的元素,我们可以把vector完全当成是一个原生的数组一样使用,也就是使用[]下标运算符来访问。也可以用使用at成员函数来访问。vector的下标是从0开始的,然后最后一个元素的下标是size()-1。因为size()拿到的是元素的个数。

其次,可以使用push_back()函数在vector的末尾添加元素,使用pop_back()函数删除末尾的元素。还可以使用insert()函数在指定位置插入元素,使用erase()函数删除指定位置的元素。

可以使用size()函数获取vector中元素的数量,使用empty()函数检查vector是否为空。还可以使用resize()函数调整vector的大小。

vector提供了迭代器,可以用于遍历容器中的元素。可以使用begin()函数获取一个元素的迭代器,使用end()函数获取指向最后-一个元素之后位置的迭代器。

下面是一个例子,使用了所有提到过的函数。

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

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	vector<int> v; // 创建一个vector对象
	v.push_back(1); // 往vector里面放一个元素1
	v.empty(); // 判断v是否为空,返回以一个bool
	cout << "size() : " << v.size() << endl; // 输出有几个元素 
	v.pop_back(); // 删除末尾元素
	v.resize(10); // 把容器的容量扩容到10
	v[2] = 2; // 往下标为2的地方放一个元素2
	for(vector<int>::iterator it = v.begin(); // 定义一个迭代器,赋初值为v的初始迭代器 
		it != v.end(); // it迭代器到v的end(),时候结束 
		it++){ // it迭代器进行迭代到下一个元素
		cout << *it <<endl; // 输出每一个元素 
	}
	
    return 0;
}

针对于vector的排序可以调用sort函数。

sort(v.begin(), v.end());

去重也是很有必要的一个操作,最好背下来。这里可以看到,是需要先利用sort进行排序,然后unique去除相邻的重复元素,再erase删除。

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

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	vector<int> v = {1, 2, 3, 3, 7, 4, 5, 6, 2, 3, 2, 1};
	sort(v.begin(), v.end());
	vector<int>::iterator last = unique(v.begin(), v.end()); 
	v.erase(last, v.end());
	
	for(vector<int>::iterator it = v.begin();
		it != v.end();
		it++){
		cout << *it << endl;
	}
	
    return 0;
}

因为蓝桥杯官方那个课是说明了可以使用C++11,但是devcpp如果不加-std=c++11的话,那么过不去编译。devcpp的菜单ToolsComplier Options里面添加这个选项。如果可以使用C++11,那么可以优化为下面这种写法。

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

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	vector<int> v = {1, 2, 3, 3, 7, 4, 5, 6, 2, 3, 2, 1};
	sort(v.begin(), v.end());
	auto last = unique(v.begin(), v.end()); 
	v.erase(last, v.end());
	
	for(auto i : v){
		cout << *i << endl;
	}
	
    return 0;
}

list

list这个链表,用的不多。因为链表的遍历速度太慢了,它必须通过一个一个节点之间的指针来访问,如果要访问最后一个元素,就要从头找到尾,这个时间复杂度是 O ( n ) O(n) O(n)。这在算法比赛里面几乎是不能忍受的,而且用vector来存数据,可以方便使用sort,而list不行,因为它没有随机访问迭代器。这是由其链式结构存储决定的。

说了这么多,有一个印象就行,list方便添加和移除元素,但是因为不能随机访问,所以在算法比赛里面不好用。虽然,STL底层使用双向链表来做list的底层容器,但是对于访问中间的元素还是性能不佳。

list容器提供了多个常用的成员函数来操作和访问链表中的元素。
以下是一-些常用的list函数对应的作用:

  1. push_ back():将元素插入到链表的末尾。
  2. push_ front():将元素插入到链表的开头。
  3. pop_ back():移除链表末尾的元素。
  4. pop_ _front():移除链表开头的元素。
  5. size():返回链表中元素的个数。
  6. empty):检查链表是否为空。
  7. clear():清空链表中的所有元素。
  8. front():返回链表中第一-个元素的引用。
  9. back():返回链表中最后一个元素的引用。
  10. begin():返回指向链表第一个元素的迭代器。
  11. end():返回指向链表末尾的下一个位置的迭代器。
  12. inser():在指定位置之前插入- -个或多个元素。
  13. erase():从链表中移除指定位置的一个或多个元素。

stack

stack,即栈,是一种先进后出(FILO,First In Last Out)的数据结构。

栈的常用函数如下。

函数描述时间复杂度
push(x)在栈顶压入一个元素O(1)
pop()弹出栈顶元素O(1)
top()返回栈顶元素O(1)
empty()检查栈是否为空O(1)
size()返回栈中元素的个数O(1)

这里说的弹出栈顶元素,就是返回栈顶元素的值,然后把栈顶元素删除。

利用栈的先进后出,把一个数组依次放入栈然后再依次取出,那么就可以让数组进行一个反转。

queue

queue,队列是一种先进先出的数据结构。下面是queue提供的常用函数。

函数描述时间复杂度
push(x)在队尾插入一个元素O(1)
pop()弹出队首元素O(1)
front()返回队首元素O(1)
back()返回队尾元素O(1)
empty()检查队列是否为空O(1)
size()返回队列中元素的个数O(1)

一道题来练习一下queue的用法,这里创建两个元素为string的队列,分别是nqvq,对应普通的窗口队列和VIP窗口队列。按照要求进队列,然后出队列即可。

题目链接:https://www.lanqiao.cn/problems/1113/learning/

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

int m;
queue<string> nq, vq;
string io_buf, name_buf, nv_buf;

int main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> m;
    for (int i = 0; i < m; i++) {
        cin >> io_buf;
        if (io_buf == "IN") {
            cin >> name_buf >> nv_buf;
            if (nv_buf == "N") {
                nq.push(name_buf);
            }
            else {
                vq.push(name_buf);
            }
        }
        else {
            cin >> nv_buf;
            if (nv_buf == "N") {
                nq.pop();
            }
            else {
                vq.pop();
            }
        }
    }
    while (!vq.empty()) {
        cout << vq.front() << endl;
        vq.pop();
    }
    while (!nq.empty()) {
        cout << nq.front() << endl;
        nq.pop();
    }
    return 0;
}

priority_queue

priority_queue,优先队列,与普通队列不同,优先队列会按照数据值的大小进行排序。大的放在队列的前面,也就是队列的顶部。为什么说是顶部,那是因为优先队列的底层实现是利用大顶堆来实现的,这个实现无需我们关心。

优先队列是重点的数据结构,下面是优先队列的常用函数。

函数描述时间复杂度
push(x)在优先队列中插入一个元素O(logn)
pop()弹出优先队列的顶部元素O(logn)
top()返回优先队列顶部的元素O(1)
empty()检查队列是否为空O(1)
size()返回队列中元素的个数O(1)
#include <bits/stdc++.h>
using namespace std;

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	priority_queue<int, vector<int>> pq;
	pq.push(1);
	pq.push(5);
	pq.push(3);
	while(!pq.empty()){
		cout << "top: " << pq.top() << endl;
		pq.pop();
	}
	
    return 0;
}

那么当然我们也有可能需要自定义这个优先队列按照什么进行排序,还是老方法,定义自己的排序函数。

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

int cmp(int lhs, int rhs)
{
	return lhs > rhs;
}

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	priority_queue<int, vector<int>, decltype(&cmp)> pq(cmp);
	pq.push(1);
	pq.push(5);
	pq.push(3);
	while(!pq.empty()){
		cout << "top: " << pq.top() << endl;
		pq.pop();
	}
	
    return 0;
}

这里是使用了decltype这个C++11添加的自动推导类型的特性。如果不用的话,自己手动写出类型,下面这样。推导前加一个&取地址是因为要传入的函数,我们要的是函数指针类型,这样构造pq的时候,传入cmp函数的地址,本质上就是一个函数指针。

priority_queue<int, vector<int>, int (*)(int, int)> pq(cmp);

那么如果使用C++11的那些特性写,就很方便。下面我使用lambda表达式来写。

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

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	auto fn = [](int lhs, int rhs){
		return lhs > rhs;
	};
	priority_queue<int, vector<int>, decltype(fn)> pq(fn);
	pq.push(1);
	pq.push(5);
	pq.push(3);
	while(!pq.empty()){
		cout << "top: " << pq.top() << endl;
		pq.pop();
	}
	
    return 0;
}

如果就是简单的要改成降序,那么使用greater,下面这样定义pq。

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

下面以合并果子这道题,来练习优先队列。通过阅读题目的例子,可以看出来,这些果子应该从小到大合并,那么此时一定是消耗体力最少的。如果想证明也是很容易的,假设先合并大的更省体力,那么后面合并的时候,需要多次重复这一堆大的,那么显然消耗的体力更多。所以,应该使用优先队列,把这个果子全部都加到队列里,然后每一次取前面两个小的,然后合并好了以后再加入到优先队列里面,直到优先队列的元素个数只有1,这种做法一定是最小的消耗体力方式。

题目链接:https://www.lanqiao.cn/problems/741/learning/

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

int n, t, sum = 0, a, b;
priority_queue<int, vector<int>, greater<int>> pq;

int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin >> n;
    // 把所有类型的果子加进队列
    while (n--) {
        cin >> t;
        pq.push(t);
    }
    // 开始合并
    while (pq.size() != 1) {
        a = pq.top();
        pq.pop();
        b = pq.top();
        pq.pop();
        pq.push(a + b);
        sum += a + b;
    }
    cout << sum << endl;
    return 0;
}

deque

deque,双端队列,这种容器它允许在两端进行高效的插入和删除操作。deque是由一系列连续的存储块(缓冲区)组成的,每个存储块都存储了多个元素。这使得deque能够在两端进行快速的插入和删除操作,而不需要移动其他元素。

一般来说不会单独使用deque,而是利用deque实现单调队列后使用。

函数描述时间复杂度
push_back(x)在队列尾部插入一个元素平均O(1)
push_front(x)在队列头部插入一个元素平均O(1)
pop_back()弹出队列尾部元素平均O(1)
pop_front()弹出队列头部元素平均O(1)
front()返回队列头部元素O(1)
back()返回队列尾部元素O(1)
empty()检查队列是否为空O(1)
size()返回队列中元素的个数O(1)
clear()清空所有元素O(1)

pair

pair表示一个键值对,下面是一个基本对应的定义。

template<class T1, class T2>
struct pair {
	T1 first; //第一个值
	T2 second; //第二个值
	//构造函数
	pair();
	pair(const T1& X, const T2& y);
	//比较运算符重载
	bool operator==(const pair& rhs) const;
	bool operator!=(const pair& rhs) const;
	//其他成员函数和特性
};

pair它把两个值绑在一起,当成一个值进行传递,存储和操作。例如下面这样,把一个int和一个double打包起来。形成一个变量。

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

int main()
{
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	pair<int, double> p(1, 3.14);
	cout << p.first << ' ' << p.second << endl;
	
    return 0;
}

pair是可以嵌套的,也就是说pair<int, pair<int, double>>也是可行的。

pair为元素类型的数组进行排序时候,默认为pairfirst进行升序,然后如果first``first相等的时候,再按照second进行升序排序。

map

map,是一种关联式的容器,存储了键值对(Key-Value),其中每一个Key都是唯一的。map的底层是使用红黑树实现的,因此是会根据Key进行自动排序,这是的其查找速度很快,而且性能稳定,几乎所有的操作的时间复杂度都是 O ( l o g n ) O(logn) O(logn)map是最常用的键值对容器。下面是其常用函数功能表。

函数功能时间复杂度
insert插入元素O(logn)
erase删除元素O(logn)
find查找元素O(logn)
count统计元素的个数O(logn)
size返回元素的个数O(1)
begin返回容器的起始迭代器O(1)
end返回容器的结束迭代器O(1)
clear清空容器O(n)
empty判断容器是否为空O(1)
lower_bound返回指向第一个不小于指定键的元素位置O(logn)
upper_bound返回指向第一个大于指定键的元素位置O(logn)

multimap

multimapmap基本上相同,但是multimap运行存储多个相同键的键值对。但是multimap在算法题中几乎不用。删除元素的时候要指定元素,要不然会把同一个键的键值对都删掉。

下面是其常用函数功能表。

函数功能时间复杂度
insert插入元素O(logn)
erase删除元素O(logn)
find查找元素O(logn)
count统计元素的个数O(logn)
size返回元素的个数O(1)
begin返回容器的起始迭代器O(1)
end返回容器的结束迭代器O(1)
clear清空容器O(n)
empty判断容器是否为空O(1)

unordered_map

unordered_map也是一种键值对容器,因为它底层是哈希表,所以存储的元素不是有序的。因而,它有最坏情况,这说明了它的不稳定性,差的时候很差,好的时候很好。所以较少使用这个容器。

下面是其常用函数功能表。

函数功能平均时间复杂度最坏时间复杂度
insert插入元素O(1)O(n)
erase删除元素O(1)O(n)
find查找元素O(1)O(n)
count统计元素的个数O(1)O(n)
size返回元素的个数O(1)O(1)
clear清空容器O(1)O(n)
empty判断容器是否为空O(1)O(1)

set

set,集合,是一种用来存储一组唯一元素的容器,并且按照一定的规则进行排序,默认使用小于号排序,所以是升序。set的底层也是红黑树,下面是其常用函数功能表。

函数描述平均时间复杂度最坏时间复杂度
insert向集合中插入元素O(logn)O(logn)
erase移除集合中的指定元素O(logn)O(logn)
find在集合中查找指定元素O(logn)O(logn)
lower_bound返回第一个不小于给定值的元素的迭代器O(logn)O(logn)
upper_bound返回第一个大于给定值的元素的迭代器O(logn)O(logn)
size返回集合中元素的个数O(1)O(1)
empty检查集合容器是否为空O(1)O(1)
clear清空结合O(n)O(n)
begin返回指向集合起始位置的迭代器O(1)O(1)
end返回指向集合末尾位置的迭代器O(1)O(1)
rbegin返回指向集合起始位置的逆向迭代器O(1)O(1)
rend返回指向集合末尾位置的逆向迭代器O(1)O(1)

multiset

multisetset的不同之处在于允许存放重复的元素。multiset相较于set的使用频率高一些。

函数描述平均时间复杂度最坏时间复杂度
insert向集合中插入元素O(logn)O(logn)
erase移除集合中的指定元素O(logn)O(logn)
find在集合中查找指定元素O(logn)O(logn)
lower_bound返回第一个不小于给定值的元素的迭代器O(logn)O(logn)
upper_bound返回第一个大于给定值的元素的迭代器O(logn)O(logn)
size返回集合中元素的个数O(1)O(1)
empty检查集合容器是否为空O(1)O(1)
clear清空结合O(n)O(n)
begin返回指向集合起始位置的迭代器O(1)O(1)
end返回指向集合末尾位置的迭代器O(1)O(1)
rbegin返回指向集合起始位置的逆向迭代器O(1)O(1)
rend返回指向集合末尾位置的逆向迭代器O(1)O(1)

unordered_set

unordered_set底层使用的是哈希表,因此没有排序功能。这里要注意的是unordered_set几乎不用。

下面是unordered_set的常用函数功能表。

函数描述平均时间复杂度最坏时间复杂度
insert向集合中插入元素O(1)O(n)
erase移除集合中的指定元素O(1)O(n)
find在集合中查找指定元素O(1)O(n)
count返回元素在集合中出现的个数O(1)O(n)
size返回集合中元素的个数O(1)O(1)

练习题

宝藏排序II

题目链接:https://www.lanqiao.cn/problems/3226/learning/

根据题目描述,需要把这些宝藏进行从小到大的排序。可以想到,有很多种做法,比如,也可以把所有数据都放到vector里面,进行升序排序,然后遍历输出。

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

int n, t;
vector<int> v;

int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> t;
        v.push_back(t);
    }
    sort(v.begin(), v.end());
    for (auto i : v) {
        cout << i << ' ';
    }
    return 0;
}

当然可以使用优先队列,把宝藏的珍贵程度作为值,然后把优先的方式改成值越小优先级越高。

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

int n, t;
priority_queue<int, vector<int>, greater<int>> pq;

int main()
{
    ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> t;
        pq.push(t);
    }
    while (!pq.empty()) {
        cout << pq.top() << ' ';
        pq.pop();
    }
    return 0;
}

小蓝吃糖果

题目链接:https://www.lanqiao.cn/problems/1624/learning/

这道题,应该能够想到使用模拟的写法。那么如何吃,才能尽可能吃完。如果先吃数量少的糖果,那么肯定会导致多的那些糖果吃不完。所以可以想到一个思路,就是先排序,让然后按照多的开始吃。这里需要对糖果进行一个编号,这样就能区分糖果类型了。模拟的做法是可以做,但是这里要注意数据量,数据最大有有一百万,模拟很容易会超时。

对于这个题目,仔细手动模拟以后,会发现,其实只要除掉最大的那种糖果类型的数量大于等于最大的那种糖果类型的数量-1即可。这个条件怎么理解呢,题目中要求不能连续两个相同类型的糖果,那么只需要在最大类型的糖果的缝隙中插入剩下的糖果,而且随着插入,可以插入的位置越来越多,这是因为空隙的数量变多了。

#include <iostream>
using namespace std;

int n, t, max_num = -1;
long long sum = 0;
int a[1000000];

int main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> a[i];
        max_num = max(a[i], max_num);
        sum += a[i];
    }
    if (sum - max_num >= max_num - 1) {
        cout << "Yes" << endl;
    }
    else {
        cout << "No" << endl;
    }

    return 0;
}

小蓝的括号串1

题目链接:https://www.lanqiao.cn/problems/2490/learning/

这个题目就是利用栈,当遇到左括号的时候入栈,遇到右括号的时候弹出栈顶元素。不过要小心,栈为空的时候还pop就会发生程序崩溃。

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

int n;
string str;
stack<char> s;

int main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n >> str;
    for (int i = 0; i < n; i++) {
        if (str[i] == '(') {
            s.push('(');
        }
        if (s.empty() && str[i] == ')') {
            cout << "No" << endl;
            return 0;
        }
        if (str[i] == ')') {
            s.pop();
        }
    }
    if (s.empty()) {
        cout << "Yes" << endl;
    }
    else {
        cout << "No" << endl;
    }
    return 0;
}

这个时候也可以利用一个整数,如果遇到左括号就加1,然后右括号就减1。

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

int n, f = 0;
string str;

int main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n >> str;
    for (int i = 0; i < n; i++) {
        if (f < 0) {
            break;
        }
        if (str[i] == '(') {
            f++;
        }
        if (str[i] == ')') {
            f--;
        }
    }
    if (f == 0) {
        cout << "Yes" << endl;
    }
    else {
        cout << "No" << endl;
    }
    return 0;
}

快递分拣

题目链接:https://www.lanqiao.cn/problems/1531/learning/

这道题,可以想到是要利用键值对存储的容器。但是这里要求,按照输入顺序的城市名称和快递单号的顺序进行输出。那么就需要单独存储这个顺序。

所以这里我利用m存储一个城市和单号的键值对,其中单号使用vector存储,因为一个城市对应多个单号。然后v存储的是城市的顺序。

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

int n, c;
string num, city;
map<string, vector<string>> m;
vector<string> v;

int main()
{
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    cin >> n;
    while (n--) {
        cin >> num >> city;
        if (m.count(city) == 0) { // 没有的话要存储城市顺序
            v.push_back(city);
        }
        m[city].push_back(num);
    }
    for (auto i : v) {
        c = m[i].size();
        cout << i << ' ' << c << endl;
        for (int j = 0; j < c; j++) {
            cout << m[i][j] << endl;
        }
    }

    return 0;
}

两数之和

题目链接:https://leetcode.cn/problems/two-sum/
我们很容易想到一个 0 ( n 2 ) 0(n^2) 0(n2)的算法,即暴力枚举,先任意确定一个数,然后再在数组内遍历找有没有其他的数字可以和这个数字进行组成target。下面是实现这个思路的代码。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        vector<int> res;
        for(int i = 0; i < nums.size() - 1; i++){
            for(int j = i + 1 ; j < nums.size(); j++){
                if(nums[i] + nums[j] ==  target){
                    res.push_back(i);
                    res.push_back(j);
                }
            }
        }
        return res;
    }
};

那么有没有更快的方法,这个时候可以想到利用哈希表,因为哈希表的查询速度可以达到 O ( 1 ) O(1) O(1),把当前的数值和原来数组内的下标作为一个键值对。那么就可以很快拿到下标了。

class Solution {
public:
    vector<int> twoSum(vector<int>& nums, int target) {
        unordered_map<int, int> m;
        vector<int> res;
        for(int i = 0; i < nums.size(); i++){
            if(m.find(target - nums[i]) != m.end()){
                res.push_back(m[target - nums[i]]);
                res.push_back(i);
                break;
            }
            m[nums[i]] = i;
        }
        return res;
    }
};
  • 25
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值