前言
我们将进入到C++STL 的学习。STL在各各C++的头文件中,以源代码的形式出现,不仅要会用,还要了解底层的实现。源码之前,了无秘密。
STL六大组件
Container通过Allocator取得数据储存空间,Algorithm通过Iterator存取Container内容,Functor可以协助Algorithm完成不同的策略变化,Adapter可以修饰或者套接Functor。
sequential containers
stack
-
stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作
-
stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
-
stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
empty:判空操作
back:获取尾部元素操作
push_back:尾部插入元素操作
pop_back:尾部删除元素操作 -
标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque
#pragma once
#include<iostream>
#include<vector>
#include<list>
#include<deque>
namespace jt
{
//适配器模式
template<class T, class Container = std::deque<T>>
class stack
{
public:
bool empty()const
{
return _con.empty();
}
size_t size()const
{
return _con.size();
}
const T& top()const
{
return _con.back();
}
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
public:
private:
Container _con;
};
}
queue
-
队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端提取元素
-
队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从队尾入队列,从队头出队列
-
底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操作:
empty:检测队列是否为空
size:返回队列中有效元素的个数
front:返回队头元素的引用
back:返回队尾元素的引用
push_back:在队列尾部入队列
pop_front:在队列头部出队列 -
标准容器类deque和list满足了这些要求。默认情况下,如果没有为queue实例化指定容器类,则使用标准容器deque。
#pragma once
#include<iostream>
#include<list>
#include<deque>
namespace jt
{
//适配器模式
template<class T, class Container = std::deque<T>>
class queue
{
public:
bool empty()const
{
return _con.empty();
}
size_t size()const
{
return _con.size();
}
const T& front()const
{
return _con.front();
}
const T& back()const
{
return _con.back();
}
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_front();
}
public:
private:
Container _con;
};
}
练习
//实现一个栈 O(1)找到栈中元素的最小值
class MinStack {
public:
MinStack(){}
void push(int val) {
_st.push(val);
//空栈/出现小于等于的值插入数据
if(_min_st.empty() || _min_st.top() >= val)
_min_st.push(val);
}
void pop() {
if(_st.top() == _min_st.top())
_min_st.pop();
_st.pop();
}
int top() {
return _st.top();
}
int getMin() {
return _min_st.top();
}
private:
//用2个栈来实现
stack<int> _st; //放数据
stack<int> _min_st; //放数据中的最小值
};
class Solution {
public:
bool IsPopOrder(vector<int>& pushV, vector<int>& popV) {
// write code here
stack<int> st;
int push_i = 0, pop_i = 0;
//走完入栈序列
while(push_i < pushV.size())
{
st.push(pushV[push_i]);//无论相等不相等先放入栈
push_i++;
while(!st.empty() && st.top() == popV[pop_i])
{
st.pop();
pop_i++;
}
}
return pop_i == popV.size(); //相等说明匹配
}
};
//后缀表达式求值
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for(const auto& str : tokens)
{
//是操作符开始运算
if(str == "+" || str == "-" || str == "*" || str == "/")
{
int r = st.top();
st.pop();
int l = st.top();
st.pop();
switch(str[0])
{
case '+':
st.push(l + r);
break;
case '-':
st.push(l - r);
break;
case '*':
st.push(l * r);
break;
case '/':
st.push(l / r);
break;
default:
assert(false);
}
}
else
{
st.push(stoi(str)); //操作数进栈
}
}
return st.top();
}
};
deque
deque是双向开口的连续线性空间,双向开口意思是可以在头尾两端分别做元素的插入和删除操作。
deque是由一段又一段连续的空间构成的,为了维护整体连续的假象并提供随机存取的接口,设计了一个复杂的迭代器。
deque采用一块连续的小空间作为主控叫map,其中的每一个元素都是指针,指向一段空间称为buffer,buffer中放的才是queued 存储主体。
为什么要选择deque做stack和queue的底层呢?
1.stack是一种后进先出的特殊线性结构,底层可以用vector和
list,queue是一种先进先出的特殊线性结构,list能作为它的底层。
2.stack和queue不需要遍历元素,只需要在固定的一端或两端进行操作。
3.在stack元素增长到需要扩容的时候,不需要像vector那样挪动数据;queue中由于底层是线性的CPU高速缓存的命中率高。
deque优势与缺陷
- 与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
- deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list。
priority_queue
-
优先队列是一种容器适配器,根据严格的弱排序标准,它的第一个元素总是它所包含的元素中最大的。此上下文类似于堆,在堆中可以随时插入元素,并且只能检索最大堆元素(优先队列中位于顶部的元素)。
-
优先队列被实现为容器适配器,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的成员函数来访问其元素。元素从特定容器的“尾部”弹出,其称为优先队列的顶部。
-
底层容器可以是任何标准容器类模板,也可以是其他特定设计的容器类。容器应该可以通过随机访问迭代器访问,并支持以下操作:
empty():检测容器是否为空
size():返回容器中有效元素个数
front():返回容器中第一个元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素
-
标准容器类vector和deque满足这些需求。默认情况下,如果没有为特定的priority_queue类实例化指定容器类,则使用vector。
-
需要支持随机访问迭代器,以便始终在内部保持堆结构。容器适配器通过在需要时自动调用算法函数make_heap、push_heap和pop_heap来自动完成此操作。
优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。
注意:默认情况下priority_queue是大堆
#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
namespace jt
{
//如果是自定义类型的对象要比较的话,需要在自定义类型中实现对'<''>'重载
//如果传入的是指针但是我们想要比较指针指向的内容的时候,需要
//自己再实现一个struct MyLess,然后传参的时候传入
//比如priority_queue<int, std::vector<int>, MyLess<int*>> pq;
//或者用模板的特化
//假如是学生类
//template<>
//struct MyLess<student*>
//{
// bool operator()(const T& x, const T& y)const
// {
// return *x < *y; //或者可以重载 ' < ' return x < y
// }
//};
template<class T>
struct Less
{
//仿函数
bool operator()(const T& x, const T& y)const
{
return x < y;
}
};
template<class T>
struct Greater
{
bool operator()(const T& x, const T& y)const
{
return x > y;
}
};
template<class T, class Container = std::vector<T>, class Compare = Less<T>>
class priority_queue
{
private:
void adjust_up(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
if (com(_con[parent], _con[child])) // == if ( _con[parent] < _con[child] )
{
std::swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else break;
}
}
void adjust_down(size_t parent)
{
Compare com;
size_t child = (parent * 2) + 1; //默认给左孩子
while (child < _con.size())
{
// == _con[child] < _con[child + 1]
if (child + 1 < _con.size() && com(_con[child], _con[child + 1]) )
{
child = child + 1;
}
// == _con[parent] < _con[child]
if (com(_con[parent], _con[child]))
{
std::swap(_con[child], _con[parent]);
parent = child;
child = (parent * 2) + 1;
}
else break;
}
}
public:
void push(const T& x)
{
_con.push_back(x);
adjust_up(_con.size() - 1);
}
void pop()
{
assert(!_con.empty());
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
const T &top()
{
return _con[0];
}
size_t size()
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
public:
priority_queue(){}
template<class InputIterator>
priority_queue(InputIterator first, InputIterator end)
:_con(first, end)
{
//(_con.size() - 1 - 1) / 2 最后一个节点的父亲
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i++)
{
adjust_down(i);
}
}
private:
Container _con;
};
}
练习
//方法1.
int findKthLargest(vector<int>& nums, int k)
{
priority_queue<int> headmix (nums.begin(), nums.end());
while(--k) headmix.pop();
return headmix.top();
}
//方法2.
int findKthLargest(vector<int>& nums, int k)
{
//用前k个元素建立小根堆
priority_queue<int, vector<int>, greater<int>> minheap (nums.begin(), nums.begin() + k);
for(int i = k; i < nums.size(); i++)
{
if(nums[i] > minheap.top())
{
minheap.pop();
minheap.push(nums[i]);
}
}
return minheap.top();
}
};