pilha e fila
1. Adaptador de contêiner
Adaptador é um padrão de design (um padrão de design é um conjunto de experiências de design de código que são usadas repetidamente, conhecidas pela maioria das pessoas, classificadas e catalogadas).Esse padrão converte a interface de uma classe em outra que desejamos.
Embora os elementos também possam ser armazenados em pilhas e filas , eles não são classificados em contêineres no STL . Em vez disso, eles são chamados de adaptadores de contêiner . Isso ocorre porque pilhas e filas apenas envolvem as interfaces de outros contêineres. STL Em pilha e fila , deque é usado por padrão (descrito posteriormente), por exemplo:
Na verdade, os adaptadores de contêiner reutilizam outros contêineres e usam as funções de outros contêineres para adaptar um novo contêiner.
2. deque (entender)
deque (fila dupla): É uma estrutura de dados de espaço "contínuo" de abertura dupla. O significado da abertura dupla é que as operações de inserção e exclusão podem ser realizadas em ambas as extremidades, e a complexidade do tempo é O (1), que é o mesmo que Comparado com vector , a inserção de cabeçalho é mais eficiente e não requer elementos móveis; em comparação com list , a utilização do espaço é relativamente alta. Se você precisar de acesso aleatório eficiente e um grande número de inserções e exclusões, é recomendável usar o deque.
Deque não é um espaço verdadeiramente contínuo, mas é composto de pequenos espaços contínuos. O deque real é semelhante a uma matriz bidimensional dinâmica. Sua estrutura subjacente é mostrada na figura abaixo:
Comparado com vector , a vantagem do deque é: ao inserir e excluir o cabeçalho, não há necessidade de mover elementos, o que é muito eficiente. Além disso, ao expandir, não há necessidade de mover um grande número de elementos, por isso sua eficiência é maior que vetor ;
Comparado com list , sua camada inferior é um espaço contínuo, a taxa de utilização do espaço é relativamente alta e não há necessidade de armazenar campos adicionais;
No entanto, deque tem uma falha fatal: não é adequado para travessia, porque durante a travessia, o iterador de deque precisa frequentemente detectar se ele se moveu para o limite de um determinado espaço pequeno, resultando em baixa eficiência. Em cenários sequenciais, ele pode ser necessário fazer Traversal com frequência, portanto, na prática, quando uma estrutura linear é necessária, a listaeo vetor Deque não possui muitos aplicativos, e um aplicativo que pode ser visto até agora é que o STL o usa como a estrutura de dados subjacente da da filaepilha
Então, por que escolher deque como o contêiner padrão subjacente para stack e queue ?
Stack é uma estrutura de dados linear especial com last-in-first-out . Portanto, qualquer estrutura linear com operações push_back() e pop_back() pode ser usada como o contêiner subjacente de stack , como vector e list ;
Queue é uma estrutura de dados linear especial que é o primeiro a entrar, primeiro a sair . Contanto que a estrutura linear tenha operações push_back e pop_front , ela pode ser usada como o contêiner subjacente de queue , como list . No entanto, deque é selecionado como contêiner subjacente por padrão para pilha e fila em STL , principalmente porque:
- A pilha e a fila não precisam ser percorridas (portanto, a pilha e a fila não possuem iteradores) , elas só precisam operar em uma ou ambas as extremidades fixas.
- Quando os elementos na pilha crescem, o deque é mais eficiente que o vetor (não há necessidade de mover uma grande quantidade de dados durante a expansão); quando os elementos na fila crescem, o deque não é apenas eficiente, mas também tem alto uso de memória . Combina as vantagens do deque e evita perfeitamente suas deficiências.
3. pilha
1. Introdução à pilha
Podemos primeiro dar uma olhada na documentação de stack : stack .
-
Stack é um adaptador de contêiner especialmente utilizado em contextos com operações last-in-first-out. Sua exclusão só pode inserir e extrair elementos de uma extremidade do contêiner.
-
Stack é implementado como um adaptador de contêiner. Um adaptador de contêiner encapsula uma classe específica como seu contêiner subjacente e fornece um conjunto de funções de membro específicas para acessar seus elementos. Ele usa uma classe específica como seu contêiner subjacente específico do elemento no final (Isso isto é, o topo da pilha) é empurrado e estourado.
-
O contentor subjacente da pilha pode ser qualquer modelo de classe de contentor padrão ou alguma outra classe de contentor específica.Estas classes de contentores devem suportar as seguintes operações:
empty:判空操作 back:获取尾部元素操作 push_back:尾部插入元素操作 pop_back:尾部删除元素操作
-
O vetor de contêineres padrão, deque e lista atendem a esses requisitos. Por padrão, se nenhum contêiner subjacente específico for especificado para a pilha , o deque será usado por padrão .
Vejamos primeiro brevemente o uso de stack :
void test_stack()
{
stack<int> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while (!st.empty())
{
cout << st.top() << ' ';
st.pop();
}
cout << endl;
}
Os resultados da execução são os seguintes:
2. Simular e implementar pilha
Usamos deque como a implementação de simulação do adaptador de stack :
#pragma once
#include <vector>
#include <deque>
namespace Young
{
template <class T, class Container = deque<T>>
class Stack
{
public:
// 入栈
void push(const T& val)
{
_con.push_back(val);
}
// 出栈
void pop()
{
_con.pop_back();
}
// 获取栈顶元素
T& top()
{
return _con.back();
}
// const 对象获取栈顶元素
const T& top() const
{
return _con.back();
}
// 获取栈的大小
size_t size()
{
return _con.size();
}
// 判断栈是否为空
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
Como acima, as interfaces comuns de pilha foram implementadas. Vamos testá-las com nossa própria pilha implementada:
4.fila
1. Uso de fila
Vamos primeiro dar uma olhada na documentação da fila : queue .
-
Uma fila é um adaptador de contêiner projetado para operar em um contexto FIFO (primeiro a entrar, primeiro a sair), onde os elementos são inseridos de uma extremidade do contêiner e extraídos da outra extremidade.
-
A fila é implementada como um adaptador de contêiner, que encapsula uma classe de contêiner específica como sua classe de contêiner subjacente. A fila fornece um conjunto específico de funções de membro para acessar seus elementos. Os elementos são colocados na fila a partir do final da fila e retirados da fila do início.
-
O contêiner subjacente pode ser um dos modelos de classe de contêiner padrão ou outras classes de contêiner especialmente projetadas. O contêiner subjacente deve suportar pelo menos as seguintes operações:
empty:检测队列是否为空 size: 返回队列中有效元素的个数 front:返回队头元素的引用 back: 返回队尾元素的引用 push_back:在队列尾部入队列 pop_front:在队列头部出队列
-
As classes de contêiner padrão deque e list atendem a esses requisitos. Por padrão, se nenhuma classe de contêiner for especificada para instanciação de fila , o deque de contêiner padrão será usado .
Primeiro, vamos dar uma breve olhada no uso de queue :
void test_queue()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
while (!q.empty())
{
cout << q.front() << ' ';
q.pop();
}
cout << endl;
}
Os resultados da execução são os seguintes:
2. Simular implementação de fila
Também usamos deque para adaptar queue :
#pragma once
#include <deque>
namespace Young
{
template<class T, class Container = deque<T>>
class Queue
{
public:
// 入队列
void push(const T& val)
{
_con.push_back(val);
}
// 出队列
void pop()
{
_con.pop_front();
}
// const 对象获取队头元素
const T& front() const
{
return _con.front();
}
// 获取队头元素
T& front()
{
return _con.front();
}
// const 对象获取队尾元素
const T& back() const
{
return _con.back();
}
// 获取队尾元素
T& back()
{
return _con.back();
}
// 获取队列长度
size_t size()
{
return _con.size();
}
// 判断队列是否空
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
Vamos usar a fila que implementamos para testá-la:
3. prioridade_fila
(1) Introdução ao prioridade_queue
Priority_queue: A fila prioritária é um tipo de fila. Vamos primeiro dar uma olhada em sua documentação para apresentar Priority_queue .
-
Uma fila de prioridade é um adaptador de contêiner cujo primeiro elemento é sempre o maior dos elementos que contém, de acordo com critérios estritos de ordenação fraca.
-
Este contexto é semelhante a um heap, onde os elementos podem ser inseridos a qualquer momento e apenas o maior elemento do heap (o elemento no topo da fila de prioridade) pode ser recuperado.
-
O contêiner subjacente pode ser qualquer modelo de classe de contêiner padrão ou outra classe de contêiner projetada especificamente. O contêiner deve ser acessível por meio de iteradores de acesso aleatório e suportar as seguintes operações:
empty():检测容器是否为空 size(): 返回容器中有效元素个数 front():返回容器中第一个元素的引用 push_back():在容器尾部插入元素 pop_back(): 删除容器尾部元素
-
As classes de contêiner padrão vector e deque atendem a essas necessidades. Por padrão, vector é usado se nenhuma classe de contêiner for especificada para uma instanciação de classe prioridade_queue específica .
-
Os iteradores de acesso aleatório precisam ser suportados para que a estrutura do heap seja sempre mantida internamente. O adaptador de contêiner faz isso automaticamente, chamando automaticamente as funções algorítmicas make_heap, push_heap e pop_heap quando necessário.
(2) Uso de prioridade_queue
Por padrão, a fila de prioridade usa o vetor como seu contêiner subjacente para armazenar dados. O algoritmo de heap é usado no vetor para construir os elementos do vetor em uma estrutura de heap . Portanto, prioridade_queue é um heap. Você pode considerar o uso de prioridade_queue sempre que um pilha é necessária. . Nota: Por padrão, priority_queue é um heap grande .
Observe que quando olhamos para a documentação, prioridade_queue é uma pilha grande por padrão , mas a caixa vermelha na imagem acima é um functor (descrito mais adiante) que implementa comparação. Menos significa pequeno, mas é implementado em uma pilha grande. , preste atenção aqui. Em segundo lugar, quando usamos os parâmetros padrão, precisamos apenas passar o primeiro parâmetro, e os parâmetros subsequentes podem usar os parâmetros padrão. No entanto, quando precisamos usar um pequeno heap, precisamos passar todos os parâmetros; vamos pegar uma olhada primeiro. use:
#include <vector>
#include <queue>
#include <functional> // greater算法的头文件
void TestPriorityQueue()
{
// 默认情况下,创建的是大堆,其底层按照小于号比较
vector<int> v{ 3,2,7,6,0,4,1,9,8,5 };
priority_queue<int> q1;
for (auto& e : v)
q1.push(e);
cout << q1.top() << endl;
// 如果要创建小堆,将第三个模板参数换成 greater 比较方式
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
cout << q2.top() << endl;
}
Os resultados da execução são os seguintes:
(3) Funtor
Functor, também conhecido como objeto de função, é uma classe que pode executar funções funcionais. Seu uso é igual às nossas chamadas de função habituais.
Primeiro temos que implementar uma classe. Esta classe precisa implementar a ()
sobrecarga do operador. As funções implementadas nela precisam ser implementadas por nós mesmos. Suponha que precisamos implementar um grande heap em prioridade_queue , como segue:
// 仿函数 --- 大堆,大的优先级大
template <class T>
class Less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
Então, como chamá-lo? Primeiro temos que criar um objeto e depois usar esse objeto para chamar a função:
Less<int> less;
cout << less(1, 8) << endl; // 与下等价
cout << less.operator()(1, 8) << endl;
Os resultados da execução são os seguintes:
Abaixo usamos o formulário functor para simular e implementar prioridade_queue .
(4) Simular implementação de prioridade_queue
#pragma once
#include <vector>
namespace Young
{
// 模板参数
template <class T, class Container = vector<T>, class Compare = Less<T>>
class PriorityQueue
{
public:
// 向上调整 --- 大堆
void adjust_up(size_t child)
{
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child]) // 与下等价
if (com(_con[parent], _con[child]))
{
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())
{
if (child + 1 < _con.size() && _con[child + 1] > _con[child])
{
++child;
}
// if (_con[parent] < _con[child]) // 与下等价
if (com(_con[parent], _con[child]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 入数据
void push(const T& val)
{
_con.push_back(val);
adjust_up(_con.size() - 1);
}
// 出数据
void pop()
{
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
// 获取优先级最大的数据
const T& top()
{
return _con.front();
}
// 判空
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
}
Abaixo usamos a fila de prioridade que implementamos para teste:
void test_priority_queue()
{
// Young::PriorityQueue<int, vector<int>, Greater<int>> pq; // 小堆
// Young::PriorityQueue<int, vector<int>, Less<int>> pq; // 大堆,与下等价
Young::PriorityQueue<int> pq; // 缺省参数默认是大堆
pq.push(2);
pq.push(8);
pq.push(1);
pq.push(0);
pq.push(10);
while (!pq.empty())
{
cout << pq.top() << ' ';
pq.pop();
}
cout << endl;
}
Os resultados do teste são os seguintes: