Escolar Documentos
Profissional Documentos
Cultura Documentos
Ricardo Brito
2023
SUMÁRIO
Capítulo 1. Introdução a Algoritmos ....................................................................... 7
Algoritmos do Google.............................................................................................................. 10
Notação Assintótica................................................................................................................. 19
Estruturas de Listas................................................................................................................. 25
Variáveis Subscritas................................................................................................................. 33
2
Loop em Arrays........................................................................................................................... 38
Arrays Multidimensionais...................................................................................................... 39
Deques ........................................................................................................................................... 61
Interseção .................................................................................................................................... 74
Diferença ....................................................................................................................................... 75
Diferença Simétrica.................................................................................................................. 75
Subconjuntos .............................................................................................................................. 76
3
Capítulo 7. Dicionário e Hashes .............................................................................. 80
Operando Dicionários.............................................................................................................. 82
Árvores balanceadas..............................................................................................................114
Rotação ........................................................................................................................................117
Matriz de adjacências............................................................................................................126
4
Lista de adjacências...............................................................................................................130
Matriz de incidências.............................................................................................................133
Percorrendo grafos.................................................................................................................134
5
1
Capítulo 1. Introdução a Algoritmos
Entendendo os Algoritmos
A jovem Ada Lovelace foi apresentada à sociedade inglesa como filha
única do poeta Lord Byron em 1815. Mais de 200 anos depois, ela é lembrada
por muitos como a primeira programadora da história da computação.
7
O que é um Algoritmo?
Um algoritmo é um procedimento usado para resolver um problema
ou executar uma computação. Os algoritmos agem como uma lista exata de
instruções que conduzem ações especificadas passo a passo em rotinas
baseadas em hardware ou software.
8
Algoritmos como Tecnologia
Algoritmos são uma grande parte do mundo de hoje. Ao fornecer às
nossas ferramentas tecnológicas diárias as instruções descritivas de que
precisam para realizar tarefas específicas, somos capazes de automatizar
muitos dos processos que os seres humanos tiveram que fazer
manualmente por milhares de anos.
9
Algoritmos do Google
O algoritmo de busca do Google foi desenvolvido pelos fundadores
do Google (Larry Page e Sergei Brin) na década de 1990. Esse algoritmo é
chamado de PageRank.
10
em um site. Porém, o contrário também é verdadeiro, sites com conteúdo de
baixa qualidade são penalizados. No seu site, deve oferecer conteúdo
informativos e diferenciadores para ser melhor referenciado. Conteúdo
duplicado, texto mal formulado ou imagens de baixa qualidade, são
penalizados. Conteúdo impróprio também não se sai bem. A última
atualização deste algoritmo data de 2015, mas não é menos importante.
11
Figura 1 – Algoritmos do Google.
Algoritmos Clássicos
O algoritmo de busca em largura ou BFS é o método mais
amplamente utilizado. BFS é uma abordagem de travessia de gráfico na qual
você começa em um nó de origem e camada por camada através do gráfico,
analisando os nós diretamente relacionados ao nó de origem. Então, na
travessia do BFS, você deve passar para os nós vizinhos do próximo nível.
12
A busca em largura usa uma estrutura de dados de fila para
armazenar o nó e marcá-lo como “visitado” até marcar todos os vértices
vizinhos diretamente relacionados a ele. A fila opera segundo o princípio
First In First Out (FIFO), de modo que os vizinhos do nodo serão visualizados
na ordem em que ele os insere no nodo, começando pelo nodo que foi
inserido primeiro.
13
Algoritmos em Computadores Quânticos
Dentro de alguns anos, os computadores quânticos poderiam
alcançar ou até superar os computadores clássicos, graças ao trabalho
significativo em hardware e nos algoritmos para executá-los.
14
Existem dois métodos usados, complexidade de tempo e
complexidade de espaço:
Complexidade Espacial
A resolução de problemas usando o computador requer memória
para armazenar dados temporários ou o resultado enquanto o programa está
em execução. A quantidade de memória requerida pelo algoritmo para
resolver determinado problema é chamada de complexidade espacial do
algoritmo.
15
A complexidade de espaço de um algoritmo quantifica a quantidade
de espaço ocupado por um algoritmo para executar como uma função do
comprimento da entrada. Considere um exemplo: suponha um problema
para encontrar a frequência dos elementos da matriz. É a quantidade de
memória necessária para a conclusão de um algoritmo.
16
o melhor caso ocorre quando x está presente no primeiro local. O número de
operações no melhor caso é constante (não dependente de n). Portanto, a
complexidade de tempo no melhor caso seria Ω(1)
17
Algoritmos Polinomiais e Exponenciais
A complexidade de tempo de um algoritmo é expressa como a
quantidade de tempo que um algoritmo leva para algum tamanho de entrada
para o problema. A notação Big O é comumente usada para expressar a
complexidade de tempo de qualquer algoritmo, pois suprime os termos de
ordem inferior e é descrita assintoticamente. A complexidade do tempo é
estimada contando as operações (fornecidas como instruções em um
programa) executadas em um algoritmo. Aqui, cada operação leva um tempo
fixo de execução. Geralmente as complexidades de tempo são classificadas
como constantes, lineares, logarítmicas, polinomiais, exponenciais etc.
Entre esses, o polinômio e o exponencial são os mais proeminentemente
considerados e define a complexidade de um algoritmo. Esses dois
parâmetros para qualquer algoritmo são sempre influenciados pelo
tamanho da entrada.
18
Tempo de execução exponencial
O conjunto de problemas que podem ser resolvidos por algoritmos
de tempo exponencial, mas para os quais nenhum algoritmo de tempo
polinomial é conhecido. Um algoritmo é dito ser de tempo exponencial, se T
(n) é limitado superiormente por 2 poly (n), onde poly(n) é algum polinômio
em n. Mais formalmente, um algoritmo é de tempo exponencial se T (n) é
limitado por O (2 nk ) para alguma constante k.
Notação Assintótica
A eficiência de um algoritmo depende da quantidade de tempo,
armazenamento e outros recursos necessários para executar o algoritmo. A
eficiência é medida com a ajuda de notações assintóticas.
19
As notações assintóticas são as notações matemáticas usadas para
descrever o tempo de execução de um algoritmo quando a entrada tende a
um valor específico ou a um valor limite.
Notação Big-O.
Notação Ômega.
Notação Theta.
20
Uma vez que fornece o pior tempo de execução de um algoritmo, é
amplamente utilizado para analisar um algoritmo, pois estamos sempre
interessados no pior cenário.
21
Figura 3 – Notação Assintótica.
Notação o - Little O
Notação o - Little O é uma notação matemática usada para descrever
a complexidade de algoritmos. Ela é usada para descrever o tempo de
execução de um algoritmo em relação ao tamanho da entrada. Ela é usada
22
para comparar algoritmos com o mesmo tamanho de entrada, mas com
diferentes taxas de crescimento de tempo de execução. A notação o - Little
O é usada para descrever o limite superior da taxa de crescimento.
23
2
Capítulo 2. Listas
Estruturas de Listas
As listas são usadas para armazenar dados de diferentes tipos de
dados de maneira sequencial. Existem endereços atribuídos a cada
elemento da lista, que é chamada de índice. O valor do índice começa em 0
e vai até o último elemento chamado de índice positivo. Há também uma
indexação negativa que começa em -1, permitindo que você acesse os
elementos do último ao primeiro.
Um loop for é usado para iterar sobre sequências como uma lista,
tupla, conjunto etc. E não apenas as sequências, mas qualquer objeto
iterável também pode ser percorrido usando um loop for.
Inicializando Lista
Criando uma lista: para criar uma lista, você usa os colchetes e
adiciona elementos a ela de acordo. Se você não passar nenhum elemento
dentro dos colchetes, obterá uma lista vazia como saída.
25
Se você quiser o elemento de volta, use a função pop() que recebe o
valor do índice. Para remover um elemento por seu valor, você usa a função
remove().
Operando Lista
Outras funções: você tem várias outras funções que podem ser
usadas ao trabalhar com listas.
Tuplas
As tuplas são iguais às listas, com a exceção de que os dados, uma
vez inseridos na tupla, não podem ser alterados, aconteça o que acontecer.
A única exceção é quando os dados dentro da tupla são mutáveis, somente
então os dados da tupla podem ser alterados.
Loops em Listas
Os loops em listas podem ser:
26
List Comprehension.
Um loop while.
Loops For
Um loop for é usado para iterar sobre uma sequência (que pode ser
uma lista, uma tupla, um dicionário, um conjunto ou uma string). Isso é
menos parecido com a palavra-chave for em outras linguagens de
programação e funciona mais como um método iterativo encontrado em
outras linguagens de programação orientadas a objetos.
Apple
Mango
27
Banana
Peach
List Comprehension
A List Comprehension oferece uma sintaxe mais curta quando você
deseja criar uma lista com base nos valores de uma lista existente. É
semelhante ao loop for, no entanto, ele nos permite criar uma lista e
percorrê-la em uma única linha.
Vejamos um exemplo:
Apple juice
Mango juice
Banana juice
Peach juice
28
The list at index 1 contains a Mango
The list at index 2 contains a Banana
The list at index 3 contains a Peach
0 : Apple
1 : Mango
2 : Banana
3 : Peach
lst1 = [1, 2, 3, 4, 5]
29
lst2 = []
# Lambda function to square number
temp = lambda i:i**2
for i in lst1:
# Add to lst2
lst2.append(temp(i))
print(lst2)
Um loop while
Também podemos iterar em uma lista Python usando um loop while.
Confira o código:
Apple
Mango
Banana
Peach
30
3
Capítulo 3. Arrays
Estruturas de Arrays
É uma estrutura de dados mais simples possível em memória. Por
esse motivo, todas linguagens de programação tem um tipo de dado array
incluído.
32
Variáveis Subscritas
Arranjos, vetores e array.
Um único nome identifica uma série de posições de memória.
sum = 0
for i in range(n):
sum = sum + s[i]
temperaturaMedia = []
temperaturaMedia.append(31.9)
temperaturaMedia.append(35.4)
33
temperaturaMedia.append(26.4)
temperaturaMedia.append(24.3)
temperaturaMedia.append(22.9)
for i in temperaturaMedia:
print(i)
Acessando Elementos em Arrays
Uma array ou matriz também é uma estrutura de dados que
armazena uma coleção de itens.
ou
import numpy as np
34
array('i', [3, 6, 9, 12])
<class 'array.array'>
import numpy as np
Métodos de Arrays
Python tem um conjunto de métodos integrados que você pode usar
em listas/arrays.
35
Python não tem suporte integrado para Arrays, mas Lista em Python
pode ser usado em seu lugar.
36
Removendo Elementos no Array
Em Python listas são mutáveis, logo, é possível inserir e remover
elementos delas.
Lista original: ['A', 'S', 'C', 'E', 'N', 'D', 'E', 'R']
Removendo um elemento: ['A', 'C', 'E', 'N', 'D', 'E', 'R']
Método Shift
Se quisermos deslocar para a direita ou esquerda os elementos de
um array NumPy, podemos usar o método numpy.roll() em Python.
37
Leva a matriz e o número de lugares onde queremos deslocar os
elementos do array e retorna a matriz deslocada.
import numpy as np
array = np.array([1,2,3,4,5])
array_new = np.roll(array, 3)
print(array_new)
[3 4 5 1 2]
Loop em Arrays
Os métodos que discutimos até agora usaram uma pequena lista.
38
É importante observar que o método aqui apresentado só pode ser
usado para arrays de tipos de dados únicos.
import numpy as np
1
2
3
4
5
Arrays Multidimensionais
Um Array Unidimensional é um tipo de matriz linear. Acessar seus
elementos envolve um único subscrito que pode representar um índice de
linha ou coluna.
39
Por exemplo, um array bidimensional é um array com duas
dimensões (linhas e colunas), enquanto um array tridimensional é um array
com três dimensões (altura, largura e profundidade).
Arrays multidimensionais:
Para criar uma matriz NumPy 1D, use a função array () e forneça um
argumento de itens de uma lista para ela.
40
Criando Array 1D:
# import numpy package
import numpy as np
# create NumPy 1D array which contain int value 2,4,6,8
arr_1D = np.array([2,4,6,8])
# print arr_1D
print(arr_1D)
[2 4 6 8]
Criando Array 2D:
# import numpy package
import numpy as np
# print arr_1D
print(arr_2D)
[[0 1]
[1 0]]
print(arr_3D)
[[0 1 1]
[1 0 1]
[1 1 0]]
41
4
Capítulo 4. Pilha
Estrutura de Dados em Pilha
Pilha é uma estrutura para armazenar um conjunto de elementos,
que funciona da seguinte forma:
ou
43
As modificações são armazenadas numa pilha para garantir a
sequência de retorno ou avanço. Nesses casos, a pilha é usada para manter
a sequência de alterações.
pilha = [1, 1, 2, 3, 5]
print("Pilha: ", pilha)
44
pilha.append(8)
print("Inserindo um elemento: ", pilha)
pilha.append(13)
print("Inserindo outro elemento: ", pilha)
pilha.pop()
print("Removendo um elemento: ", pilha)
pilha.pop()
print("Removendo outro elemento: ", pilha)
Pilha: [1, 1, 2, 3, 5]
Inserindo um elemento: [1, 1, 2, 3, 5, 8]
Inserindo outro elemento: [1, 1, 2, 3, 5, 8, 13]
Removendo um elemento: [1, 1, 2, 3, 5, 8]
Removendo outro elemento: [1, 1, 2, 3, 5]
45
Os métodos append() e pop() podem ser usados para empilhar e
desempilhar elementos.
PUSH = append()
# operações
Empilha(mp, 1)
Empilha(mp, "tipo de elemento")
Empilha(mp, (5, 4, 3))
Empilha(mp, True)
print(top(mp))
46
Pop de Elementos em Pilha
Uma pilha pode ser facilmente implementada usando uma lista em
Python. O topo da pilha é o último elemento e a base da pilha o primeiro.
POP = pop()
# operações
Desempilha(mp)
Desempilha(mp)
Desempilha(mp)
47
Desempilha(mp)
Desempilha(mp)
print(top(mp))
Código completo:
# Inicia uma pilha – vazia
def init(P):
P = []
48
# Desempilha e imprime o estado atual da pilha
def Desempilha(p):
x = pop(p)
if x is None: print("Vazia")
else: print(p)
# inicia a pilha
mp = []
init(mp)
# operações
Empilha(mp, 1)
Empilha(mp, "tipo de elemento")
Empilha(mp, (5, 4, 3))
Empilha(mp, True)
print(top(mp))
Desempilha(mp)
Desempilha(mp)
Desempilha(mp)
Desempilha(mp)
Desempilha(mp)
print(top(mp))
Será impresso:
[1]
[1, 'tipo de elemento']
[1, 'tipo de elemento', (5, 4, 3)]
[1, 'tipo de elemento', (5, 4, 3), True]
True
[1, 'tipo de elemento', (5, 4, 3)]
[1, 'tipo de elemento']
[]
Vazia
None
49
Como limpar uma pilha em Python?
def remover_pilha(pilha):
if pilha:
pilha.pop()
else:
return None
Exemplo de código:
pilha = [1, 2, 3, 4, 5]
while pilha:
pilha.pop()
print(pilha) # Esta pilha está agora vazia
50
def insere(self, novo_dado):
"""Insere um elemento no final da pilha."""
# Cria um novo nodo com o dado a ser armazenado.
novo_nodo = Nodo(novo_dado)
# Faz com que o novo nodo seja o topo da pilha.
novo_nodo.anterior = self.topo
# Faz com que a cabeça da lista referencie o novo nodo. self.topo =
novo_nodo
def remove(self):
"""Remove o elemento que está no topo da pilha."""
assert self.topo, "Impossível remover valor de pilha vazia."
self.topo = self.topo.anterior
def remove(self):
"""Remove o elemento que está no topo da pilha."""
assert self.topo, "Impossível remover valor de pilha vazia."
self.topo = self.topo.anterior
51
Insere o valor 2 no topo da pilha: [2 -> 1 -> 0 -> None]
Insere o valor 3 no topo da pilha: [3 -> 2 -> 1 -> 0 -> None]
Insere o valor 4 no topo da pilha: [4 -> 3 -> 2 -> 1 -> 0 -> None]
Removendo elemento que está no topo da pilha: [3 -> 2 -> 1 -> 0 ->
None]
Removendo elemento que está no topo da pilha: [2 -> 1 -> 0 -> None]
Removendo elemento que está no topo da pilha: [1 -> 0 -> None]
Removendo elemento que está no topo da pilha: [0 -> None]
Removendo elemento que está no topo da pilha: [None]
52
5
Capítulo 5. Filas e Deques
Estrutura de Dados em Fila
Filas são estruturas nas quais as inserções são feitas em um extremo
(final) e remoções são feitas no outro extremo (início).
Em uma fila, para que o primeiro elemento a entrar (ser inserido) seja
o primeiro a sair (ser removido), precisamos ser capazes de:
Buffers de Entrada/Saída.
Operações Principais
54
Desenfileirar(F): remove o elemento no início de F e retorna
esse elemento. Retorna null se não foi possível remover.
Operações Auxiliares
Resultado
55
Inserções acontecem à direita (no final) da fila e remoções ocorrem
à esquerda (no começo) da fila.
#incluindo a biblioteca
import queue
#criando a fila
fila = queue.Queue()
#adicionando elementos
fila.put('Elemento 1')
fila.put('Elemento 2')
fila.put('Elemento 3')
#obtendo o tamanho da fila
tamanho = fila.qsize()
print("Tamanho da Fila:", tamanho)
#removendo elementos
print("Removendo elementos da fila:")
while not fila.empty():
print(fila.get())
#verificando se a fila esta vazia
esta_vazia = fila.empty()
print("A fila esta vazia?", esta_vazia)
Tamanho da Fila: 3
Removendo elementos da fila:
Elemento 1
Elemento 2
Elemento 3
A fila esta vazia? True
56
No capítulo anterior, foi mostrado como implementar uma pilha
usando uma estrutura encadeada.
Exemplo em Python:
class Nodo:
"""Esta classe representa um nodo de uma estrutura duplamente encadeada."""
def __init__(self, dado=0, proximo_nodo=None):
self.dado = dado
self.proximo = proximo_nodo
def __repr__(self):
return '%s -> %s' % (self.dado, self.proximo)
class Fila:
"""Esta classe representa uma fila usando uma estrutura encadeada."""
def __init__(self):
self.primeiro = None
self.ultimo = None
def __repr__(self):
return "[" + str(self.primeiro) + "]"
57
# Faz com que o último da fila referencie o novo nodo.
self.ultimo = novo_nodo
class Nodo:
"""Esta classe representa um nodo de uma estrutura duplamente encadeada."""
def __init__(self, dado=0, proximo_nodo=None):
self.dado = dado
self.proximo = proximo_nodo
def __repr__(self):
return '%s -> %s' % (self.dado, self.proximo)
class Fila:
"""Esta classe representa uma fila usando uma estrutura encadeada."""
def __init__(self):
self.primeiro = None
self.ultimo = None
def __repr__(self):
return "[" + str(self.primeiro) + "]"
def remove(self):
"""Remove o último elemento da fila."""
assert self.primeiro != None, "Impossível remover elemento de fila vazia."
self.primeiro = self.primeiro.proximo
if self.primeiro == None:
self.ultimo = None
# Cria uma fila vazia.
fila = Fila()
print("Fila vazia: ", fila)
# Insere elementos na fila.
for i in range(5):
fila.insere(i)
print("Insere o valor {0} final da fila: {1}".format(i, fila))
# Remove elementos da fila.
while fila.primeiro != None:
fila.remove()
print("Removendo elemento que está no começo da fila: ", fila)
58
Insere o valor 2 final da fila: [0 -> 1 -> 2 -> None]
Insere o valor 3 final da fila: [0 -> 1 -> 2 -> 3 -> None]
Insere o valor 4 final da fila: [0 -> 1 -> 2 -> 3 -> 4 -> None]
Removendo elemento que está no começo da fila: [1 -> 2 -> 3 -> 4 -> None]
Removendo elemento que está no começo da fila: [2 -> 3 -> 4 -> None]
Removendo elemento que está no começo da fila: [3 -> 4 -> None]
Removendo elemento que está no começo da fila: [4 -> None]
Removendo elemento que está no começo da fila: [None]
59
Quando se trata de fila linear, a inserção pode ser realizada somente
a partir da extremidade traseira e a exclusão do front. Em uma fila completa,
após executar séries de deleções sucessivas na fila, surge uma certa
situação em que nenhum elemento novo pode ser adicionado ainda, mesmo
se o espaço estiver disponível, porque a condição de underflow (Rear = max
- 1) ainda existe.
Com isso, a fila circular não gera a condição de estouro até que a fila
esteja cheia.
60
A inserção e exclusão dos elementos é fixada na fila linear, ou seja,
adição da extremidade traseira e exclusão da extremidade frontal.
Deques
Fila de prioridades
Deques são estruturas que permitem inserir e remover de ambos os
extremos.
Deques são filas duplamente ligadas, isto é, filas com algum tipo de
prioridade.
61
A implementação de Deque por alocação estática ou sequencial é
feita por meio de um arranjo de dimensão máxima predefinida e de duas
variáveis inteiras que indicam o topo e a base (head e tail, respectivamente).
Operações Principais
Operações Auxiliares
62
Fila de Prioridade
A fila de prioridade é um tipo avançado da estrutura de dados da fila.
Como implementar?
Quando usar?
63
Biblioteca heapq: https://docs.python.org/3/library/heapq.html
Heaps são árvores binárias para as quais cada nó pai tem um valor
menor ou igual a qualquer um de seus filhos.
64
6
Capítulo 6. Conjuntos
Estrutura de Conjunto de Dados
O conceito de conjunto é fundamental, pois praticamente todos os
conceitos desenvolvidos em computação e informática, bem como os
correspondentes resultados, são baseados em conjuntos ou construções
sobre conjuntos.
B = { x | x é brasileiro }
66
Pertinência: Se um determinado elemento a é elemento de um
conjunto A, tal fato é denotado por: a ∈ A
A⊆B
B⊇A
A⊂B
67
Ou, alternativamente, que B contém propriamente A e denota-se por:
B⊃A
A = B - igualdade
A ⊆ B ⇔ ∀x, se x ∈ A então x ∈ B.
A ⊂ B ⇔ A ⊆ B e A é diferente de B.
68
Conjuntos em Python
Um set em Python é uma coleção de itens únicos (distintos).
Python nos permite criar sets de várias formas. Uma das formas mais
frequentemente usadas é criar um set a partir de uma lista de elementos.
Números: [1, 2, 2, 3, 3, 3]
Números distintos: {1, 2, 3}
Números: [1, 2, 2, 3, 3, 3]
Números distintos: {1, 2, 3}
numeros_distintos = set()
for num in numeros:
numeros_distintos.add(num)
Criar um conjunto vazio.
69
elemento é simplesmente descartado (não é inserido, uma vez que já está
presente no conjunto).
Números: {1, 3}
{1, 3}
70
O Python nos permite remover todos os elementos de um conjunto
de uma vez. Para isso, precisamos usar a função clear.
Números: {1, 2, 3}
Números: set()
71
conjunto_diferenca = conjunto_B - conjunto_C
A={0,1,3,5,7,9} e B={0,2,4,6,8}
72
A = {0, 1, 3, 5, 7, 9}
B = {0, 2, 4, 6, 8}
C = A.union(B)
print(C)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
A = {0, 1, 3, 5, 7, 9}
B = {0, 2, 4, 6, 8}
C=A|B
print(C)
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
Operações em Python
73
Interseção
A interseção de dois conjuntos A e B é o conjunto de elementos
comuns em ambos os conjuntos.
A = {0, 1, 3, 5, 7, 9}
B = {0, 2, 4, 6, 8}
C=A&B
D = A.intersection(B)
print(C)
print(D)
{0}
print(intersecao(conjuntoA, conjuntoB))
74
Diferença
A diferença entre dois conjuntos A e B (A - B) é um conjunto de
elementos que estão apenas em A, mas não em B.
{'A', 'B'}
print(diferenca(conjuntoA, conjuntoB))
Diferença Simétrica
A diferença entre dois conjuntos A e B (A - B) é um conjunto de
elementos que estão apenas em A, mas não em B.
75
A diferença simétrica entre dois conjuntos é um conjunto contendo
os elementos de ambos os conjuntos que não são comuns um com o outro.
Ou seja, apenas os itens não-repetidos. Para tal, empregamos o operador ^
ou o método symmetric_difference().
Conjuntos Disjuntos
Dois conjuntos são disjuntos se eles não possuírem nenhum
elemento em comum.
print(planetas1.isdisjoint(planetas2))
print(planetas1.isdisjoint(planetas3))
False
True
Subconjuntos
Abaixo temos exemplos de programas em Python tratando
subconjuntos.
76
def subconjunto(conjunto_a, conjunto_b):
conjunto_resultante = conjunto_a.intersection(conjunto_b)
return conjunto_resultante
{2, 4}
if contador == len(subconjunto):
return True
else:
return False
conjunto = {1, 2, 3, 4, 5, 6}
subconjunto = {2, 4, 6}
if(subconj(conjunto, subconjunto)):
print("É um Subconjunto")
else:
print("Não é um subconjunto")
def subconjunto(lista):
resultado = []
for i in range(0, len(lista)+1):
for j in range(i + 1, len(lista)+1):
subconj = lista[i:j]
77
resultado.append(subconj)
return resultado
lista = [2, 3, 4, 5]
print(subconjunto(lista))
Resultado
[[2], [2, 3], [2, 3, 4], [2, 3, 4, 5], [3], [3, 4], [3, 4, 5], [4], [4, 5], [5]]
78
7
Capítulo 7. Dicionário e Hashes
Estrutura de Dados de Dicionários
Abaixo temos as Estruturas de Dados Elementares, algumas já
estudadas.
Listas (lists)
Arranjos (arrays)
Pilhas (stacks)
Filas (queues)
Conjuntos (sets)
Dicionários (dictionaries)
Operações Primárias
80
DicRemove(d, x) – Remove item x (ou o item para o qual x
aponta) do dicionário d.
Podem ser:
Árvores AVL.
Tabelas Hash.
Performance?
Tipos de Dicionários:
Dicionários estáticos
81
Fazer busca binária apenas se n > 1000
Dicionários semi-estáticos
Operando Dicionários
Podemos nos referir a um dicionário como um mapeamento entre
um conjunto de índices (que são chamados de chaves) e um conjunto de
valores.
Sintaxe:
Exemplo:
82
Código para criação de um dicionário:
#Criando um dicionário
dicionario = {
"nome" : "João",
"idade" : 18,
"país" : "Brasil"
}
#Imprimindo o dicionário
print(dicionario)
Acessando Elementos
# criando um dicionario
dict = {
"nome": "João",
"idade": 21,
"cidade": "São Paulo"
}
# imprimindo o dicionario
print(dict)
# acessando um item especifico
print(dict['nome'])
# adicionando um item
83
dict['ocupacao'] = 'estudante'
# imprimindo o dicionario
print(dict)
# removendo um item
del dict['idade']
# imprimindo o dicionario
print(dict)
Estendendo um Dicionário
dic1.update = (dic2)
84
pop(chave) — retorna e remove o valor associado a chave;
dicionario = {
'nome': 'Ana',
'idade': 25,
'cidade': 'São Paulo'
}
# Usando get
print(dicionario.get('nome'))
# Usando item
print(dicionario.items())
# Usando key
print(dicionario.keys())
# Usando values
print(dicionario.values())
# Usando pop
print(dicionario.pop('idade'))
Tabela Hash
Uma tabela hash é uma estrutura de dados usada para armazenar e
organizar informações de forma eficiente.
tabela_hash = {}
# adicionar elementos à tabela
tabela_hash["chave_1"] = "valor_1"
tabela_hash["chave_2"] = "valor_2"
85
# acessar elementos da tabela
valor_1 = tabela_hash["chave_1"]
print(valor_1) # imprime "valor_1"
Uma tabela hash é uma estrutura de dados que mapeia chaves para
valores e usa função hash para calcular o índice.
Operações Primárias
Colisão em Hashes
Este é um tipo de dados Python, chamado de “matriz associativa” em
outras linguagens de programação.
86
country_code = {'25': 'USA' ,'20': 'India', '10': 'Nepal'}
print country_code['10'] # Output: Nepal
print country_code['20'] # Output: India
87
O tamanho da tabela hash pode ser aumentado para melhorar a
perfeição da função hash.
hash_table = [None] * 10
print (hash_table)
# Output:
# [None, None, None, None, None, None, None, None, None, None]
def hashing_func(key):
return key % len(hash_table)
88
insert(hash_table, 25, 'USA')
print (hash_table)
# Output:
# ['Nepal', None, None, None, None, 'USA', None, None, None, None]
Como você pode ver, 'Nepal' é substituído por 'India' como o primeiro
item da tabela hash, porque o resultado de hashing_func para as chaves 10
e 20 é o mesmo (ou seja, 0)
89
tabela de hash. Esse tipo de pesquisa sequencial é chamado
de sondagem linear.
Assim, são úteis para controlar dados maiores de uma forma legível
para a programação compartilhada.
90
Se você precisa armazenar e recuperar informações usando seu
índice ou uma sequência de dados, é mais adequado usar outras estruturas
de dados, como listas ou tuplas.
91
8
Capítulo 8. Recursividade
Entendendo uma recursão
Recursividade é a capacidade de um programa (função ou
procedimento) fazer uma ou mais chamadas a si mesmo.
Caso base:
93
Comparar entrada com o caso base.
Retornar.
Retornar
SENÃO
Condição de parada:
Se a < b retorna 0
Parte recursiva:
DIV(16/5)=
Retorna 1 -> 1 + 0
94
Retorna 2 -> 1 + 1
2 + n° de vezes que b = 5 cabe dentro de a = 1 (6 - 5)
Retorna 3 -> 2 + 1
3 + n° de vezes que b = 5 cabe dentro de a = 1 (menor que 5)
Retorna 0 – condição de parada
Condição de parada – b = 0
n° de vezes = 0
Mult (10 x 5)= a = 10, b = 3
b -1 = 2
Retorna a + 0, b-1
b -1 = 1
Retorna a + a, b-1
b -1 = 0
Retorna a +a +a, b-1
b = 0, condição de parada
Para a = 10, b = 3
95
Exemplo pelo Cálculo do Fatorial
Por exemplo:
4! = 4*3*2*1 = 24
5! = 5*4*3*2*1 = 120
6! = 6*5*4*3*2*1 = 720
5! = 5 * 4!
6! = 6 * 5!
Ou seja:
n! = n * (n-1)!
n! = n x (n-1) x … x 1 = n.(n-1)!
0! = 1
96
Veja que:
3! = 3.2! = 6
4! = 4.3! = 24
5! = 5.4! = 120
6! = 6.5! = 720
...
Fatorial Iterativo
Muitas vezes, precisamos escrever funções que realizam algum
processamento um determinado número de vezes, repetidamente.
97
É denotado por n!.
Usando For
if __name__ == '__main__':
n=5
print(f'The Factorial of {n} is {factorial(n)}')
Usando While
Fatorial Recursivo
Recursão é uma instrução que tem o artifício de chamar a si mesma
até que se chegue a algum resultado.
n! = n x (n-1) x … x 1 = n.(n-1)!
98
0! = 1
def fatorial(x):
if x==1:
return 1
else:
return x * fatorial(x-1)
while True:
x = int(input("Fatorial de: "))
print("Fatorial: ",fatorial(x) )
def somatorio(x):
if x==1:
return 1
else:
return x + somatorio(x-1)
while True:
x = int(input("Somatorio de 1 até: "))
print("Soma: ",somatorio(x) )
A primeira coisa a se fazer numa função recursiva é definir onde ela
vai parar, ou seja, dizer "ei, chega, aqui você vai parar de invocar você
mesma".
somatorio(x) = x + somatorio(x-1)
99
Primeiro return: 4 + somatorio(3)
Quarto return: 4 + 3 + 2 + 1 = 10
Pilha de Chamadas
Pilhas são estruturas de dados em que só é possível inserir ou
remover um elemento no final. Dizemos que pilhas seguem um protocolo em
que o último a entrar é o primeiro a sair.
Em uma pilha, para que o último elemento a entrar (ser inserido) seja
o primeiro a sair (ser removido), precisamos ser capazes de:
100
A pilha de chamadas de recursão permite que o programa rastreie as
chamadas de função recursiva e saiba quando retornar ao chamador original.
pilha = [1, 1, 2, 3, 5]
print("Pilha: ", pilha)
pilha.append(8)
print("Inserindo um elemento: ", pilha)
pilha.append(13)
print("Inserindo outro elemento: ", pilha)
pilha.pop()
print("Removendo um elemento: ", pilha)
pilha.pop()
print("Removendo outro elemento: ", pilha)
Pilha: [1, 1, 2, 3, 5]
Inserindo um elemento: [1, 1, 2, 3, 5, 8]
Inserindo outro elemento: [1, 1, 2, 3, 5, 8, 13]
Removendo um elemento: [1, 1, 2, 3, 5, 8]
Removendo outro elemento: [1, 1, 2, 3, 5]
101
Sequência de Fibonacci
A série de Fibonacci nada mais é que uma sequência de números
inteiros formada por uma regra bem simples, algo aparentemente 'bobo',
mas com um impacto e importância brutal na natureza.
102
É uma sequência tão importante, tão misteriosa e presente que tem
gente que até estuda os números de Fibonacci para tentar ganhar na Mega
Sena.
A regra é a seguinte:
Fibonacci Iterativo
Vamos chamar de a1, a2, a3, ...., os termos da sequência de Fibonacci:
a1 = 1
a2 = 1
a3 = a2 + a1 = 1 + 1 = 2
a4 = a3 + a2 = 2 + 1 = 3
a5 = a4 + a3 = 3 + 2 = 5
a6 = a5 + a4 = 5 + 3 = 8
a7 = a6 + a5 = 8 + 5 = 13
a8 = a7 + a6 = 13 + 8 = 21
a9 = a8 + a7 = 21 + 13 = 34
103
Usando o While
Usando o For
Fibonacci Recursivo
Vamos chamar de a1, a2, a3, ...., os termos da sequência de Fibonacci:
a1 = 1
a2 = 1
a3 = a2 + a1 = 1 + 1 = 2
a4 = a3 + a2 = 2 + 1 = 3
a5 = a4 + a3 = 3 + 2 = 5
104
a6 = a5 + a4 = 5 + 3 = 8
a7 = a6 + a5 = 8 + 5 = 13
a8 = a7 + a6 = 13 + 8 = 21
a9 = a8 + a7 = 21 + 13 = 34
def fibo(n):
if n==1:
return 0
elif n==2:
return 1
else:
return fibo(n-1) + fibo(n-2)
def menu():
n = int(input('Exibir ate o termo (maior que 2): '))
for val in range(1,n+1):
print(fibo(val))
while True:
menu()
105
Toda implementação recursiva pode ser "convertida" em iterativa de
alguma forma. Em último caso, usa-se uma pilha para simular o estado de
cada chamada.
Conclusão
106
demais (caso contrário, evite recursão devido ao fato da recursão ter alto
custo de memória e processamento). Há casos em que a recursão pode ser
implementada sem precisar re-chamar a função, mas sim empilhando dados,
como é o caso da remoção de árvores.
107
9
Capítulo 9. Árvores
Estrutura de dados de Árvore
Caminho em uma árvore:
nível de C é 1
nível de F é 2
Nível de um nó:
109
Altura: o maior nível encontrado na árvore (altura de uma árvore com
n nós pode variar de lg(n) até n-1;
110
Exemplo: 50, 20, 39, 8, 79, 26, 58, 15, 88, 4, 85, 96, 71, 42, 53.
Se raiz então
se novo < raiz entao
se raiz_esquerdo = vazio entao
novo = raiz_esquerdo
senao Insere(raiz_esquerdo,novo)
se novo > raiz entao
se raiz_direito = vazio entao
novo = raiz_direito
senao Insere(raiz_direito,novo)
senao O elemento já foi inserido!
Senao novo = raiz
Exemplo: 45, 17, 24, 8, 63, 19, 51, 10, 75, 4, 66, 37.
111
Percorrendo uma Árvore
Métodos:
2. Visita a raiz;
112
3. Caminha na subárvore direita na ordem central.
Pré-ordem:
1. Visita Raiz.
Pós-ordem:
3. Visita Raiz.
Central (in-ordem):
2. Visita a Raiz.
Exemplos:
113
Árvores balanceadas
Uma árvore balanceada é uma estrutura de árvore binária onde os
nós da árvore estão equilibrados, ou seja, a altura dos dois ramos da árvore
é aproximadamente a mesma.
114
As árvores binárias de pesquisa são projetadas para um acesso
rápido à informação. Idealmente a árvore deve ser razoavelmente
equilibrada e a sua altura será dada, no caso de estar completa, por h=log2
(n+1).
115
Contudo, para garantir essa propriedade em aplicações dinâmicas, é
preciso reconstruir a árvore para seu estado ideal a cada operação sobre
seus nós (inclusão ou exclusão).
116
Contudo, a altura dessa árvore deve ser da mesma ordem de
grandeza que a altura de uma árvore completa com o mesmo número de nós.
Rotação
A AVL (Adelson-Velskii e Landis – 1962) é uma árvore altamente
balanceada, isto é, nas inserções e exclusões, procura-se executar uma
rotina de balanceamento tal que as alturas das sub-árvores esquerda e sub-
árvores direita tenham alturas bem próximas.
Rotação simples
Rotação dupla
117
Rotação Simples:
Rotação Dupla:
118
Na inserção utiliza-se um processo de balanceamento que pode ser
de 4 tipos específicos:
119
RL → caso Right-Left (rotação direita-esquerda)
Genericamente:
FB = he - hd
120
10
Capítulo 10. Grafos
Em ciência da computação, um grafo é um tipo de dados abstrato que
se destina a implementar o grafo não direcionado e os conceitos de grafos
dirigidos do campo da teoria dos grafos dentro da matemática.
122
Terminologia dos grafos
Existem diferentes termos usados para descrever grafos e suas
características:
123
natural de gráficos com objetos e situações físicas significa que você pode
usar gráficos para modelar uma ampla variedade de sistemas.
Por exemplo:
Grafos não direcionados têm arestas que não têm uma direção. As
arestas indicam uma relação bidirecional, na medida em que cada aresta
pode ser atravessada em ambas as direções. Esta figura mostra um gráfico
simples não direcionado com três nós e três arestas.
124
atravessada em uma única direção. Esta figura mostra um gráfico
direcionado simples com três nós e duas arestas.
Auto-loops e Multigraphs
125
Por exemplo, a figura a seguir mostra um multigrafo não direcionado
com loops automáticos. O nó A tem três auto-loops, enquanto o nó C tem um.
O grafos contém essas três condições, qualquer uma das quais o torna um
multigrafo.
Matriz de adjacências
A matriz de adjacências de um grafo é uma matriz booleana com
colunas e linhas indexadas pelos vértices. Se adj[][] é uma tal matriz então,
para cada vértice v e cada vértice w,
126
Assim, a linha v da matriz adj[][] representa o leque de saída do
vértice v e a coluna w da matriz representa o leque de entrada do vértice w.
Por exemplo, veja a matriz de adjacências do grafo cujos arcos são 0-10-51-
01-52-43-15-3:
struct graph {
int V;
int A;
int **adj;
};
/* Um Graph é um ponteiro para um graph, ou seja, um Graph contém o endereço
de um graph. */
typedef struct graph *Graph;
127
Seguem algumas ferramentas básicas para a construção e
manipulação de grafos:
128
void GRAPHinsertArc( Graph G, vertex v, vertex w) {
if (G->adj[v][w] == 0) {
G->adj[v][w] = 1;
G->A++;
}
}
/* REPRESENTAÇÃO POR MATRIZ DE ADJACÊNCIAS: A função
GRAPHremoveArc() remove do grafo G o arco v-w. A função supõe que v e w
são distintos, positivos e menores que G->V. Se não existe arco v-w, a função
não faz nada. */
129
O espaço ocupado por uma matriz de adjacências é proporcional a
V2, sendo V o número de vértices do grafo. No caso de grafos densos, esse
espaço é proporcional ao tamanho do grafo.
Lista de adjacências
O vetor de listas de adjacência de um grafo tem uma lista encadeada
associada com cada vértice do grafo. A lista associada com um vértice v
contém todos os vizinhos de v. Portanto, a lista do vértice v representa o
leque de saída de v. Por exemplo, eis o vetor de listas de adjacência do grafo
cujos arcos são 0-1 0-5 1-0 1-5 2-4 3-1 5-3:
struct graph {
int V;
int A;
link *adj;
};
/* Um Graph é um ponteiro para um graph. */
typedef struct graph *Graph;
130
/* A lista de adjacência de um vértice v é composta por nós do tipo node.
Cada nó da lista corresponde a um arco e contém um vizinho w de v e o
endereço do nó seguinte da lista. Um link é um ponteiro para um node. */
131
Eis algumas funções básicas de construção e manipulação de grafos
representados por listas de adjacência:
132
O espaço ocupado pelo vetor de listas de adjacência é proporcional
ao número de vértices e arcos do grafo, ou seja, proporcional ao tamanho do
grafo. Portanto, listas de adjacência são uma maneira econômica de
representação. Para grafos esparsos, listas de adjacência ocupam menos
espaço que uma matriz de adjacências.
Matriz de incidências
Uma Matriz de Incidência representa computacionalmente um grafo
através de uma Matriz Bidimensional, onde uma das dimensões são vértices
e a outra dimensão são arestas.
133
Percorrendo grafos
Um algoritmo de busca é um algoritmo que percorre um grafo
andando pelos arcos de um vértice a outro. Depois de visitar a ponta inicial
de um arco, o algoritmo percorre o arco e visita sua ponta final. Cada arco é
percorrido no máximo uma vez.
134
fila contém vértices que já foram numerados, mas têm vizinhos ainda não
numerados. O processo iterativo consiste no seguinte:
135
Este capítulo introduz o poderoso algoritmo de busca em
profundidade (= depth-first search), ou busca DFS. Trata-se de uma
generalização do algoritmo que estudamos no capítulo Acessibilidade. Lá o
objetivo era decidir se um vértice está ao alcance de outro. Aqui, é objetivo
é visitar todos os vértices e numerá-los na ordem em que são descobertos.
136
11
Capítulo 11. Algoritmos de Ordenação e Busca
Algoritmos de ordenação
Na ciência da computação, um algoritmo de ordenação é um
algoritmo que coloca os elementos de uma lista em uma ordem. As ordens
usadas com mais frequência são ordem numérica e ordem lexicográfica,
ascendente ou descendente. A classificação eficiente é importante para
otimizar a eficiência de outros algoritmos (como algoritmos de pesquisa e
mesclagem) que exigem que os dados de entrada estejam em listas
classificadas. A classificação também costuma ser útil para canonizar dados
e produzir uma saída legível por humanos.
Complexidade computacional:
138
O(log2 n), e o mau comportamento é O(n2). O comportamento
ideal para uma classificação serial é O(n), mas isso não é
possível no caso médio. A classificação paralela ideal é O(log n).
139
Online: um algoritmo como o Insertion Sort que está online pode
classificar um fluxo constante de entrada.
Insertion Sort
Insertion Sort é um algoritmo de classificação simples muito
semelhante à Selection Sort. Dada uma matriz de elementos a serem
classificados, a Insertion Sort funciona da seguinte maneira:
Fonte: https://medium.com/star-gazers/
140
elemento do subarray não classificado, que é 4, e temos que inseri-lo na
posição correta no subarray classificado. Como 4 < 8, deslocamos 8 para a
direita em uma célula e inserimos 4. Agora, 4 foi removido do subarray não
classificado e tornou-se parte do subarray classificado. Em seguida,
escolhemos 6 e, como 6 é menor que 8 e maior que 4, movemos apenas 8
para a direita em uma célula e inserimos 6. Em seguida, escolhemos 3 e 3 é
menor que 4, 6 e 8. Portanto, mudamos todos os 4, 6 e 8 à direita em uma
célula e inserimos 3. Continuamos esse processo até que o último elemento
(que é 5) seja removido do subarray não classificado.
Fonte: https://medium.com/star-gazers/
141
O número total de comparações que precisam ser realizadas é 1 + 2
+ 3 + …. + (n-1) que é igual a n(n-1)/2, portanto O(n²).
Selection Sort
Selection Sort é um dos algoritmos básicos de classificação, fácil de
entender e implementar. A ideia é simples: dado um array de elementos a
serem ordenados,
142
Figura – Selection Sort
Fonte: https://medium.com/nerd-for-tech/
Bubble Sort
Bubble Sort é um algoritmo de ordenação comumente usado em
ciência da computação. O Bubble Sort é baseado na ideia de comparar
repetidamente pares de elementos adjacentes e, em seguida, trocar suas
posições se existirem na ordem errada.
Como funciona:
143
2. Compare o segundo e o terceiro elemento para verificar qual é o
maior e classifique-os em ordem crescente.
Abaixo está uma imagem de uma matriz, que precisa ser classificada.
Usaremos o Algoritmo Bubble Sort para ordenar este array:
Fonte: https://medium.com/karuna-sehgal
Leva apenas uma iteração para detectar que uma coleção já está
classificada.
144
A melhor complexidade de tempo para Bubble Sort é O(n). A média
e pior complexidade de tempo é O(n²).
Merge Sort
Merge Sort é um algoritmo de classificação, de divisão e conquista.
Ele funciona dividindo recursivamente um problema em dois ou mais
subproblemas do mesmo tipo ou relacionados, até que estes se tornem
simples o suficiente para serem resolvidos diretamente. As soluções para os
subproblemas são então combinadas para dar uma solução para o problema
original. Portanto, o Merge Sort primeiro divide a matriz em metades e
depois as combina de maneira ordenada.
Abaixo está uma imagem de uma matriz, que precisa ser classificada.
Usaremos o Merge Sort para classificar este array:
145
Figura – Merge Sort.
Fonte: https://medium.com/karuna-sehgal
146
A complexidade de espaço da Merge Sort é O(n). Isso significa que
esse algoritmo ocupa muito espaço e pode desacelerar as
operações para os últimos conjuntos de dados.
Quick Sort
Quick Sort é um algoritmo de ordenação de divisão e conquista. Ele
cria duas matrizes vazias para conter elementos menores que o valor pivô e
elementos maiores que o valor pivô e, em seguida, classifica recursivamente
as submatrizes. Existem duas operações básicas no algoritmo, trocando
itens no lugar e particionando uma seção do array.
147
8. Se o ponteiro esquerdo e o ponteiro direito não se encontrarem, vá
para a etapa 1.
Abaixo está uma imagem de uma matriz, que precisa ser classificada.
Usaremos o Quick Sort, para ordenar este array:
Fonte: https://medium.com/karuna-sehgal
148
Características importantes do Quick Sort:
Algoritmos de busca
Segundo a Wikipédia, um algoritmo de busca é: “Qualquer algoritmo
que resolva o problema de busca, ou seja, para recuperar informações
armazenadas dentro de alguma estrutura de dados, ou calculadas no espaço
de busca de um domínio de problema, seja com valores discretos ou
contínuos.”
149
Não há um único dia em que não queiramos encontrar algo em nossa
vida diária. A mesma coisa acontece com o sistema de computador. Quando
os dados são armazenados nele e após um certo período os mesmos dados
devem ser recuperados pelo usuário, o computador usa o algoritmo de busca
para encontrar os dados. O sistema salta para sua memória, processa a busca
de dados usando a técnica de algoritmo de busca e retorna os dados que o
usuário requer. Portanto, o algoritmo de busca é o conjunto de
procedimentos usados para localizar os dados específicos da coleta de
dados. O algoritmo de busca é sempre considerado o procedimento
fundamental da computação. E, portanto, sempre se diz que a diferença
entre o aplicativo rápido e o aplicativo mais lento geralmente é decidida pelo
algoritmo de pesquisa usado pelo aplicativo.
Busca Linear
A busca linear também é conhecida como um algoritmo de pesquisa
sequencial para encontrar o elemento dentro da coleção de dados. O
algoritmo começa a partir do primeiro elemento da lista, começa a verificar
cada elemento até que o elemento esperado seja encontrado. Se o elemento
não for encontrado na lista, o algoritmo percorre toda a lista e retorna
“elemento não encontrado”. Portanto, é apenas um algoritmo de busca
simples.
150
As operações de pesquisa são inferiores a 100 itens para encontrar
o elemento desejado.
Busca binária
A busca binária é usada com um conceito semelhante, ou seja, para
encontrar o elemento na lista de elementos. Os algoritmos de busca binária
são rápidos e eficazes em comparação com os algoritmos de busca linear. A
coisa mais importante a observar sobre a busca binária é que ela funciona
apenas em listas classificadas de elementos. Se a lista não estiver
classificada, o algoritmo primeiro classificará os elementos usando o
algoritmo de classificação e, em seguida, executará a função de busca binária
para encontrar a saída desejada. Existem dois métodos pelos quais podemos
executar o algoritmo de busca binária, ou seja, método iterativo ou método
recursivo. As etapas do processo são gerais para ambos os métodos, a
diferença é encontrada apenas na chamada da função.
151
Diferença entre pesquisa linear e pesquisa binária
152
Referencias
ANANY, Levitin. Introduction to design & analysis of algorithms. 3. ed.
Pearson, 2012.
153
GOODRICH, M. T.; TAMASSIA, R. Estrutura de Dados e Algoritmos em Java.
4. ed. Bookman, 2006. Site do autor: Disponível em:
<https://docs.ufpr.br/~volmir/PO_II_10_caminho_minimo.pdf/>. Acesso em:
15 jan. 2023.
154
MEDIUM. Disponível em:
<https://medium.com/@paulomartins_10299/grafos-
representa%C3%A7%C3%A3o-e-implementa%C3%A7%C3%A3o-
f260dd98823d/>. Acesso em: 14 jan. 2023.
155
PYTHON ACADEMY. Disponível em:
<https://pythonacademy.com.br/blog/list-comprehensions-no-python/>.
Acesso em: 15 jan. 2023.
156
THOMAS, H. Cormen; LEISERSON, Charles E.; RIVEST, Ronald L.; STEIN,
Clifford. Algoritmos: Teoria e Prática. 3. ed. Elsevier, 2012.
157
WIKIPEDIA. Disponível em: <https://pt.wikipedia.org/wiki/Selection_sort/>.
Acesso em: 26 jan. 2023.
158