Pilhas
Igor Machado Coelho
16/09/2020 - 13/04/2023
São requisitos para essa aula o conhecimento de:
A Pilha (do inglês Stack) é um Tipo Abstrato de Dado (TAD) que pode ser compreendida como vemos no cotidiano.
Em uma pilha de pratos, por exemplo:
Pilhas são estruturas fundamentais na própria computação.
Por exemplo, as chamadas de uma função recursiva podem ser feitas utilizando uma pilha!
… e é precisamente desta maneira que o sistema operacional consegue executar várias de suas funções internas!
Linguagens de programação como Java, C# e Python são implementadas através de operações em pilhas.
Uma Pilha é uma estrutura de dados linear (assim como estruturas de lista), consistindo de 3 operações básicas:
Seu comportamento é descrito como LIFO (last-in first-out), ou seja, o último elemento a entrar na pilha será o primeiro a sair.
De forma geral, uma pilha pode ser implementada utilizando uma lista linear, porém com acesso aos elementos restritos a uma única extremidade dessa lista.
⇄∣C∣B∣A∣
Em C/C++, os métodos esperados para uma pilha de tipo t
são: topo()
, empilha(t)
,
desempilha()
, tamanho()
.
As Pilhas Sequenciais utilizam um array para armazenar os dados. Assim, os dados sempre estarão em um espaço contíguo de memória.
Consideraremos uma pilha sequencial com, no máximo,
MAX_N
elementos no vetor v
do tipo
caractere.
constexpr int MAX_N = 100'000; // capacidade máxima da pilha
struct PilhaSeq1 {
char v[MAX_N]; // elementos na pilha
int N; // num. de elementos na pilha
auto cria() -> void; // inicializa agregado
auto libera() -> void; // finaliza agregado
auto topo() -> char;
auto empilha(char dado) -> void;
auto desempilha() -> char;
auto tamanho() -> int;
};
Antes de completar as funções pendentes, utilizaremos a
PilhaSeq1
:
auto main() -> int {
PilhaSeq1 p;
p.cria(); // nosso contrato no curso!
p.empilha('A'); p.empilha('B'); p.empilha('C');
println("{}", p.topo());
println("{}", p.desempilha());
p.empilha('D');
while(p.tamanho() > 0) println("{}", p.desempilha());
p.libera(); // nosso contrato no curso!
return 0;
}
Verifique as impressões em tela:
C C D B A
A operação cria
inicializa a pilha para uso, e a função
libera
desaloca os recursos dinâmicos.
A operação empilha
em uma pilha sequencial adiciona um
novo elemento ao topo da pilha. A operação desempilha
em
uma pilha sequencial remove e retorna o último elemento da pilha.
A operação de topo em uma pilha sequencial retorna o último elemento empilhado.
auto PilhaSeq1::topo() -> char {
return this->v[this->N-1];
}
auto PilhaSeq1::tamanho() -> int {
return this->N;
}
Desafio: O que aconteceria se a pilha
estivesse vazia e o topo()
fosse invocado? Como permitir
que o programa continue mesmo após situações inesperadas como
essa?
Dica: Retorne um char
opcional, com uma pequena modificação na função
topo()
.
Exemplo:
auto PilhaSeq1::topo() -> std::optional<char> {...}
Considere uma pilha sequencial (MAX_N=5
):
PilhaSeq1 p; p.cria();
p.N: | 0 | p.v: | | | | | |
0 1 2 3 4
Agora, empilhamos A
, B
e C
, e
depois desempilhamos uma vez.
p.N: | 1 | p.v: | A | | | | |
0 1 2 3 4
p.N: | 2 | p.v: | A | B | | | |
0 1 2 3 4
p.N: | 3 | p.v: | A | B | C | | |
0 1 2 3 4
p.N: | 2 | p.v: | A | B | | | |
0 1 2 3 4
Qual o topo atual da pilha?
A Pilha Sequencial tem a vantagem de ser bastante simples de implementar, ocupando um espaço constante (na memória) para todas operações.
Porém, existe a limitação física de MAX_N
posições
imposta pela alocação estática, não permitindo que a pilha ultrapasse
esse limite.
Desafio: implemente uma Pilha Sequencial
utilizando alocação dinâmica para o vetor v
. Assim, quando
não houver espaço para novos elementos, aloque mais espaço na memória
(copiando elementos existentes para o novo vetor).
Dica: Experimente a estratégia de
dobrar a capacidade da pilha (quando necessário), e reduzir à
metade a capacidade (quando necessário). Essa estratégia é bastante
eficiente, mas requer alteração nos métodos cria
,
libera
, empilha
e desempilha
.
A implementação do TAD Pilha pode ser feito através de uma estrutura encadeada com alocação dinâmica de memória.
A vantagem é não precisar pre-determinar uma capacidade máxima da pilha (o limite é a memória do computador!). A desvantagem é depender de implementações ligeiramente mais complexas.
Consideraremos uma pilha encadeada, utilizando um agregado
NoPilha1
para conectar cada elemento da pilha:
Variável local do tipo Pilha Encadeada:
p.N: 0 p.inicio: 0 topo←ϵ
| | | | | | | | | | |
0 4 ... 100 104 108 112 116 ... 8GiB
auto PilhaEnc1::empilha(char v) -> void {
auto no = new NoPilha1{.dado = v, .prox = this->inicio};
this->inicio = no;
this->N++; // N = N + 1
}
p.empilha('A'); p.empilha('B');
p.N: 0 p.inicio: 0 topo←ϵ
| | | | | | | | | | |
0 4 ... 100 104 108 112 116 ... 8GiB
p.N: 1 p.inicio: 112 topo←A
| | | | | | | A | 0 | | |
0 4 ... 100 104 108 112 116 ... 8GiB
p.N: 2 p.inicio: 100 topo←B←A
| | | | B | 112 | | A | 0 | | |
0 4 ... 100 104 108 112 116 ... 8GiB
auto PilhaEnc1::topo() -> char {
auto no = this->inicio;
return no->dado; // return (*no).dado;
// ou simplesmente...
// return this->inicio->dado;
}
p.topo();
p.N: 2 p.inicio: 100 topo←B←A
| | | | B | 112 | | A | 0 | | |
0 4 ... 100 104 108 112 116 ... 8GiB
p.desempilha();
p.N: 2 p.inicio: 100 topo←B←A
| | | | B | 112 | | A | 0 | | |
0 4 ... 100 104 108 112 116 ... 8GiB
p.N: 1 p.inicio: 112 topo←A
| | | | | | | A | 0 | | |
0 4 ... 100 104 108 112 116 ... 8GiB
auto PilhaEnc1::libera() -> void {
while (this->N > 0) {
NoPilha1* p = this->inicio->prox;
delete this->inicio; this->inicio = p; this->N--;
}
}
p.libera();
p.N: 2 p.inicio: 100 topo←B←A
| | | | B | 112 | | A | 0 | | |
0 4 ... 100 104 108 112 116 ... 8GiB
p.N: 1 p.inicio: 112 topo←A
| | | | | | | A | 0 | | |
0 4 ... 100 104 108 112 116 ... 8GiB
p.N: 0 p.inicio: 0 topo←ϵ
| | | | | | | | | | |
0 4 ... 100 104 108 112 116 ... 8GiB
Para uma implementação mais segura, é possível utilizar smart
pointers. Em especial, basta utilizar o
std::unique_ptr
. Para simplificar a sintaxe, consideramos o
seguinte “atalho” para o nome dos ponteiros únicos:
Dessa forma, basta escrever uptr<int>
para
representar um std::unique_ptr<int>
.
Consideraremos uma pilha encadeada, utilizando um agregado
NoPilha2
para conectar cada elemento da pilha:
A Pilha Encadeada é flexível em relação ao espaço de memória, permitindo maior ou menor utilização.
Como desvantagem tende a ter acessos de memória ligeiramente mais lentos, devido ao espalhamento dos elementos por toda a memória do computador (perdendo as vantagens de acesso rápido na memória cache, por exemplo).
Também é considerada como desvantagem o gasto de espaço extra com ponteiros em cada elemento, o que não acontece na Pilha Sequencial.
Uma implementação genérica da pilha sequencial pode ser feita utilizando templates, inclusive para o limite de capacidade (permitindo maior personalização caso a caso).
template<typename T, int MAX_N>
class PilhaSeq
{
public:
T v[MAX_N]; // elementos na pilha
int N; // num. de elementos na pilha
auto cria() -> void; // inicializa agregado
auto libera() -> void; // finaliza agregado
auto topo() -> T;
auto empilha(T dado) -> void;
auto desempilha() -> T;
auto tamanho() -> int;
};
Antes de completar as funções pendentes, utilizaremos a
PilhaSeq
:
int main () {
PilhaSeq<char, 100'000> p;
p.cria();
p.empilha('A');
p.empilha('B');
p.empilha('C');
print("{}\n", p.topo());
print("{}\n", p.desempilha());
p.empilha('D');
while(p.tamanho() > 0)
print("{}\n", p.desempilha());
p.libera();
return 0;
}
Verifique as impressões em tela: C C D B A
PilhaTAD
em C++O conceito de pilha somente requer suas três operações
básicas. Como consideramos uma pilha genérica (pilha de
inteiro, char, etc), definimos um conceito genérico chamado
PilhaTAD
:
template<typename Agregado, typename Tipo>
concept PilhaTAD = requires(Agregado a, Tipo t)
{
// requer operação 'topo'
{ a.topo() } -> std::same_as<t>;
// requer operação 'empilha' sobre tipo 't'
{ a.empilha(t) } -> std::same_as<void>;
// requer operação 'desempilha'
{ a.desempilha() } -> std::same_as<t>;
// requer operação 'tamanho'
{ a.tamanho() } -> std::same_as<int>;
};
PilhaSeq1
satisfaz conceito
PilhaTAD
O static_assert
pode ser usado para assegurar a
corretude de implementação do conceito PilhaTAD
:
PilhaEnc1
satisfaz conceito
PilhaTAD
O static_assert
pode ser usado para assegurar a
corretude de implementação do conceito PilhaTAD
:
PilhaEnc
genéricaImplemente uma pilha encadeada genérica PilhaEnc
, que
satisfaz o conceito PilhaTAD
:
template<typename T>
struct NoPilhaEnc {
T dado;
NoPilhaEnc<T>* prox;
};
template<typename T>
struct PilhaEnc {
NoPilhaEnc<T>* inicio; // elementos na pilha de tipo T
int N; // num. de elementos na pilha
// implementa métodos da Pilha
// ...
};
// verifica se agregado PilhaSeq satisfaz conceito PilhaTAD
static_assert(PilhaTAD<PilhaEnc<char>, char>);
std::stack
Em C/C++, é possível utilizar implementações prontas do TAD Pilha. A vantagem é a grande eficiência computacional e amplo conjunto de testes, evitando erros de implementação.
Na STL, basta fazer import std;
e usar métodos
push
, pop
e top
.
Como reverter uma string? Exemplo: rev("ESCOLA")
=>
"ALOCSE"
.
Solução com pilha (sem recursão):
std::stack
Desafio: escreva um conceito
(utilizando o recurso C++ Concepts) para o std::stack
da
STL, considerando operações push
, pop
e
top
.
Dica: Utilize o conceito
PilhaTAD
apresentado no curso, e faça os devidos ajustes.
Verifique se std::stack
passa no teste com
static_assert
.
Você pode compilar o código proposto (começando pelo slide
anterior em um arquivo chamado
material/3-pilhas/main_pilha_stl.cpp
) usando o CMake
4.
Fim parte de implementações.
Além da bibliografia do curso, recomendamos para esse tópico:
Em especial, agradeço aos colegas que elaboraram bons materiais, como o prof. Fabiano Oliveira (IME-UERJ), e o prof. Jayme Szwarcfiter cujos conceitos formam o cerne desses slides.
Estendo os agradecimentos aos demais colegas que colaboraram com a elaboração do material do curso de Pesquisa Operacional, que abriu caminho para verificação prática dessa tecnologia de slides.
Esse material de curso só é possível graças aos inúmeros projetos de código-aberto que são necessários a ele, incluindo:
Agradecimento especial a empresas que suportam projetos livres envolvidos nesse curso:
Esses slides foram escritos utilizando pandoc, segundo o tutorial ilectures:
Exceto expressamente mencionado (com as devidas ressalvas ao material cedido por colegas), a licença será Creative Commons.
Licença: CC-BY 4.0 2020
Igor Machado Coelho