Estrutura de Dados I

Pilhas

Estrutura LIFO, em que só o topo é acessível. Interface do TAD, implementações com vetor e com lista encadeada, e o casamento de delimitadores.

Uma pilha (stack) é uma estrutura linear em que os elementos só podem ser inseridos e removidos por uma das pontas, o topo. O último elemento inserido é o primeiro a sair: por isso, diz-se que a pilha segue a política LIFO (Last In, First Out).

O modelo intuitivo é uma pilha de pratos em uma prateleira: é fácil colocar um prato em cima ou tirar o de cima, mas, para pegar o penúltimo, é preciso antes tirar o último. Na computação, pilhas aparecem em muitos lugares:

  • a pilha de execução, que guarda as chamadas de função ainda não terminadas (é ela que torna a recursão possível);
  • o desfazer (undo) de um editor de texto, que desfaz primeiro a última alteração;
  • o botão voltar do navegador, que volta primeiro para a página visitada por último;
  • a análise de expressões, como verificar se os parênteses de uma expressão estão balanceados (veja a aplicação no fim desta página).

Operações

Três operações definem uma pilha:

  • empilhar (push): insere um elemento no topo;
  • desempilhar (pop): remove o elemento do topo e o devolve;
  • topo (top): consulta o elemento do topo, sem removê-lo.

Também é comum verificar se a pilha está vazia e, quando ela tem tamanho máximo, se está cheia.

Cinco estados de uma pilha desenhada como um recipiente aberto em cima: vazia; com 1; com 1 e 2; com 1, 2 e 3; e, depois de desempilhar, com 1 e 2, enquanto o 3 sai pelo topo
Empilhar três elementos e desempilhar um: sai o último que entrou.
Pergunta

Uma pilha só permite acessar o topo; uma lista encadeada permite inserir, remover e consultar em qualquer posição. Em que sentido a pilha é mais restrita e, ao mesmo tempo, mais simples e eficiente?

Ver respostaEsconder resposta

É mais restrita porque não dá para inserir, remover nem consultar elementos do meio. E é mais simples e eficiente justamente por isso: todas as operações acontecem sempre no mesmo lugar, e nenhuma precisa percorrer a estrutura.

Interface do TAD Pilha

Como em todo tipo abstrato de dados, começamos pela interface: quais operações quem usa a pilha tem à disposição. Para simplificar, a pilha guarda inteiros.

/* Pilha.h */
#ifndef PILHA_H
#define PILHA_H

#include <stdbool.h>

typedef struct pilha Pilha;

Pilha *criaPilha(int capacidade);   /* capacidade: máximo de elementos */
void liberaPilha(Pilha *p);
void empilha(Pilha *p, int valor);  /* insere no topo */
int desempilha(Pilha *p);           /* remove o topo e devolve o valor */
int topoPilha(const Pilha *p);      /* devolve o topo, sem remover */
bool pilhaVazia(const Pilha *p);

#endif

Vamos implementar essa mesma interface de duas formas: com um vetor e com uma lista encadeada. O código que usa a pilha (o cliente) não muda nada de uma para a outra; é exatamente esse o valor de programar contra um TAD.

Implementação com vetor

Quando se conhece de antemão quantos elementos a pilha pode ter ao mesmo tempo, um vetor resolve: os elementos ocupam as primeiras posições, e um inteiro topo guarda o índice do elemento do topo. A pilha vazia tem topo = -1.

struct pilha {
    int *vetor;
    int capacidade;
    int topo;       /* índice do elemento do topo; -1 se vazia */
};
Vetor de 8 posições com 10, 20, 30 e 40 nas posições 0 a 3 e as demais livres; topo vale 3 e aponta para o 40
Pilha com quatro elementos em um vetor de capacidade 8.

Empilhar é avançar o topo e gravar na nova posição; desempilhar é ler a posição do topo e recuar. Nenhum elemento sai do lugar.

void empilha(Pilha *p, int valor) {
    if (p->topo == p->capacidade - 1) {
        printf("Erro: pilha cheia\n");
        return;
    }
    p->topo++;
    p->vetor[p->topo] = valor;
}

int desempilha(Pilha *p) {
    if (pilhaVazia(p)) {
        printf("Erro: pilha vazia\n");
        return -1;
    }
    int valor = p->vetor[p->topo];
    p->topo--;
    return valor;
}

O programa completo está abaixo, com os três arquivos do TAD. O main.c empilha quatro valores, desempilha todos e, por fim, tenta desempilhar de uma pilha vazia.

Pilha com vetorC
Pergunta

Qual a complexidade de cada função: criaPilha, empilha, desempilha, topoPilha, pilhaVazia e liberaPilha?

Ver respostaEsconder resposta

Todas são : nenhuma percorre o vetor; cada uma lê ou altera o topo e, no máximo, uma posição do vetor. (Em criaPilha, o malloc não inicializa o vetor, então o custo não depende da capacidade.)

O -1 de erro é ambíguo

desempilha e topoPilha devolvem -1 para sinalizar a pilha vazia, mas -1 também pode ser um valor legítimo guardado na pilha. Quem recebe o -1 não tem como saber qual dos dois aconteceu. Uma alternativa comum é separar os dois significados: devolver um código de sucesso e entregar o valor por um ponteiro, como em bool desempilha(Pilha *p, int *valor). Nesta página mantemos a versão mais simples, e quem usa a pilha confere pilhaVazia antes de desempilhar.

Implementação com lista encadeada

Quando não se sabe quantos elementos a pilha terá, uma lista encadeada cresce e diminui à vontade. Inserir e remover no início de uma lista simplesmente encadeada custam , então basta considerar o início da lista como o topo da pilha.

typedef struct no {
    int dado;
    struct no *proximo;
} No;

struct pilha {
    No *inicio;     /* o topo da pilha é o início da lista */
    int tamanho;
};
A struct Pilha com inicio e tamanho 3; inicio aponta para o nó 30, que aponta para 20, que aponta para 10, cujo próximo é nulo; o nó 30 é o topo e o nó 10 é o fundo
Pilha com lista encadeada: o topo é o primeiro nó.

empilha é a inserção no início da lista, e desempilha é a remoção do primeiro nó:

void empilha(Pilha *p, int valor) {
    No *novo = malloc(sizeof(No));
    novo->dado = valor;
    novo->proximo = p->inicio;
    p->inicio = novo;
    p->tamanho++;
}

int desempilha(Pilha *p) {
    if (pilhaVazia(p)) {
        printf("Erro: pilha vazia\n");
        return -1;
    }
    No *removido = p->inicio;
    int valor = removido->dado;      /* lê antes de liberar */
    p->inicio = removido->proximo;
    free(removido);
    p->tamanho--;
    return valor;
}

Para liberar a pilha, basta desempilhar até esvaziá-la e então liberar a struct. A capacidade passada a criaPilha é ignorada: ela só existe para que as duas versões tenham exatamente a mesma interface.

O main.c e o Pilha.h abaixo são os mesmos da versão com vetor; só o Pilha.c mudou. A saída também é a mesma.

Pilha com lista encadeadaC
Pergunta

Por que o topo da pilha é o início da lista, e não o fim?

Ver dicaEsconder dica

Pense em quanto custa cada operação no fim de uma lista simplesmente encadeada, mesmo com um ponteiro fim.

Ver respostaEsconder resposta

No início, inserir e remover custam sem nenhum campo extra. No fim, inserir até seria com um ponteiro fim, mas remover o último nó exige achar o penúltimo, e isso só se faz percorrendo a lista desde o começo, em . Como a pilha insere e remove na mesma ponta, essa ponta tem que ser o início.

Vetor ou lista encadeada?

VetorLista encadeada
Operaçõestodas todas
Capacidadefixa, definida na criaçãocresce e diminui livremente
Memóriao vetor inteiro, mesmo com a pilha quase vaziaum nó (dado + ponteiro) por elemento
Alocaçõesuma só, na criaçãoum malloc por empilha e um free por desempilha

Na prática, a versão com vetor costuma ser mais rápida (os elementos ficam contíguos na memória e não há alocação a cada operação), e a versão com lista é a escolha natural quando não há um limite razoável para o tamanho.

Questão

A partir de uma pilha vazia, são feitas as operações abaixo, e cada valor desempilhado é impresso.

empilha(p, 1);
empilha(p, 2);
printf("%d ", desempilha(p));
empilha(p, 3);
empilha(p, 4);
printf("%d ", desempilha(p));
printf("%d ", desempilha(p));

O que é impresso?

Aplicação: delimitadores balanceados

Compiladores precisam verificar se os delimitadores de um programa (parênteses, colchetes e chaves) estão "casados": cada um que abre tem que ser fechado, pelo delimitador do mesmo tipo e na ordem certa. Por exemplo,

a = a + (c + d) * (e - f);
g[10] = h[i[9]] + (j + k)*1;
while (m < (n[8] + o) ) { p = (7 + 2) * 3; }

estão corretos, enquanto

a = a + (c + d) * (e - f));
g[10] = h[i[9]] + (j + k*1;
while (m < (n[8) + o] ) { p = (7 + 2) * 3; }

não estão: na primeira linha sobra um ), na segunda falta um ), e na terceira o ] fecha antes do ( que foi aberto depois do [.

Contar não basta

Na terceira linha incorreta, há a mesma quantidade de ( e ), de [ e ] e de { e }. Mesmo assim, os delimitadores estão cruzados, e não aninhados. A ordem importa tanto quanto a quantidade.

O delimitador que precisa ser fechado primeiro é sempre o último que foi aberto e ainda não foi fechado. "Último a entrar, primeiro a sair": é uma pilha. O algoritmo percorre a expressão e:

  • ao encontrar um delimitador de abertura, empilha-o;
  • ao encontrar um de fechamento, compara-o com o topo da pilha: se a pilha estiver vazia ou o topo for de outro tipo, a expressão está mal formada; senão, desempilha e continua;
  • ignora os demais caracteres.

No fim, a expressão está bem formada se a pilha estiver vazia (senão, sobrou alguém sem fechamento). Implementar esse algoritmo é o primeiro exercício abaixo.

Exercícios

Encontre e corrija o erro

Programa 1

Deveria empilhar 1, 2 e 3 e recusar o quarto elemento, porque a capacidade é 3.

pilha_cheia.cC
Resposta

O teste de pilha cheia está deslocado de uma posição. Com topo guardando o índice do topo, a pilha está cheia quando topo == capacidade - 1 (no exemplo, topo == 2). Com topo == capacidade, o quarto empilha passa no teste, faz topo = 3 e grava em vetor[3], uma posição depois do fim do vetor (heap-buffer-overflow). O correto é:

if (p->topo == p->capacidade - 1) {

Programa 2

desempilha.cC
Resposta

desempilha libera o nó e depois lê removido->dado: a leitura acontece em memória já devolvida ao sistema (heap-use-after-free). O valor tem que ser guardado antes do free:

No *removido = p->inicio;
int valor = removido->dado;
p->inicio = removido->proximo;
free(removido);
p->tamanho--;
return valor;

Programa 3

libera.cC
Resposta

A saída está certa, mas o programa termina com um vazamento de memória: liberaPilha só libera a struct, e os cinco nós continuam alocados, sem nenhum ponteiro para eles. É preciso liberar os nós antes, como em liberaLista:

void liberaPilha(Pilha *p) {
    No *atual = p->inicio;
    while (atual != NULL) {
        No *proximo = atual->proximo;
        free(atual);
        atual = proximo;
    }
    free(p);
}

Implemente

Delimitadores balanceados. Implemente bemFormada, que recebe uma string e diz se os delimitadores ( ), [ ] e { } dela estão bem formados, usando o algoritmo da seção anterior e a pilha (com lista encadeada) que já está pronta em Pilha.c. Os testes incluem linhas com centenas de milhares de delimitadores e verificam se não há vazamento de memória: libere a pilha também quando a função retornar antes de chegar ao fim da string.

Delimitadores balanceadosC
Executa o seu código uma vez, com os dados de entrada abaixo. Serve para experimentar; não conta como resposta.Executa o seu código em todos os casos de teste (os de exemplo e os ocultos) e mostra quais passaram.

Pilha com mínimo. Implemente uma pilha que, além das operações usuais, informa o menor valor guardado nela, com todas as operações em . Percorrer a pilha a cada consulta do mínimo não serve: os testes fazem centenas de milhares de operações. Use a pilha de Pilha.c para guardar os valores (e o que mais for preciso).

Como achar o mínimo sem percorrer?

Guardar só o menor valor em um campo funciona enquanto se empilha. O que dá errado quando esse menor valor é desempilhado?

Ver dica 1Esconder dica 1

Depois de desempilhar o mínimo, o novo mínimo é o que era o mínimo antes de ele ser empilhado. Essa informação precisa estar guardada em algum lugar.

Ver dica 2Esconder dica 2

Para cada valor empilhado, guarde também qual era o mínimo da pilha naquele momento. E onde guardar uma sequência de valores que é consumida na ordem inversa da que foi produzida?

Pilha com mínimoC
Executa o seu código uma vez, com os dados de entrada abaixo. Serve para experimentar; não conta como resposta.Executa o seu código em todos os casos de teste (os de exemplo e os ocultos) e mostra quais passaram.

Referências

  • W. Celes, R. Cerqueira e J. L. Rangel. Introdução a Estruturas de Dados: com técnicas de programação em C. 2ª ed. Grupo GEN, 2016.
  • A. Backes. Estrutura de Dados Descomplicada em Linguagem C. Elsevier, 2016.
  • Parênteses balanceados. Resolução de Problemas com Algoritmos e Estruturas de Dados usando Python.

Nesta página