Listas em Python... E pilhas também!!!
- #Python
Olá, dev!
A Competição de artigos desta semana é sobre funções em Python.
Para escrever este artigo, eu escolhi mostrar falar da estrutura lista do Python e vou mostrar como se implementa uma pilha em Python, usando listas.
Além disso, eu também vou mostrar como se implementava uma pilha antigamente, com as linguagens FORTRAN e C.
Sumário
1. Introdução
2. Estrutura de dados em programação
3. A estrutura de dados LISTA em Python
4. A estrutura de dados PILHA
5. Implementação da pilha em Python
6. Implementação da pilha no passado
7. Considerações finais
8. Referências
1 – Introdução
A Competição de artigos desta semana é sobre funções em Python.
Neste artigo, eu vou tratar da estrutura LISTA em Python. Para exemplificar o seu uso em Python, eu vou implementar um exemplo que a aproveita para implementar uma estrutura de dados do tipo pilha, usando listas.
Uma estrutura do tipo lista está presente em todas as linguagens de programação modernas, oferecidas de várias formas. Ela permite implementar algoritmos baseados em listas encadeadas, pilhas, filas, e outros modelos.
A pilha é uma estrutura bem simples, porém ela é muito importante em qualquer linguagem de programação, pois permite a implementação de operações que sigam o princípio LIFO (“Last In, First Out”, ou primeiro que entra é o primeiro que sai).
Eu também vou mostrar como se implementava uma pilha antigamente, nas linguagens FORTRAN e C.
2 – Estrutura de dados em programação
Segundo Groner [1], a programação de computadores envolve escrever uma sequência de instruções para que o processador as execute. As instruções são formadas por comandos, que determinam as ações que devem ser executadas, e os dados com os quais os comandos irão operar.
Cada linguagem de programação oferece diversos tipos de dados que podem ser usados para armazenar valores nas variáveis do programa. Entre os tipos de dados mais comuns, encontramos os inteiros, decimais, lógicos, caracteres, etc.
Quando o algoritmo a ser implementado (a sequência de instruções) precisa de dados mais complexos, temos que usar estruturas mais complexas do que aqueles tipos de dados oferecidos pela linguagem.
Nestes casos, além de criar novos tipos de dados, precisamos definir também quais as operações que poderão ser executadas nestes dados. Estes novos tipos de dados, juntamente com as operações que poderão ser usadas para operar com eles são as estruturas de dados.
A lógica utilizada para resolver um problema é que vai indicar quais são as estruturas de dados mais adequadas para a sua resolução. Uma solução pode ser criada usando uma estrutura de dados menos adequada para um determinado problema, só que o algoritmo escolhido para implementar o código pode ficar mais complexo.
Uma estrutura de dados mais adequada ao tipo de problema que se deseja resolver facilita o algoritmo para sua resolução e, consequentemente, o código ficará mais fácil e direto para escrever.
Por exemplo, para implementar uma árvore genealógica, a estrutura de dados adequada seria uma árvore, já para implementar uma rede social, a melhor forma seria usar a estrutura de grafos, e assim por diante.
Exemplos de estrutura de dados que não são disponibilizadas pelas são filas, pilhas, matrizes, árvores, grafos, etc.
Os problemas que tenham um modelo de dados parecido com estes tipos de estruturas serão modelados de forma mais adequada com dados destes tipos.
Nestes casos, será preciso criar uma estrutura de dados adequada à solução do problema.
Por exemplo, se desejamos representar um modelo de rede social com grafos e a linguagem que iremos usar para codificar o problema não fornece um dado do tipo “grafo”, precisamos criar uma estrutura de dado para armazenar grafos e criar operações sobre este dado grafo.
E isso precisa ser feito usando-se os tipos de dados oferecidos pela linguagem.
No caso de uma pilha, a mesma coisa. Se a linguagem não oferece um dado do tipo PILHA, será preciso criar uma estrutura que armazene e tenha o comportamento de uma.
3 – A Estrutura de Dados LISTA em Python
De acordo com [2], a linguagem Python oferece vários tipos de dados para armazenar as variáveis de um programa. Além disso, também oferece várias estruturas de dados complexas, com um conjunto amplo de funções e métodos para manipulação dos valores armazenados.
As principais estruturas são:
· Listas;
· Tuplas;
· Dicionários.
As listas são coleções heterogêneas (de qualquer tipo, misturados) de itens, mutáveis (podem ser alteradas) e vamos falar detalhar mais sobre elas adiante. As tuplas são semelhantes às listas, mas são imutáveis (NÃO podem ser alteradas). Já os dicionários são listas de associações (chaves, valores), com chaves únicas e valores de qualquer tipo.
A lista (“list”) da linguagem Python implementa um vetor unidimensional de valores de qualquer tipo, inclusive misturados. Algumas operações que podemos implementar para uma lista são descritas a seguir, segundo a documentação oficial do Python [3], [4].
Criação de uma variável do tipo lista
Uma variável lista pode ser definida com conteúdo vazio ou inicializada com valores, entre colchetes, como os exemplos a seguir:
vetor_vazio = [ ]
vetor_simples = [2, 1, 3]
vetor_de_livros = [“Código Limpo”, “Python Fluente”, “Arquitetura Limpa”]
vetor_heterogeneo = [10, 15, “DIO”, [1, 2, 3, 4], “100”]
Acesso aos valores armazenados
Como todas as linguagens modernas, os índices do vetor são inteiros consecutivos, iniciados em 0. O acesso a um elemento individual é feito por meio de seu índice, entre colchetes.
Os exemplos a seguir serão sempre usarão os vetores originais criados na seção anterior, ou seja, sem considerar as possíveis alterações aplicadas pelos comandos dos exemplos.
print( vetor_de_inteiros )
será impresso: [2, 1, 3]
print( vetor_de_livros[2] )
Será impresso: Python Fluente
O acesso aos valores armazenados no vetor também pode ser feito com índices negativos, sendo percorrido o vetor na ordem inversa. O índice do último item é sempre -1, o do penúltimo, -2, e assim por diante. Veja os exemplos:
print( vetor_de_livros[-1] )
Será impresso: Arquitetura Limpa
print( vetor_de_livros[-2] )
Será impresso: Python Fluente
Manipulação dos itens de uma lista
O Python oferece várias funções e métodos para a manipulação de itens armazenados em uma lista.
A função mais comum aplicada a uma lista é:
· len(lista) – retorna o comprimento (“length”) da lista, ou seja, a quantidade de itens armazenados nela.
Ex1: len(vetor_de_livros), retornando 3 (3 itens).
Ex2: len(vetor_heterogeneo), retornando 5 (5 itens, note que a lista [1, 2, 3, 4] é apenas um dos itens dessa lista).
Já os métodos mais comumente aplicados a uma lista são:
· append(item) – insere um item após o último item armazenado.
Ex: vetor_simples.append(8), resultando em [2, 1, 3, 8].
· clear( ) – Remove todos os itens da lista, resultando em uma lista vazia.
Ex: vetor_heterogeneo.clear( ), resultando em [ ].
· index(item) – Retorna o índice do primeiro elemento da lista cujo valor é igual a item.
Ex: vetor_heterogeneo.index(“DIO”), resultando em 2.
· insert(i, item) – Insere um item na posição i.
Ex: vetor_simples.insert(2, 25), resultando em [2, 1, 25, 3 ].
· pop(i) – remove da lista o item armazenado na posição i. Sem parâmetro, remove o último item da lista. O valor do item removido é retornado pelo método e pode ser utilizado no programa.
Ex1: vetor_heterpogeneo.pop(1), resultando em = [10, “DIO”, [1, 2, 3, 4], “100”].
Ex2: vetor_de_livros.pop(), resultando em [“Código Limpo”, “Python Fluente”] .
· remove(item) – Remove o primeiro elemento da lista que tem valor item.
Ex: vetor_simples.remove(3), resultando em [1, 2].
· sort( ) – Ordena os itens da lista. Os critérios da ordenação podem ser alterados por parâmetros adicionais.
Ex: vetor_simples.sort( ), resultando em [ 1, 2, 3 ].
4 – A estrutura de dados PILHA
Este artigo só enfocará a estrutura de dados do tipo pilha, embora existam outras estruturas de dados complexas que possam ser criadas para resolver problemas em uma linguagem.
Um exemplo simples (e prático) de uma pilha pode ser uma pilha de livros (ou de pratos). Quando você coloca mais um livro na pilha, ele sempre será colocado no topo da pilha, sobre os outros livros que já estão lá. E quando você tira um livro, sempre pega o que está no topo da pilha.
Ou seja, um livro que entrou na pilha primeiro só vai sair dela quando todos os livros que estão sobre ele saírem de cima dele. Dizendo de outra forma, o último livro que foi colocado na pilha será o primeiro a ser retirado.
Em português resumido, o último que entra é o primeiro que sai. Em inglês, seria “Last In, First Out” (L.I.F.O. ou LIFO). Este é o comportamento esperado para uma estrutura do tipo pilha (“stack”, em inglês).
Numa pilha de números inteiros, o primeiro inteiro que entra é o primeiro inteiro que sai. Numa pilha de nomes, o primeiro nome que entra é o primeiro nome que sai. E assim por diante.
Em geral, as linguagens de programação não oferecem este tipo de estrutura de dados nativamente. É preciso construir uma estrutura, a partir dos tipos de dados oferecidos.
Uma das formas mais simples de criar uma pilha é com um vetor (“array”). Um vetor é uma estrutura de dados homogênea (só armazena dados do mesmo tipo), finita, e sequencial.
Para ilustrar, veja esse exemplo de uma pilha a seguir:
Esta é uma pilha de 5 posições, para armazenar valores inteiros. 3 delas já estão ocupadas. O primeiro valor a entrar na pilha foi o número 12, seguido do 5 e finalmente, do 23, que foi o último número colocado na pilha.
Então, pode-se dizer que o número que está na base da pilha é o 12 e o que está no seu topo é o 23.
Uma pilha só possui 2 comportamentos:
· adicionar um novo elemento à pilha (empilhar; em inglês: “push”, que significa empurrar). O novo elemento adicionado entrará 1 posição após o topo da pilha. O número de elementos da pilha aumentará de 1 unidade;
· remover um elemento da pilha (desempilhar; em inglês: “pop”, que significa estourar). Um elemento removido da pilha sairá dela e o elemento anterior a ele ficará no seu lugar no topo da pilha. O número de elementos da pilha diminuirá de 1 unidade.
Para adicionar um novo número na pilha, por exemplo, 8, ele entrará após o valor do topo da pilha (23). A pilha passará a ter 4 posições ocupadas e o número 8 ficará no seu topo (veja a figura).
Agora, para remover um elemento da pilha, terá que ser removido o elemento do topo (8). A pilha voltará a ter apenas 3 elementos e o número 23 estará novamente no topo da pilha, sendo o próximo a ser removido, caso essa operação seja repetida.
Realizando uma nova operação de desempilhamento:
E a pilha ficará assim:
Para adicionar mais um elemento novo na pilha (10), ele entrará após o valor do topo da pilha (5). A pilha passará a ter 3 posições ocupadas novamente e o número 10 ficará no seu topo:
Existem duas situações especiais que podem ocorrer com uma pilha:
· Pilha cheia - Se a pilha tiver uma quantidade finita de posições (como nesse exemplo, 5 posições) e todas as posições estiverem ocupadas. Nesse caso, não será possível adicionar um novo elemento;
· Pilha vazia - Se a pilha ainda não tiver nenhum elemento armazenado. Neste caso, não será possível remover um elemento.
Na implementação de uma pilha, estas condições devem ser identificadas pelo programa e tratadas de acordo.
Representando as operações do exemplo acima por um vetor horizontal, fica mais fácil associar a implementação de uma pilha a um vetor oferecido por uma linguagem de programação:
A pilha é uma estrutura de dados muito importante para a programação, seja em aplicações típicas do uso dela ou então como auxiliar em aplicações que usam outras estruturas.
Segundo Karumanchi [5], algumas destas aplicações são:
· Implementação de uma calculadora com notação polonesa (ou pós-fixada – POSIX), baseada na notação reversa, proposta pelo matemático polonês Jan Lukasiewicz, para se escrever expressões aritméticas sem o uso de parênteses. Por esta notação, primeiro são apresentados os operandos e só depois, o operador que da operação que vai ser aplicada;
· Balanceamento de pares de parênteses (de símbolos, como colchetes e chaves também);
· Conversão de bases numéricas;
· Implementar recursão em funções;
· Reverter cadeias de caracteres (“strings”);
· Problemas usando backtracking, revisitando caminhos já passados;
· Desfazer ações.
· Armazenamento de variáveis e chamadas de métodos após ocorrer exceções de stack overflow;
Da mesma forma que a pilha, podemos implementar também o tipo conhecido fila, que opera da forma FIFO (“First In, First Out”), ou seja, o primeiro que entra é o primeiro que sai. Um exemplo deste comportamento é uma fila de banco (ou de cinema).
5 – Implementação de uma PILHA em Python
Python não possui uma estrutura de dados específica para implementação de uma pilha. No entanto, a linguagem, oferece a estrutura lista, que facilita muito a implementação de estruturas do tipo listas encadeadas, pilhas e filas.
Usando uma variável do tipo lista (LIST), podemos simular as operações requeridas por uma pilha.
Versão 1 – usando programação procedural
Esta versão simples usa um vetor para implementar a pilha, de tamanho indefinido, semelhante à figura abaixo, só que na figura a pilha tem um tamanho definido.
# código em python para gerar uma pilha e testar com números inteiros
def empilhar(pilha, elemento):
pilha.append(elemento)
def desempilhar(pilha):
if len(pilha) > 0:
return pilha.pop()
else:
return None
def esta_vazia(pilha):
return len(pilha) == 0
def imprimir(pilha):
print(pilha)
# Programa de teste com inúmeros inteiros
pilha1 = []
empilhar(pilha1, 10)
empilhar(pilha1, 20)
empilhar(pilha1, 30)
print(pilha1) # imprime [10, 20, 30 ]
desempilhar(pilha1)
print(pilha1) # imprime [10, 20 ]
desempilhar(pilha1)
print(pilha1) # imprime [ 10 ]
desempilhar(pilha1)
print( esta_vazia(pilha1) ) # imprime True
imprimir(pilha1) # imprime [ ]
empilhar(pilha1, 40)
imprimir(pilha1) # imprime [ 40 ]
Versão 2 – usando classes (POO)
Esta versão usou uma classe para o tipo de dado pilha, baseado neste exemplo geral da figura abaixo:
# código em python para gerar uma pilha, usando classes (POO) e testar com strings (palavras)
class Pilha:
def __init__(self):
self.pilha = []
def empilhar(self, elemento):
self.pilha.append(elemento)
def desempilhar(self):
if len(self.pilha) > 0:
return self.pilha.pop()
else:
return None
def esta_vazia(self):
return len(self.pilha) == 0
def tamanho(self):
return len(self.pilha)
def topo(self):
if len(self.pilha) > 0:
return self.pilha[-1]
else:
return None
def imprimir(self):
print(self.pilha)
# programa de teste usando strings (palavras)
pilha2 = Pilha( )
pilha2.empilhar( "Código Limpo" )
pilha2.empilhar( "Python Fluente" )
pilha2.empilhar( "Arquitetura Limpa" )
pilha2.imprimir( ) # imprime ["Código Limpo", "Python Fluente", "Arquitetura Limpa" ]
print(pilha2.topo( )) # imprime Arquitetura Limpa
pilha2.desempilhar( )
print(pilha2.topo( )) # imprime Python Fluente
pilha2.desempilhar( )
print(pilha2.topo( )) # imprime Código Limpo
pilha2.desempilhar( )
print(pilha2.esta_vazia( )) # imprime True
pilha2.imprimir( ) # imprime [ ]
pilha2.empilhar("Não Me Faça Pensar")
print(pilha2.tamanho( )) # imprime 1
pilha2.imprimir( ) # imprime ["Não Me Faça Pensar"]
OBS: Estes dois programas foram testados no Google Colab e rodaram sem erros.
6 – Implementação da pilha no passado
Como eu sou um bom dinossauro (Opa! Isso dá um bom nome para um filme!), já programei pilhas em várias linguagens, quase o mesmo número de vezes que já programei “Hello, world!”.
Agora, vou mostrar como era programar uma pilha em linguagens mais antigas, como FORTRAN e C, que não ofereciam nenhuma estrutura de dados adequada que pudesse facilitar a tarefa.
Em 1980, aprendi a programar em FORTRAN, numa das versões iniciais da linguagem (ver capa do livro na figura abaixo), perfurando cartões para um mainframe. Naquela época, a única estrutura disponibilizada pela linguagem para programação de um conjunto de objetos, homogêneos, era o vetor (ou “array”).
Um programa em FORTRAN para criar uma pilha de inteiros, com funções básicas para inserir e remover elementos está mostrado no código a seguir:
program PilhaInteiros
implicit none
integer, parameter :: MAX_ITENS = 5
integer :: pilha(MAX_ITENS)
integer :: topo
integer :: escolha, valor
! Inicializa o topo da pilha
topo = 0
! Menu de ações
do
write(*,*) '----------------------'
write(*,*) '1 - Empilhar (PUSH)'
write(*,*) '2 - Desempilhar (POP)'
write(*,*) '3 - Exibir Pilha (SHOW)'
write(*,*) '4 - Sair (QUIT)'
write(*,*) 'Escolha uma opção (1-4): '
read(*,*) escolha
select case(escolha)
! Opção Empilhar
case(1)
if (topo < MAX_ITENS) then
write(*,*) 'Digite o valor a ser empilhado: '
read(*,*) valor
topo = topo + 1
pilha(topo) = valor
write(*,*) 'Valor empilhado!'
else
write(*,*) 'PILHA CHEIA! Não pode empilhar mais itens.'
end if
! Opção Desempilhar
case(2)
if (topo > 0) then
write(*,*) 'Valor desempilhado: ', pilha(topo)
topo = topo - 1
else
write(*,*) 'PILHA VAZIA! Não tem item para desempilhar.'
end if
! Opção Exibir
case(3)
if (topo > 0) then
write(*,*) 'Conteúdo da pilha (primeiro item listado é topo da pilha):'
do valor = topo, 1, -1
write(*,*) pilha(valor)
end do
else
write(*,*) 'PILHA VAZIA!'
end if
! Opção Sair
case(4)
write(*,*) 'Fim do programa.'
exit
case default
write(*,*) 'Opção inválida! Tente novamente.'
end select
end do
end program PilhaInteiros
Em 1983, aprendi a programar na linguagem C, com o famoso livro de capa cor de rosa de Byan Kernigham e Richie (ver capa do livro na figura abaixo). Também numa das versões iniciais da linguagem, mas, já em um computador mais amigável e em terminal de texto. A linguagem oferecia um tipo de dados em que o usuário poderia criar sua própria estrutura de dados, por programação, chamada de struct, e toda estrutura de conjunto de dados, como vetores ou cadeia de caracteres (“string”), era implementada pelos famigerados apontadores (“pointers”).
Naquela época, a única estrutura disponibilizada pela linguagem para programação de um conjunto de objetos, homogêneos, era o vetor (ou “array”).
Uma pilha poderia ser implementada em C por meio de um vetor homogêneo, estático (tamanho fixo definido), ou por uma estrutura complexa do tipo lista encadeada, criando uma estrutura específica para isso, inclusive de um tipo único (homogêneo).
Um código de uma pilha baseada em lista encadeada e estrutura criada pelo usuário é mostrado no código a seguir:
#include <stdio.h>
#include <stdlib.h>
// estrutura criada para ser um nó da pilha
struct stackNode {
int data; // define data as an int
struct stackNode *nextPtr; // stackNode pointer
};
typedef struct stackNode StackNode; // synonym for struct stackNode
typedef StackNode *StackNodePtr; // synonym for StackNode*
// protótipos
void push(StackNodePtr *topPtr, int info);
int pop(StackNodePtr *topPtr);
int isEmpty(StackNodePtr topPtr);
void printStack(StackNodePtr currentPtr);
void mostraMenu(void);
// Programa Principal
int main(void){
StackNodePtr stackPtr = NULL; // points to stack top
int valor = 0; // int input by user
mostraMenu();
printf("%s", "> ");
int escolha = 0;
scanf("%d", &escolha);
// Loop do programa
while (escolha != 3) {
switch (escolha) {
case 1: // Opção Empilhar
printf("%s", "Informe um inteiro: ");
scanf("%d", &valor);
push(&stackPtr, valor);
printStack(stackPtr);
break;
case 2: // Desempilhar
// if stack is not empty
if (!isEmpty(stackPtr)) {
printf("Desempilhado -> %d.\n", pop(&stackPtr));
}
printStack(stackPtr);
break;
default:
puts("OPÇÃO INVÁLIDA!.\n");
mostraMenu();
break;
}
printf("%s", "> ");
scanf("%d", &escolha);
}
puts("Fim do programa.");
}
// Mostra o menu
void mostraMenu(void) {
puts("Escolha uma opção:\n"
"1 Empilhar (push)\n"
"2 Desempilhar (pop)\n"
"3 Sair do programa");
}
// Insere um nó no topo da pilha
void push(StackNodePtr *topPtr, int info) {
StackNodePtr newPtr = malloc(sizeof(StackNode));
if (newPtr != NULL) {
newPtr->data = info;
newPtr->nextPtr = *topPtr;
*topPtr = newPtr;
}
else {
printf("%d não inserido. Sem memória disponível.\n", info);
}
}
// Remove um nó do topo da pilha
int pop(StackNodePtr *topPtr) {
StackNodePtr tempPtr = *topPtr;
int popValue = (*topPtr)->data;
*topPtr = (*topPtr)->nextPtr;
free(tempPtr);
return popValue;
}
// Imprime o conteúdo da pilha
void printStack(StackNodePtr currentPtr) {
if (currentPtr == NULL) {
puts("PILHA VAZIA!\n");
}
else {
puts("Conteúdo da pilha:");
printf("[Topo ->]: ");
while (currentPtr != NULL) { // Varre o conteúdo da pilha, do topo para a base
printf("%d , ", currentPtr->data);
currentPtr = currentPtr->nextPtr;
}
puts("[<- Base]\n");
}
}
// Testa se a pilha está vazia
int isEmpty(StackNodePtr topPtr) {
return topPtr == NULL;
}
Em C, um a estrutura (tipo “struct”) permite ao programador criar um tipo de dado próprio, específico para sua aplicação, podendo misturar vários tipos de dados nele.
Uma struct pode ter, também, um apontador para uma estrutura do mesmo tipo, possibilitando a criação de listas encadeadas, onde cada nó é uma instância da estrutura original.
Este programa usou uma estrutura para criar um tipo de nó para um item da pilha e um apontador para uma estrutura de pilha, criando uma lista de nós do mesmo tipo, mas usada com os comportamentos de uma pilha (ver figura).
OBS: Estes dois programas foram testados no Replit e rodaram sem erros.
7 – Considerações finais
Neste artigo, eu tratei da estrutura LISTA em Python. Foram apresentadas as características gerais desse tipo de dado na linguagem Python, bem como as operações que podem ser usadas com ela.
No entanto, o foco maior foi dado em uma das possíveis utilizações do tipo lista, que é a pilha.
Embora esta estrutura não esteja presente, nativamente, em nenhuma linguagem, ela pode ser implementada a partir de vetores (“arrays”), presentes em todas as linguagens, ou listas, presentes em muitas linguagens modernas.
Para exemplificar o uso do tipo lista em Python, foram implementados dois exemplos de pilhas com a linguagem, uma usando vetores e outra usando classes.
Como eu conheço outras linguagens antigas, também implementei 2 exemplos de pilhas, como era costume nos anos 80, usando a linguagens FORTRAN e C.
Embora a pilha seja uma estrutura muito simples, seu uso na programação é muito importante, pois ela implementa operações do tipo LIFO (“Last In, first Out”), ou seja, o primeiro elemento que entra é o primeiro que sai.
Da mesma forma que a pilha, com pequenas alterações, podemos implementar também o tipo conhecido FILA.
8 – Referências
[1] Loiane Groner, Learning Javascript Data Structures and Algorithms, 3rd Edition, Birmingham, 2018, Packt Publishing.
[2] Luciano Ramalho, Python Fluente, São Paulo, 2015, Novatec.
[3] Python.org, 3.1.3. Lists. Disponível em: <https://docs.python.org/3/tutorial/introduction.html#lists>. Acessado em: 10/01/2024.
[4] Python.org, 5.1. More on Lists. Disponível em: <https://docs.python.org/3/tutorial/datastructures.html#more-on-lists>. Acessado em: 10/01/2024.
[5] Narasimha Karumanchi, Data Structures And Algorithms Made Easy, 5th Edition, Bombay, 2015, CareerMonk Publishing.