Hoje falarei sobre tecnologias relacionadas a pilhas e filas, apenas como uma revisão para mim mesmo.
Expressões infixas e postfixas (saiba mais sobre o aprendizado)
O conceito de infixo e sufixo:
Nossas expressões comuns são geralmente expressões infixas , por exemplo:
1+2*3-4
Então, o que é uma expressão pós-fixada ?
Como as expressões infixas não podem determinar muito bem a precedência dos operadores, há o nascimento de expressões sufixos, por exemplo:
523*-
As regras de operação são as seguintes:
Ao encontrar operandos, pulamos primeiro e, ao encontrar operadores, calculamos . Um operador é avaliado com os dois números mais próximos à sua esquerda, e o resultado do cálculo serve como o operando direito do próximo operador, e assim por diante.
Aqui, primeiro calcule 2*3 para obter 6, 6 é calculado novamente como o operando direito do sinal de menos, ou seja, 5-6=-1, e o resultado da expressão acima é -1.
Podemos concretizar as ideias acima através da pilha: (aqui é só falar da ideia)
Primeiro crie uma pilha: stack<int>st; percorremos desde o início da expressão, quando encontramos um operando, empurramos o operando de volta para a pilha, quando encontramos um operador, pegamos o número no topo da pilha como o operando direito, após pop Em seguida, pegue o número no topo da pilha como o operando e o resultado da operação é empurrado de volta para a pilha novamente. Quando a expressão chega ao final, o último elemento no topo do pilha é o resultado da expressão.
O método de conversão de infixo em sufixo:
Quando encontramos um operando, nós o produzimos, e quando encontramos um operador, colocamos o operador na pilha. Para dois operadores adjacentes na pilha, se o último for maior que o anterior, ele não poderá ser operado e armazenado na pilha primeiro, e o último é maior que o anterior.Se um for baixo ou igual, o anterior pode ser retirado da pilha para operação.
A implementação simulada da pilha:
Cada design de contêiner terá seu padrão de design:
padrão do adaptador . Converta o que você já tem no que deseja.
Padrão de iterador . Os detalhes subjacentes não são expostos e um contêiner de acesso unificado é fornecido após o encapsulamento.
Aqui queremos simular a percepção de que a pilha pode ser encapsulada por contêineres existentes, como vetor ou lista. O código é o seguinte:
#pragma once
#include <iostream>
#include<vector>
#include<list>
using namespace std;
namespace kang
{
template<class T, class container=vector<T>>
class stack
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
container _con;
};
}
O contêiner de parâmetros do modelo usa o adaptador cuja camada inferior é vetorial por padrão, e também podemos usar a pilha de lista vinculada em nosso aplicativo real:
int main()
{
kang::stack<int,list<int>> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while (!st.empty())
{
cout << st.top() << endl;
st.pop();
}
return 0;
}
A implementação simulada da fila:
Assim como na implementação de uma pilha, também usamos outros contêineres para adaptar uma fila: (semelhante ao código acima, mas diferente em detalhes)
#pragma once
#include <iostream>
#include<vector>
#include<list>
using namespace std;
namespace kang
{
template<class T, class container = list<T>>
class queue
{
public:
void push(const T& val)
{
_con.push_back(val);
}
void pop() //因为是队列,所以pop出的是队头的数据
{
_con.pop_front();
}
const T& front() //队列特有接口
{
return _con.front();
}
const T& back() //队列特有接口
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
size_t size()
{
return _con.size();
}
private:
container _con;
};
}
Aqui, o adaptador de contêiner só pode ser uma lista por padrão. O motivo: na interface pop(), pop_front existe na lista, mas o vetor não. A eficiência da exclusão do cabeçalho do vetor é baixa, portanto não há interface que suporte a exclusão do cabeçalho do vetor.O vetor implementa a exclusão do cabeçalho por meio de erase().
Pilhas e filas implementadas na biblioteca:
Escusado será dizer sobre a interface, nossa implementação simulada é basicamente a mesma da biblioteca, mas a pilha e a fila na biblioteca usam o mesmo adaptador de contêiner deque. Que tipo de contêiner é esse e por que devemos usá-lo? A seguir discutiremos em detalhes:
Os respectivos defeitos de vetor e lista:
vetor: A expansão é cara e a inserção e exclusão no meio da cabeça são ineficientes. (apenas a pilha pode ser satisfeita)
list: O acesso aleatório não é suportado e a taxa de acertos do cache da CPU é baixa. (Pode satisfazer pilha e fila)
Então, podemos projetar um contêiner que tenha as vantagens do vetor e da lista? Neste momento, deque é comumente conhecido como fila dupla.
Nota : A fila dupla não é uma fila em essência! ! Portanto, não tem natureza de primeiro a entrar, primeiro a sair! ! !
Deque de fila dupla:
Ele não só pode suportar acesso aleatório como vetor, mas também suportar operações de plug-in e exclusão como lista.
list é equivalente a Zhuge Liang, vector é equivalente a Lu Bu e deque é equivalente a Wei Yan. Embora o deque tenha as vantagens de ambos os lados, raramente é usado em cenários porque não maximiza as vantagens de ambos os lados.Você entenderá quando eu terminar de falar sobre sua implementação subjacente.
A implementação subjacente do deque:
Deque é composto por vários arrays de buffer e um array de ponteiros de controle central.
Os ponteiros de buffer são armazenados na matriz de ponteiros de controle central e todos os dados são armazenados em cada buffer. Esse espaço contínuo não só tem as vantagens do acesso aleatório vetorial e da alta taxa de acertos do cache da CPU, mas também tem as vantagens da lista: inserção e exclusão no início e no fim (não há necessidade de expandir, basta abrir a matriz de buffer diretamente). A única coisa que precisa ser expandida é que a matriz de ponteiros de controle central será expandida quando estiver cheia.
Suas falhas:
Inserir dados no meio é ineficiente e precisa movê-los. Comparado com a exclusão de dados no meio da lista, não é perfeito o suficiente e não é tão rápido quanto a lista .
vector下标随机访问是指针直接加减就行,而deque随机访问需要计算数据在第几个buffer中已经在这个buffer的第几个位置。因此它的随机访问有一定的消耗,没有vector快。
总结:
deque的应用虽不是很广泛,但作为栈和队列的容器适配器非常的合适!
优先级队列:
priority_queue底层是一个堆,它的容器适配器是vector。它默认是大的优先级高(大堆)
它的使用非常的简单接口如下:
优先级队列的底层实现:
优先级队列本质上是一个堆,让我们先写出它的构造函数:
priority_queue(InputIterator first, InputIterator last)
:_con(first,last)
{
//建堆
for (int i = (_con.size() - 1-1)/2; i >= 0; --i)
{
adjust_down(i);
}
}
//_con默认是vector
建堆如果使用向下调整建堆它的时间复杂度是O(N)。
建堆如果使用向上调整建堆它的时间复杂度是O(N*logN)。
由此可见用向下调整的方法最优,所以在上面的代码我们使用adjust_down。但是需要注意的是:在我们向下调整的时候,不是最后一个数据做向下调整,而是他的父节点做第一次向下调整。通过先减1后除2一个找到一个子节点的父节点。
然后是push和pop操作:
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(_con[0]);
}
push用的是向上调整,因为实现不了向下调整。
一个堆中的pop操作是头删,方法是先将第一个数据和最后一个位置互换,再删除最后一个数据,最后将第一个数据做向下调整。
讲了这么多最重要的还没讲,那就是向上调整和向下调整的代码实现:
//默认是大堆
void adjust_up(size_t child)
{
size_t parent = (child-1) / 2;
while (child > 0)
{
if (_con[parent] < _con[child])
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//默认是大堆
void adjust_down(size_t parent)
{
size_t child = parent * 2 + 1;
while (child <_con.size())
{
//默认认为左孩子大,右孩子大就++child
if (child + 1 < _con.size() && _con[child] < _con[child + 1])
{
++child;
}
if (_con[child] > _con[parent])
{
swap(_con[child], _con[parent]);
parent = child;
child=parent*2+1
}
else
{
break;
}
}
}
仿函数/函数对象:
仿函数是一个类,它的要求是必须封装operator(),这样可以使类对象可以像函数一样使用。
例如我们实现两个数大小比较的仿函数:
namespace kang
{
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
}
我们实现的类就可以称为是:仿函数。
在这里我们调用了lessFunc(1,2),按照我们之前的理解,lessFunc是函数名或者是函数指针。但是在这里它却是一个类实例化出来的对象。一个对象像函数一样被使用,那我们称它为:函数对象。
它的好处:
我们用冒泡排序举例子,如果我们想控制它的升序降序问题,我们需要在它的参数中传入一个函数指针,用函数指针控制降序升序:
但是传入函数指针的方法太麻烦了,有的函数指针甚至非常复杂,这时仿函数的优势就体现了出来:
template<class T,class Compare>
void bubble_sort(T arr[], int sz,Compare con)//传入数组和数组元素的个数sz
{
for (int i = 0; i <= sz ; i++)//趟数
{
int exchange = 1;
for (int j = 1; j <= sz - i; j++)//交换的个数
{
if (con(arr[j],arr[j-1]))//此处是精华!!!!
{
swap(arr[j - 1], arr[j]);
exchange = 1;
}
if (exchange == 0)
{
break;
}
}
}
}
con(arr[j],arr[j-1])这句代码我们不需要关注底层的实现细节,我们只要传入需要的仿函数对象来达到我们想要的目的。不仅可以这样调用,也可以这样调用:
第一个是有名对象,第二个是匿名对象。
这时有人会提出一个问题:为什么这里的con不加引用呢?
没错,这里不加引用就会进行拷贝,但拷贝的代价在这里不大!!!我们先思考一个仿函数对象是几个字节?一个仿函数是没有成员变量的类,所以它的大小为1个字节。
仿函数优化优先级队列:
以下是优先级队列的完整代码:
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace kang
{
template<class T>
class less
{
public:
bool operator()(const T& x, const T& y)
{
return x < y;
}
};
template<class T>
class greater
{
public:
bool operator()(const T& x, const T& y)
{
return x > y;
}
};
template<class T,class Container= vector<T>,class Compare=less<T>>
class priority_queue
{
public:
priority_queue()
{}
template <class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:_con(first, last)
{
//建堆
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; --i)
{
adjust_down(i);
}
}
//默认是大堆
void adjust_up(size_t child)
{
Compare com; //显示实例化
size_t parent = (child-1) / 2;
while (child > 0)
{
if (com(_con[parent] , _con[child]))
{
swap(_con[parent], _con[child]);
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() && com(_con[child] , _con[child + 1]))
{
++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() const
{
return _con[0];
}
bool empty() const
{
return _con.empty();
}
size_t size() const
{
return _con.size();
}
private:
Container _con;
};
}
仿函数的高级用法:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue()
{
// 大堆,需要用户在自定义类型中提供<的重载
kang::priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;
// 如果要创建小堆,需要用户提供>的重载
kang::priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2018, 10, 29));
q2.push(Date(2018, 10, 28));
q2.push(Date(2018, 10, 30));
cout << q2.top() << endl;
// 大堆
kang::priority_queue<Date*> q3;
q3.push(new Date(2018, 10, 29));
q3.push(new Date(2018, 10, 28));
q3.push(new Date(2018, 10, 30));
cout << *q3.top() << endl;
// 小堆
kang::priority_queue<Date*> q4;
q4.push(new Date(2018, 10, 29));
q4.push(new Date(2018, 10, 28));
q4.push(new Date(2018, 10, 30));
cout << *q4.top() << endl;
}
int main()
{
TestPriorityQueue();
return 0;
}
在上面代码运行的结果中,我们发现前两个结果正确,后两个结果错误并且结果每次各不相同。原因就是每次new出来的指针大小是不固定的。在后面两个代码中我们想要比较的是日期类的大小,而不是指针的大小,所以我们可以用仿函数来解决:
struct PDateLess
{
bool operator()(const Date* d1, const Date* d2)
{
return *d1 < *d2;
}
};
struct PDateGreater
{
bool operator()(const Date* d1, const Date* d2)
{
return *d1 > *d2;
}
};
最后再将仿函数传进参数中:(因为没有用模板,所以不用加类型)
priority_queue<Date*, vector<Date*>, PDateLess> q3;
priority_queue<Date*, vector<Date*>, PDateGreater> q4;
这里的应用就是在特定的场景下我们可以实现一个特定的仿函数来满足我们的需求,因为库中的仿函数可能满足不了我们的需求,所以我们要自己写。好了到这里栈和队列的所有内容就结束了,创作不易,感谢阅读,希望大佬了能够多多支持我。