Você está na página 1de 158

Algoritmos e Estrutura de Dados

Ricardo Brito
2023
SUMÁRIO
Capítulo 1. Introdução a Algoritmos ....................................................................... 7

Entendendo os Algoritmos ...................................................................................................... 7

O que é um Algoritmo? .............................................................................................................. 8

Algoritmos como Tecnologia .................................................................................................. 9

Algoritmos do Google.............................................................................................................. 10

Algoritmos Clássicos ............................................................................................................... 12

Algoritmos em Computadores Quânticos ..................................................................... 14

Complexidade dos Algoritmos ............................................................................................ 14

Algoritmos Polinomiais e Exponenciais ......................................................................... 18

Notação Assintótica................................................................................................................. 19

Notação Big-O (O) ..................................................................................................................... 20

Notação Ômega (Ω) .................................................................................................................. 21

Notação Theta (Θ) ..................................................................................................................... 21

Notação - Little Ômega ....................................................................................................... 22

Notação o - Little O .................................................................................................................. 22

Capítulo 2. Listas ........................................................................................................... 25

Estruturas de Listas................................................................................................................. 25

Capítulo 3. Arrays .......................................................................................................... 32

Estruturas de Arrays ................................................................................................................ 32

Variáveis Subscritas................................................................................................................. 33

Criando e Inicializando Arrays ............................................................................................ 33

Acessando Elementos em Arrays ...................................................................................... 34

Métodos de Arrays .................................................................................................................... 35

Inserindo e Removendo Elementos ................................................................................. 36

Método Shift ................................................................................................................................ 37

2
Loop em Arrays........................................................................................................................... 38

Arrays Multidimensionais...................................................................................................... 39

Capítulo 4. Pilha ............................................................................................................. 43

Estrutura de Dados em Pilha ............................................................................................... 43

Push de Elementos em Pilha ............................................................................................... 45

Pop de Elementos em Pilha ................................................................................................. 47

Verificando Pilha Vazia ........................................................................................................... 48

Limpando Elementos da Pilha ............................................................................................ 49

Pilhas com Estruturas Encadeadas .................................................................................. 50

Capítulo 5. Filas e Deques ......................................................................................... 54

Estrutura de Dados em Fila .................................................................................................. 54

Inserção de Elementos na Fila Encadeada ................................................................... 56

Limpando Elementos da Fila Encadeada ....................................................................... 58

Fila Linear versus Fila Circular ............................................................................................ 59

Deques ........................................................................................................................................... 61

Fila de Prioridade ...................................................................................................................... 63

Capítulo 6. Conjuntos .................................................................................................. 66

Estrutura de Conjunto de Dados ....................................................................................... 66

Conjuntos em Python ............................................................................................................. 69

Remoção de Elementos em Conjuntos ........................................................................... 70

Operações Matemáticas com Sets - União ................................................................... 71

Interseção .................................................................................................................................... 74

Diferença ....................................................................................................................................... 75

Diferença Simétrica.................................................................................................................. 75

Conjuntos Disjuntos ................................................................................................................ 76

Subconjuntos .............................................................................................................................. 76

3
Capítulo 7. Dicionário e Hashes .............................................................................. 80

Estrutura de Dados de Dicionários ................................................................................... 80

Operando Dicionários.............................................................................................................. 82

Tabela Hash ................................................................................................................................. 85

Colisão em Hashes ................................................................................................................... 86

Quando não usar Dicionários .............................................................................................. 90

Capítulo 8. Recursividade .......................................................................................... 93

Entendendo uma recursão ................................................................................................... 93

Exemplo pelo Cálculo do Fatorial ...................................................................................... 96

Fatorial Iterativo ....................................................................................................................... 97

Fatorial Recursivo ..................................................................................................................... 98

Pilha de Chamadas .................................................................................................................100

Sequência de Fibonacci .......................................................................................................102

Fibonacci Iterativo .................................................................................................................103

Fibonacci Recursivo ...............................................................................................................104

Usar recursão é mais rápido............................................................................................... 105

Capítulo 9. Árvores..................................................................................................... 109

Estrutura de dados de Árvore............................................................................................ 109

Árvore de Busca Binária .......................................................................................................110

Percorrendo uma Árvore ......................................................................................................112

Árvores balanceadas..............................................................................................................114

Rotação ........................................................................................................................................117

Capítulo 10. Grafos .................................................................................................... 122

Terminologia dos grafos ......................................................................................................123

Grafos direcionados e não direcionados ......................................................................123

Matriz de adjacências............................................................................................................126

4
Lista de adjacências...............................................................................................................130

Matriz de incidências.............................................................................................................133

Percorrendo grafos.................................................................................................................134

Busca em largura - BFS ........................................................................................................134

Encontrando caminhos mais curtos com BFS ........................................................... 135

Busca em profundidade - DFSO que são Datasets .................................................135

Capítulo 11. Algoritmos de Ordenação e Busca ........................................... 138

Algoritmos de ordenação ....................................................................................................138

Insertion Sort ........................................................................................................................... 140

Selection Sort ........................................................................................................................... 142

Bubble Sort ................................................................................................................................ 143

Merge Sort ..................................................................................................................................145

Quick Sort ...................................................................................................................................147

Algoritmos de busca ..............................................................................................................149

Busca Linear .............................................................................................................................. 150

Busca binária ............................................................................................................................. 151

Referencias ...................................................................................................................... 153

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.

No livro raro, intitulado "Sketch of the Analytical Engine Invented by


Charles Babbage, Esq" (Richard & John Taylor, 1843), Lovelace traduziu um
artigo do matemático italiano Luigi Menabrea, que descreve uma máquina
de calcular automática (também conhecido como um computador) proposto
pelo engenheiro inglês Charles Babbage.

Desde a adolescência, Lovelace colaborou extensivamente com


Babbage. Seu trabalho no manuscrito de 1843 não foi apenas uma simples
tradução; suas próprias contribuições foram mais longas do que o artigo
original de Menabrea, incluindo muitas novas notas, equações e uma
fórmula que ela desenvolveu para calcular os números de Bernoulli (uma
sequência complexa de números racionais frequentemente usados em
computação e aritmética). Essa fórmula, dizem alguns estudiosos, pode ser
vista como o primeiro programa de computador já escrito.

Embora Lovelace tenha mostrado uma aptidão matemática durante


toda a sua vida, ela é mais conhecida por sua colaboração com Babbage nas
máquinas automáticas de calcular, a "Máquina de Diferença" e a nunca
construída "Máquina Analítica". A extensão das contribuições de Lovelace
para este trabalho tem sido debatida por estudiosos durante séculos, mas
as evidências de suas proezas matemáticas – incluindo correspondência
com Babbage e anotações manuscritas de algoritmos – continuam a
aumentar.

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.

Algoritmos são amplamente utilizados em todas as áreas de TI. Em


matemática e ciência da computação, um algoritmo geralmente se refere a
um pequeno procedimento que resolve um problema recorrente. Os
algoritmos também são usados como especificações para realizar o
processamento de dados e desempenham um papel importante em
sistemas automatizados.

Um algoritmo pode ser usado para classificar conjuntos de números


ou para tarefas mais complicadas, como recomendar o conteúdo do usuário
nas mídias sociais. Os algoritmos geralmente começam com uma entrada
inicial e instruções que descrevem uma computação específica. Quando a
computação é executada, o processo produz uma saída.

Algoritmos podem ser expressos como linguagens naturais,


linguagens de programação, pseudocódigo, fluxogramas e tabelas de
controle. Expressões de linguagem natural são raras, pois são mais
ambíguas. Linguagens de programação são normalmente usadas para
expressar algoritmos executados por um computador.

Algoritmos usam uma entrada inicial junto com um conjunto de


instruções. A entrada são os dados iniciais necessários para tomar decisões
e podem ser representados na forma de números ou palavras. Os dados de
entrada passam por um conjunto de instruções ou cálculos, que podem
incluir processos aritméticos e de tomada de decisão. A saída é a última
etapa de um algoritmo e normalmente é expressa como mais dados.

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.

Além disso, os algoritmos ajudam a formar o cálculo intenso que


levou a algumas das maiores descobertas na medicina, ciência, engenharia
e outras áreas.

Sem fórmulas específicas que ofereçam respostas para as equações


mais complicadas conhecidas por nossa espécie, nunca teríamos
conseguido melhorar a vida como a conhecemos.

Você dirigiu um carro hoje? Você usou um smartphone ou até


preparou uma xícara de café em uma cafeteira com timer digital? Você
verificou o status de sua conta bancária ou utilizou uma calculadora? Então
você encontrou um algoritmo pelo menos uma vez e provavelmente mais.

O acesso à internet é regido por milhões de algoritmos diferentes.


Estas instruções especiais usadas por plataformas de software informam ao
seu computador: o que mostrar na tela; as diferentes partes de um site a
serem exibidas; quais páginas classificar em primeiro lugar no Google.

Para levar essa ideia um passo adiante, você poderia tecnicamente


dizer que cada parte da codificação do site é uma forma de algoritmo, pois
fornece instruções de computador para transmitir informações às massas.

Essencialmente, um algoritmo de IA é um subconjunto estendido de


aprendizado de máquina que informa ao computador como aprender a
operar por conta própria. Por sua vez, o dispositivo continua adquirindo
conhecimento para aprimorar processos e executar tarefas com mais
eficiência.

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.

Este é o núcleo do Google, o algoritmo que faz do Google a empresa


que é hoje. A primeira versão foi baseada na popularidade de um site. Assim,
nos primórdios do Google, quanto mais links você tivesse para o seu site,
mais importante ele seria para o algoritmo de busca e melhor ele seria
classificado. Além disso, quanto mais peso os sites que apontam para um
site têm, mais importância os links têm e mais peso seu próprio site tem.
Encontramos esse critério hoje em dia com backlinks, netlinking.

No entanto, o algoritmo evoluiu devido aos primeiros desvios do


SEO. De fato, o PageRank tinha suas desvantagens. Alguns sites queriam
contornar esse sistema com trocas de links comerciais. Essas foram as
primeiras técnicas de Black Hat SEO. O grande problema era que algumas
páginas da web com conteúdo muito ruim eram mais referenciadas do que
aquelas com conteúdo rico, apenas por causa dos backlinks. As atualizações
do Google são usadas para resolver esse tipo de problema e de práticas
abusivas.

O Google PageRank é o primeiro algoritmo do Google, desenvolvido


na década de 1990. Ele classifica os sites de acordo com sua popularidade.

O Google Hummingbird, que entrou em vigor em 2013, tornou-se


um algoritmo de busca chave para o mecanismo de busca. O algoritmo
reconhece o contexto de uma consulta e não depende mais apenas da
palavra-chave. É por isso que é importante trabalhar no campo semântico de
suas consultas de destino. Esse algoritmo é a base dos assistentes de voz.

O Google Panda é outro algoritmo de busca utilizado pela gigante


americana. Desde 2011, conteúdo informativo e de qualidade é importante

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.

O Google Penguin, implantado em 2012, penaliza técnicas


"fraudulentas" (Black Hat SEO) e spam. É um pouco como a polícia do
Google. Sempre que um site comete uma infração, é penalizado no motor de
busca por este algoritmo. Entre as técnicas ditas fraudulentas, encontramos
o preenchimento de palavras-chave em um conteúdo (Keyword Stuffing), a
camuflagem (escrever em uma determinada cor sobre um fundo da mesma
cor para ocultar o texto) ou backlinks abusivos (compra, troca etc.).

Em 2015, o Google lançou a atualização do RankBrain. O algoritmo


do Google RankBrain é baseado na inteligência artificial e no aprendizado de
máquina do mecanismo de busca. O objetivo deste algoritmo de busca é
analisar o significado de palavras e formulações para associá-las a
perguntas e resultados conhecidos. Dessa forma, o Google aprende com
cada nova consulta.

Claro, existem muitas outras atualizações "menores" e "maiores"


não relatadas. Ao longo de um ano, podem ocorrer 3, 4 ou 5 grandes
atualizações que alteram as SERPs. Isso mostra que SEO é uma estratégia
de longo prazo e que é importante se manter atualizado e realizar as
melhores práticas de SEO. Para adaptar sua estratégia de SEO às
atualizações do Google e seguir as melhores práticas, você pode consultar
as diretrizes do Google.

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.

De acordo com o BFS, você deve percorrer o gráfico na direção da


largura: Para começar, mova-se horizontalmente e visite todos os nós da
camada atual e continue para a próxima camada.

Figura 2 – Busca em Largura (BFS).

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.

Quicksort é um algoritmo de classificação rápida que funciona


dividindo uma grande matriz de dados em submatrizes menores. Isso
implica que cada iteração funciona dividindo a entrada em dois
componentes, classificando-os e recombinando-os. Para grandes conjuntos
de dados, a técnica é altamente eficiente, pois sua complexidade média e de
melhor caso é O (n*logn).

Foi criado por Tony Hoare em 1961 e continua sendo um dos


algoritmos de classificação de uso geral mais eficazes disponíveis
atualmente. Ele funciona classificando recursivamente as sublistas para
cada lado de um determinado pivô e deslocando dinamicamente os
elementos dentro da lista em torno desse pivô.

Como resultado, o método de classificação rápida pode ser resumido


em três etapas:

 Pick: selecione um elemento.

 Dividir: sivida o conjunto de problemas, mova as partes menores para


a esquerda do pivô e os itens maiores para a direita.

 Repita e combine: repita as etapas e combine as matrizes que foram


classificadas anteriormente.

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.

Os computadores quânticos exploram a mecânica quântica para


realizar cálculos. Sua unidade básica de computação, o qubit, é análoga ao
bit padrão (zero ou um), mas está em uma superposição quântica entre dois
estados quânticos computacionais: pode ser zero e um ao mesmo tempo.
Essa propriedade, junto com outro recurso quântico exclusivo conhecido
como emaranhamento, pode permitir que os computadores quânticos
resolvam certas classes de problemas com mais eficiência do que qualquer
computador convencional. Os maiores computadores quânticos que os
laboratórios demonstraram até agora – os exemplos mais notáveis são da
IBM, Google, Rigetti Computing e IonQ – contêm apenas dezenas de bits
quânticos.

Complexidade dos Algoritmos


Para um estimador imparcial, a eficiência assintótica é o limite de
sua eficiência, pois o tamanho da amostra tende ao infinito. Um estimador
com eficiência assintótica 1,0 é chamado de “estimador assintoticamente
eficiente”. De forma simples, a precisão de um estimador assintoticamente
eficiente tende ao limite teórico à medida que o tamanho da amostra cresce.

A eficiência assintótica de um estimador depende da população.


Para um tipo de população (distribuições) um estimador pode ser
assintoticamente eficiente, para outros – não assintoticamente eficiente.

Entre os estimadores conhecidos, o número de estimadores


assintoticamente eficientes é muito maior do que o número de estimadores
eficientes.

14
Existem dois métodos usados, complexidade de tempo e
complexidade de espaço:

Complexidade de tempo: a complexidade de tempo de um algoritmo


quantifica a quantidade de tempo que um algoritmo leva para ser executado
em função do comprimento da entrada. Observe que o tempo de execução é
uma função do comprimento da entrada e não do tempo real de execução da
máquina na qual o algoritmo está sendo executado.

O algoritmo válido leva uma quantidade finita de tempo para


execução. O tempo requerido pelo algoritmo para resolver determinado
problema é chamado de complexidade de tempo do algoritmo. A
complexidade de tempo é uma medida muito útil na análise de algoritmos.

É o tempo necessário para a conclusão de um algoritmo. Para


estimar a complexidade de tempo, precisamos considerar o custo de cada
instrução fundamental e o número de vezes que a instrução é executada.

Para calcular a complexidade de tempo em um algoritmo, assume-se


que um tempo constante c é necessário para executar uma operação e, em
seguida, o total de operações para um comprimento de entrada em N é
calculado. Considere um exemplo para entender o processo de cálculo:
suponha que um problema seja descobrir se um par (X, Y) existe em uma
matriz, A de N elementos cuja soma é Z. A ideia mais simples é considerar
cada par e verificar se ele satisfaz a condição dada ou nã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.

Para estimar o requisito de memória, precisamos nos concentrar em


duas partes:

(1) Uma parte fixa: é independente do tamanho da entrada. Inclui


memória para instruções (código), constantes, variáveis etc.

(2) Uma parte variável: depende do tamanho da entrada. Inclui


memória para pilha de recursão, variáveis referenciadas etc.

Medição da Complexidade de um Algoritmo


Com base na complexidade de tempo, existem três casos para
analisar um algoritmo:

 Análise do pior caso (mais usada):

Na análise do pior caso, calculamos o limite superior do tempo de


execução de um algoritmo. Devemos conhecer o caso que faz com que um
número máximo de operações seja executado. Para Busca Linear, o pior caso
acontece quando o elemento a ser buscado (x) não está presente no array.
Quando x não está presente, a função search() o compara com todos os
elementos de arr[] um por um. Portanto, a complexidade de tempo de pior
caso da busca linear seria O(n).

 Melhor análise de caso (muito raramente usado):

Na análise do melhor caso, calculamos o limite inferior do tempo de


execução de um algoritmo. Devemos conhecer o caso que faz com que um
número mínimo de operações seja executado. No problema de busca linear,

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)

 Análise de caso médio (raramente usado):

Na análise de caso médio, pegamos todas as entradas possíveis e


calculamos o tempo de computação para todas as entradas. Some todos os
valores calculados e divida a soma pelo número total de entradas. Devemos
conhecer (ou prever) a distribuição dos casos. Para o problema de busca
linear, vamos assumir que todos os casos são distribuídos uniformemente
(incluindo o caso de x não estar presente na matriz). Então somamos todos
os casos e dividimos a soma por (n+1).

Abaixo está a menção classificada da notação de análise de


complexidade com base na popularidade:

 Análise do pior caso:

Na maioria das vezes, fazemos análises de pior caso para analisar


algoritmos. Na pior análise, garantimos um limite superior no tempo de
execução de um algoritmo que é uma boa informação.

 Análise de Caso Médio:

A análise de caso médio não é fácil de fazer na maioria dos casos


práticos e raramente é feita. Na análise do caso médio, devemos conhecer
(ou prever) a distribuição matemática de todas as entradas possíveis.

 Melhor Análise de Caso:

A análise do melhor caso é falsa. Garantir um limite inferior em um


algoritmo não fornece nenhuma informação, pois, no pior dos casos, um
algoritmo pode levar anos para ser executado.

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.

Tempo de execução polinomial


Diz-se que um algoritmo pode ser resolvido em tempo polinomial se
o número de passos necessários para completar o algoritmo para uma dada
entrada for O(n k ) para algum inteiro não negativo k, onde n é a
complexidade da entrada. Algoritmos de tempo polinomial são considerados
“rápidos”. A maioria das operações matemáticas familiares, como adição,
subtração, multiplicação e divisão, bem como cálculo de raízes quadradas,
potências e logaritmos, podem ser executadas em tempo polinomial. Calcula
os dígitos das constantes matemáticas mais interessantes, incluindo pi, e
pode ser feito em tempo polinomial.

Todas as operações aritméticas básicas (adição, subtração,


multiplicação e divisão), operações de comparação e operações de
ordenação, são consideradas como algoritmos de tempo polinomial.

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.

Algoritmos que têm complexidade de tempo exponencial crescem


muito mais rápido que algoritmos polinomiais. A diferença que você
provavelmente está procurando é onde a variável está na equação que
expressa o tempo de execução. Equações que apresentam complexidade de
tempo polinomial possuem variáveis nas bases de seus termos. Exemplos: n
3 + 2n 2 + 1. Observe que n está na base, NÃO no expoente. Em equações
exponenciais, a variável está no expoente. Exemplos: 2 n. Como dito antes,
o tempo exponencial cresce muito mais rápido. Se n for igual a 1000 (uma
entrada razoável para um algoritmo), observe que 1000 3 é 1 bilhão e 2 1000
é simplesmente enorme! Para referência, existem cerca de 2 80 átomos de
hidrogênio no sol, isso é muito mais do que 1 bilhão.

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.

Um algoritmo pode não ter o mesmo desempenho para diferentes


tipos de entradas. Com o aumento do tamanho da entrada, o desempenho
mudará.

O estudo da mudança no desempenho do algoritmo com a mudança


na ordem do tamanho da entrada é definido como análise assintótica.

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.

Por exemplo: no bubble sort, quando o array de entrada já está


ordenado, o tempo gasto pelo algoritmo é linear, ou seja, o melhor caso.

Mas, quando a matriz de entrada está em condição inversa, o


algoritmo leva o tempo máximo (quadrático) para ordenar os elementos, ou
seja, o pior caso.

Quando a matriz de entrada não está classificada nem na ordem


inversa, leva um tempo médio. Essas durações são indicadas usando
notações assintóticas.

Existem principalmente três notações assintóticas:

 Notação Big-O.

 Notação Ômega.

 Notação Theta.

Notação Big-O (O)


A notação Big-O representa o limite superior do tempo de execução
de um algoritmo. Assim, dá a complexidade de pior caso de um algoritmo.

O(g(n)) = { f(n): existem constantes positivas c e n 0


tais que 0 ≤ f(n) ≤ cg(n) para todo n ≥ n 0 }

A expressão acima pode ser descrita como uma função f(n)


pertencente ao conjunto O(g(n)) se existir uma constante positiva c tal que
esteja entre 0 e cg(n), para suficientemente grande n. Para qualquer valor de
n, o tempo de execução de um algoritmo não cruza o tempo fornecido por
O(g(n)).

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.

Notação Ômega (Ω)


A notação Ômega representa o limite inferior do tempo de execução
de um algoritmo. Assim, ele fornece a melhor complexidade de caso de um
algoritmo.

Ω(g(n)) = { f(n): existem constantes positivas c e n 0


tais que 0 ≤ cg(n) ≤ f(n) para todo n ≥ n 0 }

A expressão acima pode ser descrita como uma função f(n)


pertencente ao conjunto Ω(g(n)) se existir uma constante positiva c, tal que
esteja acima cg(n), para suficientemente grande n. Para qualquer valor de n,
o tempo mínimo exigido pelo algoritmo é dado por Ômega Ω(g(n)).

Notação Theta (Θ)


A notação Theta inclui a função de cima e de baixo. Uma vez que
representa o limite superior e inferior do tempo de execução de um
algoritmo, é usado para analisar a complexidade do caso médio de um
algoritmo.

Para uma função g(n), Θ(g(n)) é dada pela relação:

Θ(g(n)) = { f(n): existem constantes positivas c 1 , c 2 e n 0


tais que 0 ≤ c 1 g(n) ≤ f(n) ≤ c 2 g(n) para todos n ≥ n 0 }

A expressão acima pode ser descrita como uma função f(n)


pertencente ao conjunto Θ(g(n)) se existirem constantes positivas e tal que
possa ser colocada entre e, para n suficientemente grande.c1, c2, c1g(n),
c2g(n). Se uma função f(n) está em qualquer lugar entre e para todos, diz-se
que ela é assintoticamente limitada: c1g(n), c2g(n), n ≥ n0, f(n).

21
Figura 3 – Notação Assintótica.

Notação - Little Ômega


O algoritmo Little Ômega é uma técnica de análise de complexidade
de algoritmos. É usado para descrever o desempenho de um algoritmo em
tempo de execução em termos de sua taxa de crescimento assintótico. A
notação Little Omega é usada para descrever o comportamento de pior caso
de um algoritmo, o que significa que ele é avaliado com base na pior entrada
que ele pode receber. A notação Little Omega é escrita como Ω(n), onde n é
o tamanho de entrada de um algoritmo.

 f(n) é (g(n)) se f(n) cresce a uma taxa maior que g(n).

 Nota: f(n) é (g(n)) se e somente se g(n) é o(f(n)).

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.

f(n) é o(g(n)) se f(n) cresce a uma taxa menor que g(n).

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.

Adicionando Elementos na Lista


Adicionando Elementos: adicionar os elementos na lista pode ser
feito usando as funções append(), extend() e insert().

A função append() adiciona todos os elementos passados a ela como


um único elemento. A função extend() adiciona os elementos um por um na
lista. A função insert() adiciona o elemento passado ao valor do índice e
também aumenta o tamanho da lista.

Removendo Elementos na Lista


Excluindo elementos: para excluir elementos, use a palavra-chave
del que está embutida no Python, mas isso não retorna nada para nós.

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().

Acessando Elementos na Lista


Acessando Elementos: acessar elementos é o mesmo que acessar
Strings em Python. Você passa os valores de índice e, portanto, pode obter
os valores conforme necessário.

Operando Lista
Outras funções: você tem várias outras funções que podem ser
usadas ao trabalhar com listas.

 A função len() nos retorna o tamanho da lista.

 A função index() encontra o valor do índice do valor passado onde foi


encontrado pela primeira vez.

 A função count() encontra a contagem do valor passado para ela.

 As funções sorted() e sort() fazem a mesma coisa, ou seja, ordenar os


valores da lista. O sorted() tem um tipo de retorno enquanto o sort()
modifica a lista original.

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:

 Um simples loop for

26
 List Comprehension.

 Um loop for com range().

 Um loop for com enumerate().

 Um loop for com lambda.

 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.

Com o loop for podemos executar um conjunto de instruções, uma


vez para cada item em uma lista, tupla, conjunto etc. O loop for não requer
uma variável de indexação para definir de antemão.

Usar um loop for em Python é um dos métodos mais simples para


iterar sobre uma lista ou qualquer outra sequência (por exemplo, TUPLES,
SETS OU DICTIONARIES).

LOOPS em Python é bem simples e eficiente, por isso é importante


que os programadores entendam sua versatilidade.

Podemos usá-los para executar as instruções contidas no loop uma


vez para cada item em uma lista. Por exemplo:

fruits = ["Apple", "Mango", "Banana", "Peach"]


for fruit in fruits:
print(fruit)

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.

Devido à sua extrema simplicidade, esse método é considerado uma


das formas mais robustas de iterar em listas do Python.

Vejamos um exemplo:

fruits = ["Apple", "Mango", "Banana", "Peach"]


[print(fruit + " juice") for fruit in fruits]

Apple juice
Mango juice
Banana juice
Peach juice

Um loop for com range()


Outro método para percorrer uma lista do Python é a função range()
junto com um loop for.

range() gera uma sequência de inteiros a partir dos índices iniciais e


finais fornecidos. Um índice refere-se à posição dos elementos em uma lista.

O primeiro item tem um índice de 0, o segundo item da lista é 1 e


assim por diante. A sintaxe da função de intervalo é a seguinte:

range(start, stop, step)


fruits = ["Apple", "Mango", "Banana", "Peach"]

# Constructs range object containing elements from 0 to 3


for i in range(len(fruits)):
print("The list at index", i, "contains a", fruits[i])

The list at index 0 contains a Apple

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

Um loop for com enumerate()


Às vezes você quer saber o índice do elemento que está acessando
na lista.

A função enumerate() irá ajudá-lo. Ela adiciona um contador e o


retorna como algo chamado 'objeto enumerado’.

Este objeto contém elementos que podem ser descompactados


usando um simples loop for do Python.

Assim, um objeto enumerate reduz a sobrecarga de manter uma


contagem do número de elementos em uma iteração simples.

fruits = ["Apple", "Mango", "Banana", "Peach"]

for index, element in enumerate(fruits):


print(index, ":", element)

0 : Apple
1 : Mango
2 : Banana
3 : Peach

Um loop for com lambda


A função lambda do Python é uma função anônima na qual uma
expressão matemática é avaliada e depois retornada. Como resultado,
lambda pode ser usado como um objeto de função.

Vamos ver como usar lambda enquanto percorremos uma lista.


Faremos um loop for para iterar sobre uma lista de números, encontrar o
quadrado de cada número e salvá-lo ou anexá-lo à lista. Por fim,
imprimiremos uma lista de quadrados. Aqui está o código:

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.

Este é um dos primeiros loops que os programadores iniciantes


encontram. É também um dos mais fáceis de entender.

Um loop while é executado até que uma determinada condição seja


atendida.

No código abaixo, essa condição é o comprimento da lista; o


contador i é definido como zero e adiciona 1 toda vez que o loop imprime um
item na lista. Quando i se torna maior que o número de itens na lista, o loop
while termina.

Confira o código:

fruits = ["Apple", "Mango", "Banana", "Peach"]


i=0
while i < len(fruits):
print(fruits[i])
i=i+1

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.

Um array armazena valores que são todos do mesmo tipo,


sequencialmente.

Uma variável do tipo array armazena diversos valores que são


referenciados por um número denominado de índice.

 É uma variável do tipo composta.

 Em Python, Arrays são tratados como listas.

 São sequências ordenadas de itens.

o Por exemplo, uma sequência de n números pode ser chamada de S:

S = s0, s1, s2, s3, …, sn-1

 Valores específicos na sequência podem ser referenciados usando


subscritos.

Ao usar números como subscritos, os matemáticos podem resumir


sucintamente cálculos sobre itens em uma sequência usando variáveis
subscritas.

Variáveis simples são inadequadas quando é necessário guardar


muitos valores simultaneamente na memória.

32
Variáveis Subscritas
Arranjos, vetores e array.
Um único nome identifica uma série de posições de memória.

Cada elemento da série é referenciado individualmente por um


índice que indica sua posição relativa na série.

Suponha que a sequência seja armazenada em uma variável s.

Poderíamos escrever um loop para calcular a soma dos itens na


sequência assim:

sum = 0
for i in range(n):
sum = sum + s[i]

Quase todas as linguagens de computador têm uma estrutura de


sequência como essa, às vezes chamada de array.

Criando e Inicializando Arrays


Já adiantando, listas são um dos principais tipos de dados em
Python.

Saiba que dominar o funcionamento de listas em Python fará com


que você seja muito mais produtivo ao programar.

Em Python, listas de objetos são representadas pelo tipo list. Esse


tipo de dados é basicamente uma sequência de elementos, que podem ou
não ser do mesmo tipo.

Exemplo da temperatura média, que ilustra a inicialização de arrays


em Python:

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.

Assim como as listas, os arrays são ordenados, mutáveis, colocados


entre colchetes e capazes de armazenar itens não exclusivos.

Mas quando se trata da capacidade do array de armazenar diferentes


tipos de dados, a resposta não é tão direta. Depende do tipo de array usado.

Para usar arrays em Python, você precisa importar um módulo array


ou um pacote NumPy.

import array as arr

ou

import numpy as np

O módulo array do Python requer que todos os elementos do array


sejam do mesmo tipo. Além disso, para criar uma matriz (array), você
precisará especificar um tipo de valor.

No código abaixo, o "i" significa que todos os elementos em array_1


são inteiros:

import array as arr

array_1 = arr.array("i", [3, 6, 9, 12])


print(array_1)
print(type(array_1))

34
array('i', [3, 6, 9, 12])

<class 'array.array'>

Por outro lado, os arrays NumPy suportam diferentes tipos de dados.

Para criar uma matriz NumPy, você só precisa especificar os itens


(entre colchetes, é claro):

import numpy as np

array_2 = np.array(["numbers", 3, 6, 9, 12])


print (array_2)
print(type(array_2))

['numbers' '3' '6' '9' '12']


<class 'numpy.ndarray’>
Como você pode ver, array_2 contém um item do tipo string (ou seja,
"numbers") e quatro inteiros.

Agora que conhecemos suas definições e recursos, podemos falar


sobre as diferenças entre listas e arrays em Python:

Arrays precisam ser declarados por meio de bibliotecas. As listas não,


pois são incorporadas ao Python.

Nos exemplos, você viu que as listas são criadas simplesmente


colocando uma sequência de elementos entre colchetes.

A criação de um array, por outro lado, requer uma função específica


do módulo array (ou seja, array.array()) ou do pacote NumPy (ou seja,
numpy.array()).

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.

Veremos como usar LISTAS como ARRAYS, porém, para trabalhar


com arrays em Python, você terá que importar uma biblioteca, como por
exemplo a biblioteca NumPy.

Inserindo e Removendo Elementos


Inserindo Elementos no Array
Em Python listas são mutáveis, logo, é possível inserir e remover
elementos delas.

Lista2=[‘L', ‘U', ‘Z', ‘E', ‘S']


lista = ['S', 'C', 'E', 'N', 'D', 'E']
print("Lista original: ", lista)
lista.insert(0, “A")
lista.append(“R")
Lista.extend(lista2)
print(“Inserindo elementos: ", lista)

Lista original: ['S', 'C', 'E', 'N', 'D', 'E’]


Inserindo elementos: ['A', 'C', 'E', 'N', 'D', 'E', ‘R’, ‘L’, ‘U’, ‘Z’, ‘E’, ’S’]

36
Removendo Elementos no Array
Em Python listas são mutáveis, logo, é possível inserir e remover
elementos delas.

lista = ['A', 'S', 'C', 'E', 'N', 'D', 'E', 'R']


print("Lista original: ", lista)
lista.remove("S")
print("Removendo um elemento: ", lista)

Lista original: ['A', 'S', 'C', 'E', 'N', 'D', 'E', 'R']
Removendo um elemento: ['A', 'C', 'E', 'N', 'D', 'E', 'R']

Para remover um elemento em uma posição específica, usamos pop.


Note a diferença em relação à função remove, que recebe como parâmetro o
valor que desejamos remover.

lista = ['A', 'S', 'C', 'E', 'N', 'D', 'E', 'R']


print("Lista original: ", lista)
lista.pop(1)
print("Removendo elemento: ", lista)
Lista original: ['A', 'S', 'C', 'E', 'N', 'D', 'E', 'R']
Removendo um elemento: ['A', 'C', 'E', 'N', 'D', 'E', 'R’]
lista.clear()

Lista: ['A', 'C', 'E', 'N', 'D', 'E', ‘R’]


Limpando a Lista: []

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.

O método numpy.roll() é usado para rolar elementos do array ao


longo de um eixo especificado.

37
Leva a matriz e o número de lugares onde queremos deslocar os
elementos do array e retorna a matriz deslocada.

Se quisermos deslocar os elementos para a direita, temos que usar


um número inteiro positivo como valor de deslocamento. Se quisermos
deslocar os elementos para a esquerda, temos que especificar um valor de
deslocamento negativo.

O exemplo de código a seguir mostra como deslocar elementos de


um array com o método numpy.roll().

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.

No entanto, a eficiência é essencial quando você trabalha com


grandes quantidades de dados.

Suponha que você tenha grandes listas unidimensionais com um


único tipo de dados. Nesse caso, uma biblioteca externa como NumPy é a
melhor maneira de percorrer grandes listas.

O NumPy reduz a sobrecarga, tornando a iteração mais eficiente.


Isso é feito convertendo as listas em NumPy ARRAYS. Assim como nas
listas, o loop for também pode ser usado para iterar sobre esses arrays.

38
É importante observar que o método aqui apresentado só pode ser
usado para arrays de tipos de dados únicos.

import numpy as np

nums = np.array([1, 2, 3, 4, 5])

for num in nums:


print(num)

1
2
3
4
5

Embora tenhamos usado:

for num in nums:

por sua simplicidade neste exemplo, geralmente é melhor usar:

for num in np.nditer(nums):

quando você está trabalhando com listas grandes. A função


np.nditer retorna um iterador que pode percorrer a matriz NumPy, que é
computacionalmente mais eficiente do que usar um loop for simples.

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.

Um Array Multidimensional é uma estrutura de dados que consiste


em um conjunto de arrays aninhados.

O array mais externo pode conter um ou mais arrays internos, que


por sua vez podem conter um ou mais arrays internos ainda maiores.

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:

Biblioteca Python NUMPY:


A matriz Python NumPy é uma coleção de um tipo de dados
homogêneo. É mais semelhante à lista de Python. Você pode inserir
diferentes tipos de dados nele. Como inteiro, flutuante, lista, tupla, string
etc.

Para criar uma matriz multidimensional e executar uma operação


matemática, o Python NumPy ndarray é a melhor escolha.

O ndarray significa matrizes N-Dimensionais.

Para criar uma matriz NumPy 1D, use a função array () e forneça um
argumento de itens de uma lista para ela.

Sintaxe: array(object, dtype=None, copy=True, order='K',


subok=False, ndmin=0)

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

# Create Numpy 2D array which contain inter type valye


arr_2D = np.array([[0, 1], [1, 0]])

# print arr_1D
print(arr_2D)

[[0 1]
[1 0]]

Criando Array 3D:


import numpy as np

# create numpy 3D array which contain integer values


arr_3D = np.array([[[0, 1, 1], [1, 0, 1], [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:

Novos elementos entram no conjunto sempre no topo da Pilha.

O único elemento que pode ser retirado da Pilha em um dado


momento, é o elemento do topo.

Uma Pilha (em Inglês: Stack) é uma estrutura que obedece o


critério Last In, First Out - L.I.F.O. – o último elemento que entrou no
conjunto será o primeiro elemento a sair do conjunto.

A estrutura de dados em pilha é bastante intuitiva. A analogia é uma


pilha de pratos. Se quisermos usar uma pilha de pratos com a máxima
segurança, devemos inserir um novo prato “no topo” da pilha e retirar um
novo prato “do topo” da pilha.

Por isso dizemos que uma pilha é caracterizada pelas seguintes


operações:

O último a entrar é o primeiro a sair.

ou

O primeiro a entrar é o último a sair.

Existem vários exemplos de programas que utilizam uma pilha.

Editores de texto, planilhas, browsers etc., tem sempre um ícone que


restaura o texto antes da última modificação.

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.

Existe uma forma de usarmos listas como se fossem pilhas em


Python.

Usaremos listas em nossos exemplos, mas lembrando que pilhas são


implementadas com arranjos.

Um arranjo (em inglês array) é uma estrutura de dados que armazena


uma coleção de elementos de tal forma que cada um dos elementos possa
ser identificado por, pelo menos, um índice ou uma chave.

Podemos inclusive dizer que pilhas são arranjos em que as


operações de inserção e remoção de elementos seguem um protocolo
específico: o último elemento a ser inserido é sempre o primeiro elemento a
ser removido.

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]

O exemplo anterior ilustra o funcionamento das operações de


inserção e remoção de elementos em uma pilha.

Tanto as inserções (append) quando remoções (pop) acontecem à


direita (no final) da pilha.

Se implementadas cuidadosamente, essas operações possuem


complexidade constante, ou seja, O(1).

Push 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.

45
Os métodos append() e pop() podem ser usados para empilhar e
desempilhar elementos.

PUSH = append()

# Inicia uma pilha – vazia


def init(P):
P = []

# Empilha novo elemento


def push(P, x):
P.append(x)

# Retorna o elemento do topo da pilha mas não desempilha


def top(P):
if is_empty(P):
return None
return P[-1]

# Empilha e imprime o estado atual da pilha


def Empilha(p, z):
push(p, z)
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))

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.

Os métodos append() e pop() podem ser usados para empilhar e


desempilhar elementos.

POP = pop()

# Remove elemento do topo da pilha


def pop(P):
if is_empty(P):
return None
return P.pop()

# retorna True se a pilha está vazia


def is_empty(P):
return len(P) == 0

# 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
Desempilha(mp)
Desempilha(mp)
Desempilha(mp)

47
Desempilha(mp)
Desempilha(mp)
print(top(mp))

Verificando Pilha Vazia


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.

Os métodos append() e pop() podem ser usados para empilhar e


desempilhar elementos.

A operação len(P) é a própria função do Python.


# retorna True se a pilha está vazia
def is_empty(P):
return len(P) == 0

Código completo:
# Inicia uma pilha – vazia
def init(P):
P = []

# Empilha novo elemento


def push(P, x):
P.append(x)

# Retorna o elemento do topo da pilha mas não desempilha


def top(P):
if is_empty(P): return None
return P[-1]

# Remove elemento do topo da pilha


def pop(P):
if is_empty(P): return None
return P.pop()

# retorna True se a pilha está vazia


def is_empty(P):
return len(P) == 0
# Empilha e imprime o estado atual da pilha
def Empilha(p, z):
push(p, z)
print(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

Limpando Elementos da Pilha


Como limpar uma pilha em Python?
Pilhas são geralmente implementadas com arranjos. Em uma pilha,
para que o último elemento a entrar (ser inserido) seja o primeiro a sair (ser
removido), precisamos ser capazes de:

Inserir um novo elemento no final da pilha.

Remover um elemento do final da pilha.

49
Como limpar uma pilha em Python?

def remover_pilha(pilha):
if pilha:
pilha.pop()
else:
return None

 Portanto, a função para remover todos os elementos de uma pilha em


Python é o método pop(). Este método remove o último elemento da
pilha. Para remover todos os elementos, você deve fazer um loop e
chamar o método pop() até que a pilha esteja vazia.

Exemplo de código:

pilha = [1, 2, 3, 4, 5]
while pilha:
pilha.pop()
print(pilha) # Esta pilha está agora vazia

Pilhas com Estruturas Encadeadas


Já vimos o uso de arranjos como pilhas.

A ideia básica é que precisamos inserir e remover elementos de um


arranjo de um modo específico: elementos são sempre inseridos no final do
arranjo e elementos são sempre removidos do final do arranjo. LIFO.

Agora vamos ver como implementar uma pilha usando uma


estrutura encadeada. Sempre que inserirmos um elemento, o colocaremos
no final da pilha, ou seja, no topo da pilha. E sempre que removermos um
elemento, removeremos o elemento que está no topo da pilha.

O código a seguir implementa uma função que insere um novo


elemento em uma pilha.

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

O código a seguir implementa uma função que remove um elemento


no topo da pilha.

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

O código a seguir implementa uma função que remove um elemento


no topo da pilha.

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

É importante ressaltar que tanto inserções quanto remoções


ocorrem em somente um extremo da pilha, no topo.

# Cria uma pilha vazia.


pilha = Pilha()
print("Pilha vazia: ", pilha)
# Insere elementos na pilha.
for i in range(5):
pilha.insere(i)
print("Insere o valor {0} no topo da pilha: {1}".format(i, pilha))
# Remove elementos na pilha.
while pilha.topo != None:
pilha.remove()
print("Removendo elemento que está no topo da pilha: ", pilha)
Pilha vazia: [None]

Insere o valor 0 no topo da pilha: [0 -> None]


Insere o valor 1 no topo da pilha: [1 -> 0 -> None]

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).

Política “First-in, First-out” (FIFO)

Modelos intuitivos de Filas são itens de supermercado por conta da


validade. Organiza cargas e mercadorias que servem para gerar filas de
espera, ou melhor, para criar uma ordem de saída no estoque.

Em uma fila, para que o primeiro elemento a entrar (ser inserido) seja
o primeiro a sair (ser removido), precisamos ser capazes de:

 Inserir um novo elemento no final da fila.

 Remover um elemento do início da fila.

Exemplos de aplicações de Filas:

 Filas de espera e algoritmos de simulação.

 Controle por parte do sistema operacional a recursos


compartilhados, tais como impressoras.

 Buffers de Entrada/Saída.

 Estrutura de dados auxiliar em alguns algoritmos como a


busca em largura.

Operações Principais

 Enfileirar(F,x): insere o elemento x no final da fila F. Retorna


true se foi possível inserir, false caso contrário.

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

 frente(F): retorna o elemento no início de F, sem remover.

 tamanho(F): retorna o número de elementos em F.

 vazia(F): indica se a fila F está vazia.

 cheia(F): indica se a fila F está cheia (útil para


implementações estáticas).

Existe uma forma de usarmos listas como se fossem filas em Python,


mas isso não é muito eficiente devido à forma como Python implementa
listas. Felizmente, existe uma biblioteca que implementa as operações de
filas em Python – “collections.deque”.

from collections import deque


# Cria uma fila com três elementos.
fila = deque(["Banana", "Maçã", "Pera"])
print("Fila: ", fila)
# Adiciona um elemento ao final da fila.
fila.append("Uva")
print("Adicionando um elemento: ", fila)
# Remove o primeiro elemento adicionado à fila.
fila.popleft()
print("Removendo um elemento: ", fila)

Resultado

Fila: deque(['Banana', 'Maçã', 'Pera'])


Adicionando um elemento: deque(['Banana', 'Maçã', 'Pera', 'Uva'])
Removendo um elemento: deque(['Maçã', 'Pera', 'Uva’])

O exemplo dado ilustra o funcionamento das operações de inserção


e remoção de elementos em uma fila.

55
Inserções acontecem à direita (no final) da fila e remoções ocorrem
à esquerda (no começo) da fila.

Se implementadas cuidadosamente, essas operações possuem


complexidade constante, ou seja, O(1).

#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

Inserção de Elementos na Fila Encadeada


Na estrutura de Fila, o acesso aos elementos também segue regras.

Sua ideia fundamental é que só podemos inserir elementos no final


da fila e retirar do início da fila (FIFO – First in, first out).

Assim como a pilha, a fila pode ser implementada com vetores ou


com listas encadeadas, dependendo apenas se soubermos a quantidade
máxima de elementos que cabem nesta fila.

56
No capítulo anterior, foi mostrado como implementar uma pilha
usando uma estrutura encadeada.

Agora, implementaremos uma fila usando uma estrutura encadeada.


Em nossa implementação de pilha, tanto inserções quanto remoções
ocorriam no final da pilha.

Em filas, inserções ocorrem no final e remoções ocorrem no começo.


Para isso, usaremos dois ponteiros: um para o começo da fila e outro para o
final. Esses ponteiros nos permitirão implementar inserções e remoções
com custo constante.

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) + "]"

def insere(self, novo_dado):


"""Insere um elemento no final da fila."""
# Cria um novo nodo com o dado a ser armazenado.
novo_nodo = Nodo(novo_dado)
# Insere em uma fila vazia.
if self.primeiro == None:
self.primeiro = novo_nodo
self.ultimo = novo_nodo
else:
# Faz com que o novo nodo seja o último da fila.
self.ultimo.proximo = novo_nodo

57
# Faz com que o último da fila referencie o novo nodo.
self.ultimo = novo_nodo

Limpando Elementos da Fila Encadeada


Em filas, inserções ocorrem no final e remoções ocorrem no começo.
Para isso, usaremos dois ponteiros: um para o começo da fila e outro para o
final. Esses ponteiros nos permitirão implementar inserções e remoções
com custo constante.

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)

Fila vazia: [None]


Insere o valor 0 final da fila: [0 -> None]
Insere o valor 1 final da fila: [0 -> 1 -> None]

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]

Fila Linear versus Fila Circular


Uma fila linear é assim chamada porque se assemelha a uma linha
reta onde os elementos são posicionados um após o outro. Ela contém uma
coleção homogênea dos elementos nos quais novos elementos são
adicionados em uma extremidade e excluídos da outra extremidade.

O conceito da fila pode ser entendido pelo exemplo de uma fila do


público que está esperando do lado de fora do balcão para obter o bilhete de
teatro. Nessa fila, a pessoa se une à extremidade traseira da fila para pegar
o ticket e o ticket é emitido no front da fila.

 Existem várias operações realizadas na fila

 Em primeiro lugar, a fila é inicializada (isto é, vazia).

 Determine se a fila está vazia ou não.

 Determine se a fila está cheia ou não.

 Inserção do novo elemento pela extremidade traseira


(Enfileirar).

 Exclusão do elemento da extremidade dianteira (Dequeue).

Uma fila circular é uma variante da fila linear que efetivamente


supera a limitação da fila linear.

Na fila circular, o novo elemento é adicionado na primeira posição


da fila, se a última estiver ocupada e o espaço estiver disponível.

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.

Fila circular conecta as duas extremidades através de um ponteiro


onde o primeiro elemento vem depois do último elemento. Ele também
mantém o controle da parte frontal e traseira, implementando alguma lógica
extra para que possa rastrear os elementos a serem inseridos e excluídos.

Com isso, a fila circular não gera a condição de estouro até que a fila
esteja cheia.

Frente deve apontar para o primeiro elemento.

A fila estará vazia se Frontal = Traseira.

Quando um novo elemento é adicionado, a fila é incrementada pelo


valor um (Rear = Rear + 1).

Quando um elemento é excluído da fila, a frente é incrementada em


um (Frente = Frente + 1).

A fila linear é uma lista ordenada na qual os elementos de dados são


organizados na ordem sequencial.

Em contraste, a fila circular armazena os dados na forma circular.

Fila linear segue a ordem FIFO para executar a tarefa (o elemento


adicionado na primeira posição será deletado na primeira posição).

Por outro lado, na fila circular, a ordem das operações executadas


em um elemento pode mudar.

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.

Por outro lado, a fila circular é capaz de inserir e excluir o elemento


de qualquer ponto até que ele esteja desocupado.

A fila linear desperdiça o espaço de memória enquanto a fila circular


faz uso eficiente do espaço.

Deques
Fila de prioridades
Deques são estruturas que permitem inserir e remover de ambos os
extremos.

Uma fila duplamente terminada (DEQUE, do inglês Double Ended


Queue) é um tipo de dado abstrato que generaliza uma fila na qual os
elementos podem ser adicionados ou removidos da frente (cabeça) ou de
trás (cauda).

Também é chamada de lista encadeada cabeça-cauda, apesar de


propriamente isto se referir a uma implementação de estrutura de dados
específica.

Deques são filas duplamente ligadas, isto é, filas com algum tipo de
prioridade.

Por exemplo, sistemas distribuídos sempre necessitam que algum


tipo de processamento seja mais rápido, por ser mais prioritário naquele
momento, deixando outros tipos mais lentos ou em fila de espera, por não
requerem tanta pressa. Ele pode ser entendido como uma extensão da
estrutura de dados Fila.

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).

Da mesma forma que ocorre com a fila, o deque deve ser


implementado segundo a abordagem circular, que confere eficiência à
estrutura ao mesmo tempo em que evita o desperdício de memória.

Operações Principais

 inserir_inicio(D,x): insere o elemento x no início do deque D.


Retorna true se foi possível inserir, false caso contrário.

 inserir_fim(D,x): insere o elemento x no final do deque D.


Retorna true se foi possível inserir, false caso contrário.

 remover_inicio(D): remove o elemento no início de D e retorna


esse elemento. Retorna null se não foi possível remover.

 remover_fim(D): remove o elemento no final de D e retorna


esse elemento. Retorna null se não foi possível remover.

Operações Auxiliares

 primeiro(D): retorna o elemento no início de D. Retorna null se


o elemento não existe.

 ultimo(D): retorna o elemento no final de D. Retorna null se o


elemento não existe.

 contar(D): retorna o número de elementos em D.

 vazia(D): indica se o deque D está vazio.

 cheia(D): indica se o deque D está cheio (útil para


implementações estáticas).

62
Fila de Prioridade
A fila de prioridade é um tipo avançado da estrutura de dados da fila.

Em vez de desenfileirar o elemento mais antigo, uma fila de


prioridade classifica e desenfileira os elementos com base em suas
prioridades.

As filas de prioridade são usadas para lidar com problemas de


agendamento em que algumas tarefas são priorizadas em detrimento de
outras.

Estruturas de dados que suportam as operações:

 FPInsere(q, x) – Insere item x na fila de prioridade q.

 FPRemove(q) – Remove e retorna o maior item da fila de


prioridade q.

 Maximo(q) – Retorna o maior item da fila de prioridade q (se o


item existir).

 FPInicializa(q) – Inicializa a fila de prioridade d.

 FPCheio(q), FPVazio(q) – Testa se a fila de prioridade q está


cheia ou vazia, respectivamente.

Como implementar?

Um heap binário é uma forma eficiente.

Se houver poucas inserções, que tal um array ordenado?

Quando usar?

Utilizado para manter cronogramas e calendários, determinando


próximas tarefas.

63
Biblioteca heapq: https://docs.python.org/3/library/heapq.html

Este módulo fornece uma implementação do algoritmo de fila de


heap, também conhecido como algoritmo de fila de prioridade.

Heaps são árvores binárias para as quais cada nó pai tem um valor
menor ou igual a qualquer um de seus filhos.

Esta implementação usa arrays para os quais heap[k] <=


heap[2*k+1] e heap[k] <= heap[2*k+2] para todo k, contando elementos a
partir de zero.

Para fins de comparação, elementos inexistentes são considerados


infinitos.

A propriedade interessante de um heap é que seu menor elemento


é sempre a raiz, heap[0].

Fila de Prioridades com Heap

# Inicializando a fila de prioridades


import heapq
fila = []
# Inserindo elementos na fila de prioridades
heapq.heappush(fila, (4, 'A'))
heapq.heappush(fila, (3, 'B'))
heapq.heappush(fila, (2, 'C'))
heapq.heappush(fila, (1, 'D'))
heapq.heappush(fila, (5, 'E'))

# Removendo elementos da fila de prioridades


print(heapq.heappop(fila)) # Saída: (1, 'D')
print(heapq.heappop(fila)) # Saída: (2, 'C')
print(heapq.heappop(fila)) # Saída: (3, 'B')
print(heapq.heappop(fila)) # Saída: (4, 'A')
print(heapq.heappop(fila)) # Saída: (5, 'E')

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.

Conjunto é uma estrutura que agrupa objetos e constitui uma


base para construir estruturas mais complexas.

Assim, informalmente, um conjunto é uma coleção, sem repetições


e sem qualquer ordenação, de objetos denominados elementos.

Vogais = { a, e, i, o, u } - Neste caso, Vogais denota o conjunto { a, e, i,


o, u }.

A definição de um conjunto por propriedades é denominada


denotação por compreensão como, por exemplo:

Pares = { n | n é número par } interpretada como: o conjunto de todos


os elementos n tal que n é número par.

Assim, a forma geral de definição de um conjunto por propriedades


é como segue:

{ x | p(x) } e é tal que um determinado elemento a é elemento deste


conjunto se a propriedade p é verdadeira para a, ou seja, se p(a) é verdadeira.
Por exemplo, para o conjunto:

B = { x | x é brasileiro }

Tem-se que Pelé é elemento de B e Bill Gates não é elemento de B.

66
Pertinência: Se um determinado elemento a é elemento de um
conjunto A, tal fato é denotado por: a ∈ A

Caso contrário, afirma-se que a não pertence ao conjunto A. Tal fato


é denotado por: a ∉ A

Um conjunto especialmente importante é o conjunto vazio, ou seja,


o conjunto sem elementos { }, usualmente representado pelo seguinte
símbolo: ∅

Conjunto unitário: a é o conjunto constituído pelo jogador de


futebol Pelé.

Subconjunto e Igualdade de Conjuntos

Além da noção de pertinência já introduzida, outra noção


fundamental da teoria dos conjuntos é a de continência, que permite
introduzir os conceitos de subconjunto e de igualdade de conjuntos.

Se todos os elementos de um conjunto A também são elementos


de um conjunto B, então se afirma que A está contido em B e denota-se
por:

A⊆B

ou, alternativamente, que B contém A, e denota-se por:

B⊇A

Nesse caso (A ⊆ B ou B ⊇ A), afirma-se que A é subconjunto de B.

Adicionalmente, se A ⊆ B, mas existe b ∈ B tal que b ∉ A, se afirma


que A está contido propriamente em B ou que A é subconjunto próprio de
B, e denota-se por:

A⊂B

67
Ou, alternativamente, que B contém propriamente A e denota-se por:

B⊃A

Os conjuntos A e B são ditos conjuntos iguais, o que é denotado por:

A = B - igualdade

A ⊆ B – As frases “A está contido em B” e “B contém A” são formas


alternativas de dizer que A é um subconjunto de B.

A ⊆ B ⇔ ∀x, se x ∈ A então x ∈ B.

A ⊂ B – Se A e B são conjuntos, A é subconjunto próprio de B se cada


elemento de A está em B, mas existe pelo menos um elemento de B que não
está em A. A está contido propriamente em B.

A ⊂ B ⇔ A ⊆ B e A é diferente de B.

Exemplo com Conjunto Numérico

68
Conjuntos em Python
Um set em Python é uma coleção de itens únicos (distintos).

Python provê formas eficientes e convenientes para criação e


manipulação de sets. Nesta seção aprenderemos as principais
características e funcionalidades dessa poderosa estrutura de dados em
Python.

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.

# Exemplo de criação de sets


numeros = [1, 2, 2, 3, 3, 3]
numeros_distintos = set(numeros)
print("Números: ", numeros)
print("Números distintos: ", numeros_distintos)

Números: [1, 2, 2, 3, 3, 3]
Números distintos: {1, 2, 3}

Outra forma de criarmos sets em Python é criar um conjunto vazio e


inserir elementos nele à medida que desejarmos.

# Exemplo de criação de sets.


numeros = [1, 2, 2, 3, 3, 3]
numeros_distintos = set()
for num in numeros:
numeros_distintos.add(num)
print("Números: ", numeros)
print("Números distintos: ", numeros_distintos)

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.

Adiciona um elemento ao conjunto criado anteriormente. Se o


elemento não existir no conjunto, ele é adicionado. Caso contrário, o

69
elemento é simplesmente descartado (não é inserido, uma vez que já está
presente no conjunto).

Remoção de Elementos em Conjuntos


Para remover um elemento de um conjunto em Python, podemos
usar a função remove ou a função discard.

# O exemplo abaixo mostra o uso da função remove.


lista = [1, 2, 2, 3, 3, 3]
nums = set([1, 2, 2, 3, 3, 3])
print("Números: ", nums)
nums.remove(2)
print("Lista: ", lista)
print("Números: ", nums)

Números: {1, 3}

A função remove deve ser usada somente se tivermos certeza de que


o elemento está presente no conjunto, pois, se o elemento não estiver
presente, a função remove causa uma exceção, como mostramos abaixo.

nums = set([1, 2, 2, 3, 3, 3])


nums.remove(4)

Traceback (most recent call last):


File "<stdin>", line 1, in <module>
KeyError: 4

Uma alternativa à função remove é a função discard, que remove um


elemento do conjunto se o elemento estiver presente, mas não faz nada
caso contrário.

nums = set([1, 2, 2, 3, 3, 3])


nums.discard(4)
nums.discard(2)
print(nums)

{1, 3}

Apesar de 4 não estar presente no conjunto, nenhum erro é


retornado ao tentarmos removê-lo.

70
O Python nos permite remover todos os elementos de um conjunto
de uma vez. Para isso, precisamos usar a função clear.

nums = set([1, 2, 2, 3, 3, 3])


print("Números: ", nums)
nums.clear()
print("Números: ", nums)

Números: {1, 2, 3}
Números: set()

Esta notação é a forma de Python indicar um conjunto vazio. Talvez


você estivesse esperando ver {} impresso aqui, mas {} é a forma que Python
usa para indicar um dicionário vazio. Por isso, para evitar confusão, quando
imprimimos um conjunto vazio, Python imprime set() ao invés de {}.

Operações Matemáticas com Sets - União


Sets em Python podem parecer restritos, mas eles são estruturas de
dados incrivelmente úteis e são muito bons naquilo que se propõem a fazer:
armazenar elementos distintos.

Um set em Python é uma representação de um conjunto na


matemática. E assim como nela, em que temos união, interseção e diferença
de conjuntos (além de outras operações), em Python podemos realizar essas
mesmas operações em sets de forma muito eficiente.

# isso cria um conjunto vazio


conjunto_A = set()

# isso cria um conjunto com valores


conjunto_B = {1, 2, 3, 4, 5}

# isso adiciona um elemento ao conjunto


conjunto_A.add(10)

# isso cria um conjunto com valores


conjunto_C = {3, 4, 5, 6, 7}

# isso cria um conjunto com a união dos conjuntos


conjunto_uniao = conjunto_B | conjunto_C

# isso cria um conjunto com a diferença entre os conjuntos

71
conjunto_diferenca = conjunto_B - conjunto_C

# isso cria um conjunto com a intersecção dos conjuntos


conjunto_interseccao = conjunto_B & conjunto_C

# isso cria um conjunto com os elementos que estão em B mas não em C


conjunto_diferenca_simetrica = conjunto_B ^ conjunto_C

# imprime o conjunto de união


print(conjunto_uniao)

# imprime o conjunto de diferenca


print(conjunto_diferenca)

# imprime o conjunto de intersecção


print(conjunto_interseccao)

# imprime o conjunto de diferença simétrica


print(conjunto_diferenca_simetrica)

Como exemplo, considere a operação de união de conjuntos na


matemática. Suponha que tenhamos dois conjuntos:

A={0,1,3,5,7,9} e B={0,2,4,6,8}

Desejamos construir um conjunto C, que é a união dos conjuntos A


e B.

Matematicamente, temos que C = A∪B = {0,1,2,3,4,5,6,7,8,9}

Note que o 0 aparece em ambos os conjuntos, mas aparece uma


única vez no conjunto C.

Em Python, podemos realizar a operação União da seguinte forma:

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}

Esta é uma forma de criarmos um conjunto já contendo alguns


elementos.

Alternativamente, podemos realizar a operação acima de forma mais


concisa, fazendo como mostrado no exemplo abaixo.

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}

C = A | B - modo alternativo de escrever A.union(B).

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.

Em python, a interseção é realizada usando o operador &. Essa


mesma operação pode ser realizada usando o método intersection().

Outra operação comum em conjuntos é a interseção, que nos retorna


os elementos que aparecem 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}

C = A & B - outra forma de escrever esta linha seria: C = A.intersect(B)

Outra operação comum em conjuntos é a interseção, que nos retorna


os elementos que aparecem em ambos os conjuntos.

def intersecao(conjuntoA, conjuntoB):


return conjuntoA.intersection(conjuntoB)

conjuntoA = {0, 1, 3, 5, 7, 9, 10}


conjuntoB = {0, 2, 4, 6, 8, 10}

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.

Da mesma forma, a diferença entre os conjuntos B e A (B - A) é um


conjunto de elementos em B, mas não em A.

Em Python, a diferença é realizada usando o operador -. Essa mesma


operação pode ser realizado usando o método difference().

set1 = {'A', 'B', 'C', 'D'}


set2 = {'C', 'D', 'E', 'F'}
set3 = set1 - set2
# Podemos usar o método difference:
set4 = set1.difference(set2)
set3
set4

{'A', 'B'}

Outra operação comum em conjuntos é a diferença, que nos retorna


os elementos que aparecem em um dos conjuntos.

def diferenca(conjuntoA, conjuntoB):


return conjuntoA.difference(conjuntoB)

conjuntoA = {0, 1, 3, 5, 7, 9, 10}


conjuntoB = {0, 2, 4, 6, 8, 10}

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().

planetas1 = {"Vênus", "Mercúrio", "Terra", "Netuno", "Marte"}


planetas2 = {"Terra", "Júpiter", "Urano", "Saturno", "Marte"}
simetrica = planetas1 ^ planetas2
print(simetrica)
print(planetas1.symmetric_difference(planetas2))

{'Saturno', 'Vênus', 'Netuno', 'Mercúrio', 'Urano', 'Júpiter'}

Conjuntos Disjuntos
Dois conjuntos são disjuntos se eles não possuírem nenhum
elemento em comum.

Para verificar se dois conjuntos são disjuntos, empregamos o


método isdisjoint().

planetas1 = {'Vênus', 'Mercúrio', 'Terra', 'Netuno', 'Marte'}


planetas2 = {'Terra', 'Júpiter', 'Urano', 'Saturno', 'Marte'}
planetas3 = {'Júpiter', 'Urano', 'Saturno'}

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

conjunto_a = set([0, 1, 2, 3, 4, 5])


conjunto_b = set([2, 4, 6, 8])
print(subconjunto(conjunto_a, conjunto_b))

{2, 4}

def subconj(conjunto, subconjunto):


contador = 0
for elemento in subconjunto:
if elemento in conjunto:
contador += 1

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)

 Listas de Prioridade (priority queues)

Estruturas de Dados são formas que temos para armazenar e


organizar coleções de dados.

A estrutura de dados do tipo Dict (Dicionário) é uma sequência


mutável de objetos mapeados.

Esse mapeamento é realizado criando uma relação de chave e valor


— para a uma chave temos um valor.

Um Dicionário possui uma quantidade variável de objetos, os quais


podem ser adicionados e removidos a qualquer momento.

Operações Primárias

 DicInsere(d, x) – Insere item x no dicionário d.

80
 DicRemove(d, x) – Remove item x (ou o item para o qual x
aponta) do dicionário d.

 DicBusca(d, k) – Retorna o item com chave k do dictionário d


(se o item existir).

 DicInicializa(d) – Inicializa o dicionário d.

 DicCheio(d), DicVazio(d) – Testa se o dicionário d está cheio


ou vazio, respectivamente.

Permite retornar um item a partir de sua chave, ao invés de utilizar a


posição do elemento.

Podem ser:

 Arrays ordenados e não-ordenados.

 Listas ordenadas e não-ordenadas.

 Árvores AVL.

 Tabelas Hash.

Qual deles utilizar? O que analisar?

 Performance?

 Dados serão modificados?

Tipos de Dicionários:

Dicionários estáticos

 Dados não mudam

 Utilizar arrays (ordenados ou não-ordenados)

81
 Fazer busca binária apenas se n > 1000

Dicionários semi-estáticos

 Apenas inserções e buscas (pouquíssimas remoções)

 Tabelas Hash (com endereçamento aberto)

Dicionários totalmente dinâmicos

 Dados são modificados o tempo todo

 Tabelas Hash (atenção para a função de Hash)

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.

Cada chave mapeia um valor.

A associação de uma chave e um valor é chamada de par chave-valor.

Sintaxe:

my_dict = {'key1': 'value1','key2': 'value2','key3': 'value3'…'keyn':


'value'}

Um dicionário é uma coleção não ordenada de pares chave-valor.


Tem um comprimento, especificamente o número de pares chave-valor e
fornece pesquisa rápida por chave.

As chaves devem ser tipos de objetos imutáveis.

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)

Medindo um Dicionário: assim como nas outras estruturas de


dados, podemos obter a quantidade de pares (chave e valor) de um
Dicionário utilizando o comando len(<Dicionário>).

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

Existem diversas formas de estendermos um Dicionário, mas as que


mais nos interessam no momento são:

 Inserção de um de par (chave e valor) utilizando o operador [].

 Inserção de outro dicionário em um dicionário utilizando o


comando update(<dicionário>):

dic1.update = (dic2)

Métodos dos Dicionários

Os métodos dos Dicionários alteram o objeto — diferente do método


das Strings.

Lembre-se que Dicionários são mutáveis.

Lista de métodos do tipo Dict:

 get(chave) — retorna o valor associado a chave;

 items() — retorna uma lista de tuplas, onde cada tupla é


composta de uma chave e valor do dicionário;

 keys() — retorna uma lista com todas as chaves do dicionário;

 values() — retorna uma lista com todos os valores do


dicionário;

84
 pop(chave) — retorna e remove o valor associado a chave;

 popitem() — retorna e remove um elemento aleatório do


dicionário.

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.

Em uma tabela hash, os dados são armazenados em uma tabela com


linhas e colunas, onde cada coluna contém um campo com um valor único.

O valor único é usado como chave para a tabela e é usado para


recuperar um registro específico.

O armazenamento e a recuperação de dados são muito eficientes,


pois os dados são armazenados de forma indexada.

As tabelas hash são usadas para armazenar informações, como


nomes de usuários, números de telefone e endereços de e-mail.

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.

A partir deste índice, o valor desejado pode ser encontrado.

A função hash atribui cada chave a um repositório exclusivo.

Em alguns casos, fornece o mesmo índice para mais de uma chave.

Hashing é distribuir as entradas pela matriz. Uma chave é usada para


calcular o índice.

Hash Table usa uma matriz como meio de armazenamento e usa a


técnica de hash para gerar um índice onde um elemento deve ser inserido
ou localizado.

Operações Primárias

A seguir estão as operações primárias básicas de uma tabela de


hash:

 Search − Pesquisa um elemento em uma tabela hash.

 Insert − insere um elemento em uma tabela hash.

 Delete - Exclui um elemento de uma tabela de hash.

Colisão em Hashes
Este é um tipo de dados Python, chamado de “matriz associativa” em
outras linguagens de programação.

Os dados são armazenados em pares chave-valor.

É fácil recuperar dados do Dicionário Python.

86
country_code = {'25': 'USA' ,'20': 'India', '10': 'Nepal'}
print country_code['10'] # Output: Nepal
print country_code['20'] # Output: India

Uma lista é outro tipo de dados em Python.

Os dados podem ser armazenados como tuplas.

Não é tão fácil quanto o dicionário Python recuperar itens.

Precisamos usar um loop para procurar qualquer item na lista.

É demorado recuperar dados.

country_code = [('25', 'USA'), ('20', 'India'), ('10', 'Nepal')]

def insert(item_list, key, value):


item_list.append((key, value))

def search(item_list, key):


for item in item_list:
if item[0] == key:
return item[1]

print (search(country_code, '20’))


# Output: India
print (insert(country_code, '100’))
# Output: None, python returns 'None' by default if the searched item is not
found in the list

Aqui está uma implementação muito simples da tabela de hash e da


função de hash.

Lida com a geração de slot ou índice para qualquer valor de “chave”.

Hashing perfeito ou função hash perfeita é aquele que atribui um


slot exclusivo para cada valor de chave.

Às vezes, pode haver casos em que a função de hash gera o mesmo


índice para vários valores de chave.

87
O tamanho da tabela hash pode ser aumentado para melhorar a
perfeição da função hash.

Vamos criar uma tabela hash de tamanho 10 com dados vazios.

hash_table = [None] * 10
print (hash_table)

# Output:
# [None, None, None, None, None, None, None, None, None, None]

Abaixo está uma função simples de hash que retorna o módulo do


comprimento da tabela de hash.

No nosso caso, o comprimento da tabela hash é 10.

O operador de módulo (%) é usado na função hash.

O operador % (módulo) produz o restante da divisão do primeiro


argumento pelo segundo.

def hashing_func(key):
return key % len(hash_table)

print (hashing_func(10)) # Output: 0


print (hashing_func(20)) # Output: 0
print (hashing_func(25)) # Output: 5

Implementação simples de inserção de dados/valores na tabela


hash.

Primeiro usamos a função de hash para gerar um slot/índice e


armazenar o valor fornecido nesse slot.

def insert(hash_table, key, value):


hash_key = hashing_func(key)
hash_table[hash_key] = value

insert(hash_table, 10, 'Nepal')


print (hash_table)
# Output:
# ['Nepal', None, None, None, None, None, None, None, None, None]

88
insert(hash_table, 25, 'USA')
print (hash_table)
# Output:
# ['Nepal', None, None, None, None, 'USA', None, None, None, None]

Uma colisão ocorre quando dois itens/valores obtêm o mesmo


slot/índice, ou seja, a função hash gera o mesmo número de slot para vários
itens.

Se as etapas adequadas de resolução de colisão não forem


executadas, o item anterior no slot será substituído pelo novo item sempre
que ocorrer a colisão.

Por exemplo: inserimos os itens Nepal e USA com as chaves 10 e 25,


respectivamente. Se tentarmos inserir um novo item com a chave 20, a
colisão ocorrerá porque nossa função hash irá gerar o slot 0 para a chave 20.
Mas o slot 0 na tabela hash já foi atribuído ao item 'Nepal'.

insert(hash_table, 20, 'India')


print (hash_table)
# Output:
# ['India', 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)

Geralmente, existem duas maneiras de resolver uma colisão:

 Sondagem Linear: uma maneira de resolver a colisão é


encontrar outro slot aberto sempre que houver uma colisão e
armazenar o item nesse slot aberto. A busca pelo slot aberto
começa no slot onde ocorreu a colisão. Ele se move
sequencialmente pelos slots até encontrar um slot vazio. O
movimento é circular. Ele pode se mover para o primeiro slot
enquanto procura um slot vazio, portanto, cobrindo toda a

89
tabela de hash. Esse tipo de pesquisa sequencial é chamado
de sondagem linear.

 Encadeamento: a outra maneira de resolver a colisão é o


encadeamento. Isso permite que vários itens existam no
mesmo slot/índice. Isso pode criar uma cadeia/coleção de
itens em um único slot. Quando a colisão acontece, o item é
armazenado no mesmo slot usando o mecanismo de
encadeamento.

Ao implementar o encadeamento em Python, primeiro criamos a


tabela de hash como uma lista aninhada (listas dentro de uma lista).

Quando não usar Dicionários


Como vimos, os dicionários em Python são uma ótima ferramenta
para lidar com instâncias do mundo real e criar representações fiéis.

Se destacam de outras coleções como listas e tuplas, pois


apresentam elementos multidimensionais e permitem representar
componentes mais complexos de forma fácil de gerenciar.

Assim, são úteis para controlar dados maiores de uma forma legível
para a programação compartilhada.

O uso de dicionários Python é uma boa prática, pois facilita até


mesmo a busca por itens em coleções.

Por essa razão, é fundamental entender melhor esse tipo de


conjunto de dados e saber como gerenciá-lo para obter melhores resultados
em problemas da vida real.

Dicionários em Python não são adequados para todas as tarefas.

Eles funcionam melhor quando você precisa armazenar e recuperar


informações usando uma chave.

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.

Na execução de um programa recursivo, uma pilha é responsável


pelo armazenamento das variáveis recursivas.

Uma função recursiva tem que seguir duas regras básicas:

 Condição de parada para garantir que uma chamada recursiva


não criará um loop infinito.

 Deve-se tornar o problema mais simples possível.

A estrutura básica da recursão é:

Caso base:

É a condição de parada da recursão.

Pode haver mais de um.

Ele não abre uma nova chamada da função.

Hipótese de indução (ou caso recursivo):

É o conjunto de instruções que a função exerce que incluem uma


nova chamada da função.

Obrigatoriamente, há uma nova chamada recursiva.

 Algoritmo para recursão:

Receber um valor para a chamada.

93
Comparar entrada com o caso base.

Se for, o retorno será o valor definido para o caso.

Se não, aplicar a hipótese de indução, abrindo uma nova chamada


para a função recursiva.

Esperar o retorno da nova chamada recursiva para definir o valor de


retorno.

Retornar.

Genericamente um módulo recursivo segue o algoritmo abaixo:

SE <condição de parada satisfeita>

Retornar

SENÃO

Divida o problema num caso mais simples utilizando recursão

Apresente um esquema recursivo para dividir dois números inteiros


usando contagens sucessivas. Por exemplo: calcular A/B sendo A=16 e B=5.

Condição de parada:

Se a < b retorna 0

Parte recursiva:

Sabemos que dividir A por B é o mesmo que descobrir quantas vezes


B cabe em A. Assim, para dividir 16 por 5, podemos contar o no. de vezes que
5 cabe dentro de 16.

DIV(16/5)=

1 + n° de vezes que b = 5 cabe dentro de a = 11 (16 – 5)

Retorna 1 -> 1 + 0

1 + n° de vezes que b = 5 cabe dentro de a = 6 (11 - 5)

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

int Mult_ab(int a, int b)


{
if (b== 0) return b;
else if (b == 1) return a;
else return a + Mult(a, b - 1);
}
a = 10, b = 3

int Divide_ab(int a, int b) {


if ( a < b) return 0;
else return 1 + Divide(a-b,b);
}
Apresente um esquema recursivo para somar os elementos de um
vetor.

Por exemplo, temos um vetor de 6 posições:

seja V=[2, 10, 5, 7, 8, 11] e N=6

V=[2, 10, 5, 7, 8, 11] e N=6


int Soma_elem(int V[], int N){
if (N ==0) return V[N];
else return (V[N]+ Soma_elementos(V, N-1));
}

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

Agora note duas coisas:

5! = 5 * 4!

6! = 6 * 5!

Ou seja:

n! = n * (n-1)!

Recursão é uma instrução que tem o artifício de chamar a si mesma


até que se chegue a algum resultado.

A estrutura de recursão consiste em caso base e hipótese de


indução.

A operação fatorial é um exemplo de recursão em matemática:

n! = n x (n-1) x … x 1 = n.(n-1)!

0! = 1

A função fatorial calcula o fatorial de um dado número 'n'. Para isso,


ele multiplica n pelo fatorial de n – 1, pois a hipótese de indução é que

“n! = n.(n – 1)!”.

Se n for 0, o caso base, a função retornará 1.

96
Veja que:

0! = 1 => 1! = 1.0! = 1 => 2! = 2.1! = 2

3! = 3.2! = 6

4! = 4.3! = 24

5! = 5.4! = 120

6! = 6.5! = 720

...

E assim por diante.

numero = int(input("Fatorial de: ") )


resultado=1
count=1
while count <= numero:
resultado *= count
count += 1
print(resultado)
numero = int(input("Fatorial de: ") )
resultado=1
for n in range(1,numero+1):
resultado *= n
print(resultado)

Fatorial Iterativo
Muitas vezes, precisamos escrever funções que realizam algum
processamento um determinado número de vezes, repetidamente.

Estas funções possuem alguma estrutura de iteração, como um


while ou um for, que permite repetir determinado processamento.

Estas funções são ditas funções iterativas.

O fatorial de um inteiro não negativo n é o produto de todos os


inteiros positivos menores ou iguais a n.

97
É denotado por n!.

O fatorial é usado principalmente para calcular o número total de


maneiras pelas quais n objetos distintos podem ser organizados em uma
sequência.

Usando For

# Função interativa para encontrar fatorial de um número


def factorial(n):
fact = 1
for i in range(1, n + 1):
fact = fact * i
return fact

if __name__ == '__main__':

n=5
print(f'The Factorial of {n} is {factorial(n)}')

Usando While

# Função iterativa para encontrar fatorial de um número


def factorial(n):
fact = 1
i=1
while i < n+1:
fact = fact * i
i += 1
return fact
if __name__ == '__main__':
n=5
print(f'The Factorial of {n} is {factorial(n)}')

Fatorial Recursivo
Recursão é uma instrução que tem o artifício de chamar a si mesma
até que se chegue a algum resultado.

A estrutura de recursão consiste em caso base e hipótese de


indução.

A operação fatorial é um exemplo de recursão em matemática:

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) )

Vamos calcular a soma do somatório de 1 até um determinado


número.

Nosso código fica assim:

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".

Nesse caso, é quando o argumento for 1, aí o somatório é 1 e retorna


1.

Se não for argumento 1, cai no ELSE, então o retorno é o próprio


argumento x somado de somatorio(x-1).

Lembra da função f(x)?

somatorio(x) = x + somatorio(x-1)

No corpo do script, pedimos um número ao usuário.

Por exemplo, se x =4:

99
Primeiro return: 4 + somatorio(3)

Segundo return: 4 + 3 + somatorio(2)

Terceiro return: 4 + 3 + 2 + somatorio(1)

Quarto return: 4 + 3 + 2 + 1 = 10

# função recursiva para calcular o fatorial de um número


def fatorial(num):
if num <= 1:
return 1
else:
return num * fatorial(num - 1)
# função principal do programa
def main():
for i in range(5+1):
print("%2d! = %d" % (i, fatorial(i)))
if __name__== "__main__":
main()

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.

Pilhas são geralmente implementadas com arranjos.

Em uma pilha, para que o último elemento a entrar (ser inserido) seja
o primeiro a sair (ser removido), precisamos ser capazes de:

 Inserir um novo elemento no final da pilha.

 Remover um elemento do final da pilha.

Uma pilha de chamadas em recursão é uma estrutura de dados que


armazena todas as chamadas de funções recursivas.

À medida que uma função recursiva é chamada, ela é adicionada à


pilha. Quando a função recursiva é terminada, ela é removida da pilha.

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.

Existe uma forma de usarmos listas como se fossem pilhas em


Python.

Usaremos listas em nossos exemplos, mas lembrando que pilhas são


implementadas com arranjos. Podemos inclusive dizer que pilhas são
arranjos em que as operações de inserção e remoção de elementos seguem
um protocolo específico: o último elemento a ser inserido é sempre o
primeiro elemento a ser removido.

Exemplo simples do uso de pilhas em Python:

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]

Tanto inserções (append) quando remoções (pop) acontecem à


direita (no final) da pilha.

Se implementadas cuidadosamente, essas operações possuem


complexidade constante, ou seja, O(1)

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.

A parte mais interessante da sequência de Fibonacci é sua aplicação


e uso em nosso dia a dia, bem como sua presença na natureza.

Na Matemática, se usa para achar o MDC (máximo divisor comum) de


dois números inteiros.

Pode ser usado na conversão de milhas para Km.

Há estudos e usos de Fibonacci na música (afinar instrumentos).

O animal Nautilus possui uma espiral em sua concha, formada por


proporções de números da série de Fibonacci.

Nossos dentes, por exemplo, possuem larguras cuja


proporcionalidade entre si obedece aos números da sequência.

As folhas da Bromélia são formadas por espirais, cujos raios


pertencem a série.

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:

O primeiro número da série é 1

O segundo número da série é também 1

O próximo número da série é sempre a soma dos dois anteriores

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ...

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

n = int(input("Que termo deseja encontrar: "))


ultimo=1
penultimo=1
if (n==1) or (n==2):
print("1")
else:
count=3
while count <= n:
termo = ultimo + penultimo
penultimo = ultimo
ultimo = termo
count += 1
print(termo)

Usando o For

n = int(input("Que termo deseja encontrar: "))


ultimo=1
penultimo=1
if (n==1) or (n==2):
print("1")
else:
for count in range(2,n):
termo = ultimo + penultimo
penultimo = ultimo
ultimo = termo
count += 1
print(termo)

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

Exemplo de programa em Python para se calcular Fibonacci de forma


recursiva:

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()

Usar recursão é mais rápido


Há várias situações em que usar recursão é mais eficiente.

Quando usar Recursividade:

Uma delas é quando se sabe de antemão que não haverá muitos


níveis de chamada e a versão iterativa gastaria mais processamento e
memória com uma pilha de estados.

Outra é quando há a possibilidade de Otimização de Chamada em


Cauda (Tail Call Optimization).

Quando a versão iterativa é complicada demais.

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.

O problema é que, às vezes, a solução é tão mais complexa que na


prática a versão recursiva tem um melhor custo-benefício.

Quando técnicas naturalmente usadas em recursão puderem ser


utilizadas.

Algoritmos recursivos naturalmente podem obter vantagens de


técnicas de caching e memoization para obter resultados muito mais
rápidos.

Quando não usar Recursão:

Situações em que não devemos usar recursão.

A recursão como regra geral deve ser evitada principalmente pelos


seguintes motivos:

Comumente produz código complexo, de mais difícil manutenção e


entendimento.

Pode causar um StackOverflow, após estourar o empilhamento


máximo da linguagem ou do processador.

Conclusão

Não há uma resposta absoluta, pois há casos em que a solução


iterativa é difícil de se obter ou tem desempenho inferior à recursiva. Porém,
sempre que possível, deve-se optar por uma solução não recursiva.

Basicamente, use recursão quando o problema sendo resolvido for


recursivo (cálculos de fórmulas recursivas, mergesort, quicksort, estruturas
de árvore, etc.) e se, além disso, sua solução usando laços for complexa

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.

Quanto à eficiência, a linguagem precisa dar suporte à Otimização


de Chamada em Cauda (Tail Call Optimization) para a recursão ser viável na
maioria dos casos. A menos que para um problema em particular a solução
iterativa também empregue uma pilha, e essa pilha seja tão ou mais
ineficiente em consumo de memória que a pilha de chamada, as funções
recursivas serão, em geral, menos eficientes que as iterativas.

Em ciência da computação, uma chamada de cauda é uma chamada


de sub-rotina executada como a ação final de um procedimento. Se o alvo
de uma cauda for a mesma sub-rotina, a sub-rotina é chamada de cauda
recursiva, que é um caso especial de recursão direta.

107
9
Capítulo 9. Árvores
Estrutura de dados de Árvore
Caminho em uma árvore:

É uma lista de vértices distintos e sucessivos, conectados por arcos


(arestas) da árvore.

Nó raiz existe exatamente um caminho entre a raiz e cada um dos


nós da árvore.

Os vértices da árvore estão classificados em níveis.

 nível da raiz é zero

 nível de C é 1

 nível de F é 2

 Nível de um nó:

 nível de seu pai + 1

Altura de uma árvore: corresponde ao maior nível, maior distância


entre a raiz e qualquer 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;

Folha: o nó que não possui filho.

Floresta: um conjunto de árvores.

Árvore de Busca Binária


Árvores binárias nas quais os elementos são organizados de forma
que:

 Todos os elementos na sub-árvore esquerda de cada nó k têm


valor menor ou igual ao valor no nó k.

 Todos os elementos na sub-árvore direita de cada nó k têm


valor maior do que o valor no nó k.

Árvores binárias nas quais os elementos são organizados de forma


que:

110
Exemplo: 50, 20, 39, 8, 79, 26, 58, 15, 88, 4, 85, 96, 71, 42, 53.

Ocorre sempre em uma folha.

Procedimento Insere (raiz, novo):

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:

 Pré-ordem: visite a raiz, então visite a subárvore da esquerda,


depois a subárvore da direita.

 Em-ordem ou ordem simétrica: visite a subárvore da esquerda,


então visite a raiz, depois a subárvore da direita.

 Pós-ordem: visite a subárvore da esquerda, então visite a


subárvore da direita, depois a raiz.

Pré-ordem: lista o nó raiz, seguido de suas subárvores (da esquerda


para a direita), cada uma em pré-ordem.

In-ordem: em árvores de pesquisa, a ordem de caminhamento mais


útil é a chamada ordem de caminhamento central, que é também chamado
de em-ordem ou ordem simétrica.

O caminhamento central é mais bem expresso em termos recursivos:

1. Caminha na subárvore esquerda na ordem central;

2. Visita a raiz;

112
3. Caminha na subárvore direita na ordem central.

Pós-ordem: lista os nós das subárvores (da esquerda para a direita)


cada uma em pós-ordem, lista o nó raiz.

Portanto, temos o seguinte resumo:

Pré-ordem:

1. Visita Raiz.

2. Percorre subárvore esquerda em pré-ordem.

3. Percorre subárvore direita em pré-ordem.

Pós-ordem:

1. Percorre subárvore esquerda em pós-ordem.

2. Percorre subárvore direita em pós-ordem.

3. Visita Raiz.

Central (in-ordem):

1. Percorre a subárvore esquerda em in-ordem.

2. Visita a Raiz.

3. Percorre a subárvore direita em in-ordem.

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.

Esta propriedade ajuda a garantir que as operações de busca,


inserção e remoção de dados sejam mais eficientes.

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.

Definição: uma árvore AVL é uma árvore na qual as alturas das


subárvores esquerda e direita de cada nó diferem no máximo por uma
unidade.

Fator de balanceamento: Altura da subárvore direita - altura da


subárvore esquerda

Portanto, uma árvore binária balanceada (AVL) é uma árvore binária


na qual as alturas das duas subárvores de todo nó nunca difere em mais de
1. O balanceamento de um NÓ é definido como a altura de sua subárvore
esquerda menos a altura de sua subárvore direita.

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).

O tempo de pesquisa tende a O(log2N).

Porém, com sucessivas inserções de dados principalmente


ordenados, ela pode se degenerar para O(n).

Árvores completas são aquelas que minimizam o número de


comparações efetuadas no pior caso para uma busca com chaves de
probabilidades de ocorrências idênticas.

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).

Balanceamento: suponha a inclusão da chave 0 (zero).

Para reorganizar a árvore anterior, foi utilizada uma abordagem O(n),


no pior caso.

Naturalmente, essa é uma péssima solução, uma vez que operações


como inserção e remoção geralmente são efetuados em O(logn) passos.

Por esse motivo, árvores completas não são recomendadas para


aplicações que requeiram estruturas dinâmicas.

Alternativa: utilizar um determinado tipo de árvore binária cujo pior


caso para a busca não seja necessariamente tão pequeno quanto o mínimo
1 + lower_bound(logn) passos pela árvore completa.

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.

Ou seja, deve possuir altura O(logn) para todas as suas subárvores.

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.

Definição: uma árvore AVL é uma árvore na qual as alturas das


subárvores esquerda e direita de cada nó diferem no máximo por uma
unidade.

Fator de balanceamento: Altura da subárvore direita - altura da


subárvore esquerda.

Na inserção, utiliza-se um processo de balanceamento que pode ser


de 2 tipos gerais:

 Rotação simples

 Rotação dupla

117
Rotação Simples:

k2 é nó mais profundo onde falha o equilíbrio sub-árvore esquerda


está 2 níveis abaixo da direita.

Rotação Dupla:

Rotação simples não resolve o desequilíbrio.

 sub-árvore Q está a 2 níveis de diferença de R.

 sub-árvore Q passa a estar a 2 níveis de diferença de P.

Uma das subárvores B ou C está 2 níveis abaixo de D k2, e a chave


intermédia fica na raiz.

Posições de k1, k3 e subárvores completamente determinadas pela


ordenação.

118
Na inserção utiliza-se um processo de balanceamento que pode ser
de 4 tipos específicos:

RR → caso Right-Right (rotação a direita)

LL → caso Left-Left (rotação a esquerda)

LR → caso Left-Right (rotação esquerda-direita)

119
RL → caso Right-Left (rotação direita-esquerda)

Coeficiente que serve como referência para verificar se uma árvore


AVL está ou não balanceada.

O fator é calculado nó a nó e leva em consideração a diferença das


alturas das sub-árvores da direita e da 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.

Uma estrutura de dados de grafo consiste em um conjunto finito (e


possivelmente mutável) de vértices (também chamados de nós ou pontos),
juntamente com um conjunto de pares não ordenados desses vértices para
um grafo não direcionado ou um conjunto de pares ordenados para um grafo
direcionado. Esses pares são conhecidos como arestas (também chamadas
de ligações ou linhas), e para um grafo direcionado também são conhecidos
como arestas, mas também às vezes setas ou arcos. Os vértices podem fazer
parte da estrutura do grafo, ou podem ser entidades externas representadas
por índices inteiros ou referências.

Uma estrutura de dados de gráfico também pode associar a cada


borda algum valor de borda, como um rótulo simbólico ou um atributo
numérico (custo, capacidade, comprimento, etc.).

Figura com um grafo direcionado com três vértices (círculos azuis) e


três arestas (setas pretas).

122
Terminologia dos grafos
Existem diferentes termos usados para descrever grafos e suas
características:

1. Vértice: Um vértice é um nó ou um ponto de conexão em um


gráfico.

2. Aresta: Uma aresta é uma linha que liga dois vértices.

3. Grau: O grau de um vértice é o número de arestas conectadas a ele.

4. Grafo direcionado: Um grafo direcionado é um grafo em que as


arestas têm direções associadas a elas.

5. Grafo não direcionado: Um grafo não direcionado é um grafo em


que as arestas não têm direções associadas a elas.

6. Ciclo: Um ciclo é um caminho fechado em um gráfico, no qual um


vértice é conectado a outro vértice

Grafos direcionados e não direcionados


Um grafo é uma coleção de nós e arestas que representa relações:

 Nós são vértices que correspondem a objetos.

 Bordas são as conexões entre objetos.

As arestas do gráfico às vezes têm Pesos, que indicam a força (ou


algum outro atributo) de cada conexão entre os nós.

Essas definições são gerais, pois o significado exato dos nós e


arestas em um gráfico depende do aplicativo específico. Por exemplo, você
pode modelar as amizades em uma rede social usando um gráfico. Os nós do
grafo são pessoas e as arestas representam amizades. A correspondência

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:

 Vinculação de página da Web — Os nós do gráfico são páginas


da Web e as bordas representam hiperlinks entre páginas.

 Aeroportos — Os nós do gráfico são aeroportos e as arestas


representam voos entre aeroportos.

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.

Grafos direcionados têm arestas com direção. As arestas indicam


uma relação unidirecional, na medida em que cada aresta só pode ser

124
atravessada em uma única direção. Esta figura mostra um gráfico
direcionado simples com três nós e duas arestas.

A posição, o comprimento ou a orientação exata das bordas em uma


ilustração gráfica normalmente não têm significado. Em outras palavras, o
mesmo gráfico pode ser visualizado de várias maneiras diferentes,
reorganizando os nós e / ou distorcendo as arestas, desde que a estrutura
subjacente não mude.

Auto-loops e Multigraphs

Grafos criados usando grafo e dígrafo podem ter um ou mais auto-


loops, que são arestas que conectam um nó a si mesmo. Além disso, os grafos
podem ter várias arestas com os mesmos nós de origem e destino, e o grafos
é então conhecido como um multigrafo. Um multigrafo pode ou não conter
autoloops.

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.

 O nó A tem três auto-loops.

 Os nós A e B têm cinco arestas entre eles.

 Os nós A e C têm duas arestas entre eles.

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,

adj[v][w] = 1 se v-w é um arco e

adj[v][w] = 0 em caso contrário.

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:

Como nossos grafos não têm laços, os elementos da diagonal da


matriz de adjacências são iguais a 0. Se o grafo for não-dirigido, a matriz é
simétrica: adj[v][w] ≡ adj[w][v].

Um grafo é representado por uma struct graph que contém a matriz


de adjacências, o número de vértices, e o número de arcos do grafo:

/* REPRESENTAÇÃO POR MATRIZ DE ADJACÊNCIAS: A estrutura graph


representa um grafo. O campo adj é um ponteiro para a matriz de adjacências
do grafo. O campo V contém o número de vértices e o campo A contém o
número de arcos do grafo.*/

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:

/* REPRESENTAÇÃO POR MATRIZ DE ADJACÊNCIAS: A função GRAPHinit()


constrói um grafo com vértices 0 1 .. V-1 e nenhum arco. */

Graph GRAPHinit( int V) {


Graph G = malloc( sizeof *G);
G->V = V;
G->A = 0;
G->adj = MATRIXint( V, V, 0);
return G;
}
/* REPRESENTAÇÃO POR MATRIZ DE ADJACÊNCIAS: A função MATRIXint()
aloca uma matriz com linhas 0..r-1 e colunas 0..c-1. Cada elemento da matriz
recebe valor val.*/

static int **MATRIXint( int r, int c, int val) {


int **m = malloc( r * sizeof (int *));
for (vertex i = 0; i < r; ++i)
m[i] = malloc( c * sizeof (int));
for (vertex i = 0; i < r; ++i)
for (vertex j = 0; j < c; ++j)
m[i][j] = val;
return m;
}
/* REPRESENTAÇÃO POR MATRIZ DE ADJACÊNCIAS: A função
GRAPHinsertArc() insere um arco v-w no grafo G. A função supõe que v e w
são distintos, positivos e menores que G->V. Se o grafo já tem um arco v-w,
a função não faz nada.*/

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. */

void GRAPHremoveArc( Graph G, vertex v, vertex w) {


if (G->adj[v][w] == 1) {
G->adj[v][w] = 0;
G->A--;
}
}
/* REPRESENTAÇÃO POR MATRIZ DE ADJACÊNCIAS: A função
GRAPHshow() imprime, para cada vértice v do grafo G, em uma linha, todos
os vértices adjacentes a v. */

void GRAPHshow( Graph G) {


for (vertex v = 0; v < G->V; ++v) {
printf( "%2d:", v);
for (vertex w = 0; w < G->V; ++w)
if (G->adj[v][w] == 1)
printf( " %2d", w);
printf( "\n");
}
}

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:

Na representação por listas de adjacência, um grafo é representado


por uma struct graph que contém o vetor de listas de adjacência, o número
de vértices, e o número de arcos do grafo:

/* REPRESENTAÇÃO POR LISTAS DE ADJACÊNCIA: A estrutura graph


representa um grafo. O campo adj é um ponteiro para o vetor de listas de
adjacência, o campo V contém o número de vértices e o campo A contém o
número de arcos do grafo. */

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. */

typedef struct node *link;


struct node {
vertex w;
link next;
};
/* A função NEWnode() recebe um vértice w e o endereço next de um nó e
devolve o endereço a de um novo nó tal que a->w == w e a->next == next. */

static link NEWnode( vertex w, link next) {

link a = malloc( sizeof (struct node));


a->w = w;
a->next = next;
return a;
}

131
Eis algumas funções básicas de construção e manipulação de grafos
representados por listas de adjacência:

/* REPRESENTAÇÃO POR LISTAS DE ADJACÊNCIA: A função GRAPHinit()


constrói um grafo com vértices 0 1 .. V-1 e nenhum arco. */

Graph GRAPHinit( int V) {


Graph G = malloc( sizeof *G);
G->V = V;
G->A = 0;
G->adj = malloc( V * sizeof (link));
for (vertex v = 0; v < V; ++v)
G->adj[v] = NULL;
return G;
}
/* REPRESENTAÇÃO POR LISTAS DE ADJACÊNCIA: A função
GRAPHinsertArc() insere um arco v-w no grafo G. A função supõe que v e w
são distintos, positivos e menores que G->V. Se o grafo já tem um arco v-w,
a função não faz nada. */

void GRAPHinsertArc( Graph G, vertex v, vertex w) {


for (link a = G->adj[v]; a != NULL; a = a->next)
if (a->w == w) return;
G->adj[v] = NEWnode( w, G->adj[v]);
G->A++;
}
A função GRAPHinsertArc() consome muito tempo (no pior caso,
tempo proporcional ao número de arcos), pois verifica se o arco a inserir já
existe no grafo.

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.

Dado um grafo G com n vértices e m arestas, podemos representá-lo


em uma Matriz M dada por n x m A definição precisa das ENTRADAS da
matriz varia de acordo com as propriedades do grafo que se deseja
representar, porém de forma geral guarda informações sobre como os
vértices se relacionam com cada aresta (isto é, informações sobre a
incidência de um vértice em uma aresta).

Para representar um Grafo Sem Pesos nas Arestas e Não-


Direcionado, basta que as entradas da matriz M contenham 1 se o vértice
incide na aresta, 2 caso seja um Laço (incide duas vezes) e 0 caso o vértice
não incida na aresta.

Por exemplo, a matriz de incidência do grafo ao lado é:

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.

Há duas estratégias básicas para pesquisar/percorrer/"caminhar"


sobre um grafo: a busca em largura (BreadthFirst Search, BFS) e a busca em
profundidade (Depth-First Search, DFS). Vértices ainda não descobertos
estão marcados como "White", vértices já descobertos, mas ainda
aguardando para serem visitados ou em processo de visitação estão
marcados como "Gray", e vértices já visitados são marcados como "Black".
Este é um recurso, em princípio didático, mas esta ideia também às vezes
pode ser usada em certos algoritmos.

Busca em largura - BFS


A busca em largura começa por um vértice, digamo s, especificado
pelo usuário. O algoritmo visita s, depois visita todos os vizinhos de s, depois
todos os vizinhos dos vizinhos, e assim por diante.

O algoritmo numera os vértices, em sequência, na ordem em que eles


são descobertos (ou seja, visitados pela primeira vez). Para fazer isso, o
algoritmo usa uma fila (= queue) de vértices. No começo de cada iteração, a

134
fila contém vértices que já foram numerados, mas têm vizinhos ainda não
numerados. O processo iterativo consiste no seguinte:

 enquanto a fila não estiver vazia


o retire um vértice v da fila
o para cada vizinho w de v
 se w não está numerado
 então numere w
 ponha w na fila

No começo da primeira iteração, a fila contém o vértice s, com


número 0, e nada mais.

Encontrando caminhos mais curtos com BFS


Problema do Caminho Mínimo ou Shortest Path Problem

O problema do caminho mínimo, ou caminho mais curto, consiste em


encontrar o melhor caminho entre dois nós. Assim, resolver este problema
pode significar determinar o caminho entre dois nós com o custo mínimo, ou
com o menor tempo de viagem.

Numa rede qualquer, dependendo das suas características, pode


existir vários caminhos entre um par de nós, definidos como origem e
destino. Entre os vários caminhos aquele que possui o menor “peso” é
chamado de caminho mínimo. Este peso representa a soma total dos valores
dos arcos que compõem o caminho e estes valores podem ser: o tempo de
viagem, a distância percorrida ou um custo qualquer do arco.

Busca em profundidade - DFSO que são Datasets


Um algoritmo de busca (ou de varredura) é qualquer algoritmo que
visita todos os vértices de um grafo andando pelos arcos de um vértice a
outro. Há muitas maneiras de fazer uma tal busca. Cada algoritmo de busca
é caracterizado pela ordem em que visita os vértices.

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.

A busca em profundidade não resolve um problema específico. Ela é


apenas um arcabouço, ou pré-processamento, para a resolução eficiente de
vários problemas concretos. A busca DFS nos ajuda a compreender o grafo
com que estamos lidando, revelando sua forma e reunindo informações
(representadas pela numeração dos vértices) que ajudam a responder
perguntas sobre o grafo.

A busca em profundidade está relacionada com expressões como


pré-ordem, exploração de labirintos, exploração Trémaux, fio de Ariadne (no
mito de Teseu e o Minotauro), etc.

O algoritmo de busca DFS visita todos os vértices e todos os arcos do


grafo numa determinada ordem e atribui um número a cada vértice: o k-
ésimo vértice descoberto recebe o número k.

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.

Formalmente, a saída de qualquer algoritmo de ordenação deve


satisfazer duas condições:

1. A saída está em ordem monotônica (cada elemento não é


menor/maior que o elemento anterior, de acordo com a ordem
necessária).

2. A saída é uma permutação (uma reordenação, mas mantendo todos


os elementos originais) da entrada.

Para maior eficiência, os dados de entrada devem ser armazenados


em uma estrutura de dados que permita acesso aleatório, em vez de uma
que permita apenas acesso sequencial.

Os algoritmos de ordenação podem ser classificados por:

 Complexidade computacional:

1. Comportamento do caso melhor, pior e médio em termos de


tamanho da lista. Para algoritmos típicos de ordenação serial, o
bom comportamento é O (n log n), com ordenação paralela em

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).

2. Troca por algoritmos "no local".

 Uso de memória (e uso de outros recursos do computador). Em


particular, alguns algoritmos de classificação são " in-place ".
Estritamente, uma classificação no local precisa apenas de
memória O(1) além dos itens que estão sendo classificados; às
vezes, O(log n ) memória adicional é considerada "no local".

 Recursão: Alguns algoritmos são recursivos ou não recursivos,


enquanto outros podem ser ambos (por exemplo, classificação por
mesclagem).

 Estabilidade: algoritmos de ordenação estáveis mantêm a ordem


relativa dos registros com chaves iguais (isto é, valores).

 Se eles são ou não um tipo de comparação. Uma classificação por


comparação examina os dados apenas comparando dois
elementos com um operador de comparação.

 Método geral: inserção, troca, seleção, fusão etc. As classificações


de troca incluem Bubble Sort e Quick Sort. Selection Sort incluem
classificação de ciclo e heapsort.

 Se o algoritmo é serial ou paralelo. O restante desta discussão


concentra-se quase exclusivamente em algoritmos seriais e
assume a operação serial.

 Adaptabilidade: Se a pré-classificação da entrada afeta ou não o


tempo de execução. Algoritmos que levam isso em consideração
são conhecidos por serem adaptativos.

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:

1. Divida o array em dois subarrays: subarray classificado e não


classificado (semelhante ao do selection sort)

2. Em cada iteração, remova o primeiro elemento do subarray não


classificado e insira-o na posição adequada (dependendo se deve
ser classificado em ordem crescente ou decrescente) no subarray
classificado.

3. Iterar até que o último elemento da matriz não classificada seja


removido.

Figura – Exemplo de Insertion Sort

Fonte: https://medium.com/star-gazers/

Inicialmente, o subarray classificado consiste apenas no primeiro


elemento e o subarray não classificado consiste no restante dos elementos
do array, conforme mostrado na figura acima. Escolhemos o primeiro

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.

Insertion Sort é um algoritmo no local, o que significa que não


requer espaço de memória adicional para executar a classificação.

A complexidade de tempo de melhor caso da ordenação por inserção


é O(n). Quando o array já está classificado (que é o melhor caso), a ordenação
por inserção deve realizar apenas uma comparação em cada iteraçãoe,
portanto, O(n).

A complexidade de pior caso é O(n²). Quando o array é ordenado na


ordem inversa (que é o pior caso), temos que realizar i número de
comparações na iteração iᵗʰ

Figura – Número de Comparações de Insertion Sort.

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²).

Devido à complexidade de tempo quadrática do pior caso, o Insertion


Sort seria ineficiente para classificar um grande número de elementos. No
entanto, semelhante à Selection Sort, a inserção seria útil quando o espaço
de memória livre disponível for limitado devido à sua capacidade de
classificação no local.

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,

1. Divida o array em dois subarrays: subarrays classificados e não


classificados

2. Em cada iteração, encontre o elemento máximo/mínimo no subarray


não classificado e troque-o pelo primeiro elemento do subarray não
classificado. Após a troca, o primeiro elemento do subarray não
classificado será anexado ao subarray classificado. A troca do valor
mínimo classificará os elementos em ordem crescente, enquanto a
troca do valor máximo classificará os elementos em ordem
decrescente.

3. Continue a iteração até o último elemento.

Suponha que a matriz de números a ser classificada em ordem


crescente seja (8,4,6,3,1,9,5)

142
Figura – Selection Sort

Fonte: https://medium.com/nerd-for-tech/

Inicialmente, todo o array será o subarray não classificado conforme


mostrado na figura acima. Encontramos o valor mínimo 1 e o trocamos por
8, que é o primeiro elemento do subarray não classificado. O subarray
classificado é colorido em amarelo. O próximo valor mínimo no subarray não
classificado é 3. Nós o trocamos por 4, que é o primeiro elemento do subarray
não classificado. Assim, na iteração iᵗʰ, trocamos o valor mínimo pelo
elemento i ᵗʰ do array. Após m número de iterações, os primeiros m
elementos do array estarão em ordem de classificação.

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:

1. Em uma matriz não classificada de 5 elementos, comece com os


dois primeiros elementos e classifique-os em ordem crescente.
(Compare o elemento para verificar qual é o maior).

143
2. Compare o segundo e o terceiro elemento para verificar qual é o
maior e classifique-os em ordem crescente.

3. Compare o terceiro e o quarto elemento para verificar qual é o maior


e classifique-os em ordem crescente.

4. Compare o quarto e o quinto elemento para verificar qual é o maior


e classifique-os em ordem crescente.

5. Repita as etapas 1 a 5 até que não sejam necessárias mais trocas.

Abaixo está uma imagem de uma matriz, que precisa ser classificada.
Usaremos o Algoritmo Bubble Sort para ordenar este array:

Figura – Bubble Sort

Fonte: https://medium.com/karuna-sehgal

Características do Bubble Sort:

 Valores grandes são sempre classificados primeiro.

 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²).

 A complexidade de espaço para Bubble Sort é O(1), porque apenas


um único espaço de memória adicional é necessário.

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.

Etapas de como funciona:

1. Se for apenas um elemento da lista que já está classificado, retorne.

2. Divida a lista recursivamente em duas metades até que não possa


mais ser dividida.

3. Mescle as listas menores em uma nova lista em ordem de


classificação.

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

Características importantes do Merge Sort:

 O Merge Sort é útil para classificar listas vinculadas.

 Merge Sort é uma classificação estável, o que significa que o


mesmo elemento em uma matriz mantém suas posições originais
em relação um ao outro.

 A complexidade de tempo geral do Merge sort é O(nLogn). É mais


eficiente, pois no pior caso também o tempo de execução é
O(nlogn)

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.

Etapas de como funciona:

1. Encontre um item “pivot” na matriz. Este item é a base de comparação


para uma única rodada.

2. Inicie um ponteiro (o ponteiro esquerdo) no primeiro item da matriz.

3. Inicie um ponteiro (o ponteiro direito) no último item da matriz.

4. Enquanto o valor no ponteiro esquerdo da matriz for menor que o valor


pivô, mova o ponteiro esquerdo para a direita (adicione 1). Continue
até que o valor no ponteiro esquerdo seja maior ou igual ao valor do
pivô.

5. Enquanto o valor no ponteiro direito na matriz for maior que o valor


pivô, mova o ponteiro direito para a esquerda (subtraia 1). Continue
até que o valor no ponteiro direito seja menor ou igual ao valor pivô.

6. Se o ponteiro esquerdo for menor ou igual ao ponteiro direito, troque


os valores nesses locais na matriz.

7. Mova o ponteiro esquerdo para a direita em um e o ponteiro direito


para a esquerda em um.

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:

Figura – Merge Sort

Fonte: https://medium.com/karuna-sehgal

148
Características importantes do Quick Sort:

 Quick Sort é útil para classificar matrizes.

 Em implementações eficientes, o Quick Sort não é uma


classificação estável, o que significa que a ordem relativa de itens
de classificação iguais não é preservada.

 A complexidade de tempo geral do Quick Sort é O(nLogn). No pior


caso, faz O(n2) comparações, embora esse comportamento seja
raro.

 A complexidade de espaço do Quick Sort é O(nLogn). É uma


classificação no local (ou seja, não requer nenhum
armazenamento extra).

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.”

Os algoritmos de pesquisa são projetados para verificar ou recuperar


um elemento de qualquer estrutura de dados onde esse elemento está sendo
armazenado. Eles procuram um alvo (chave) no espaço de busca. Um
algoritmo de busca é uma fórmula exclusiva que um mecanismo de pesquisa
usa para recuperar informações específicas armazenadas em uma estrutura
de dados e determinar o significado de uma página da Web e seu conteúdo.
Os algoritmos de busca são exclusivos de seu mecanismo de pesquisa e
determinam as classificações dos resultados do mecanismo de pesquisa das
páginas da web.

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.

Existem muitos tipos de algoritmos de busca possíveis, como busca


linear, busca binária, Jump Search, busca exponencial, busca Fibonacci, etc.

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.

A complexidade do tempo de execução do algoritmo de busca linear


é O(n) para o número N de elementos na lista, pois o algoritmo precisa passar
por todos os elementos

Aplicações para busca linear:

 Usado para encontrar o elemento desejado da coleção de dados


quando o conjunto de dados é pequeno

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.

A complexidade do tempo de execução para busca binária é diferente


para cada cenário. A complexidade de tempo do melhor caso é O(1), o que
significa que o elemento está localizado no meio do ponteiro. A
complexidade de tempo do caso médio e do pior caso é O(log n), o que
significa que o elemento a ser encontrado está localizado no lado esquerdo
ou no lado direito do ponteiro central. Aqui, n indica o número de elementos
na lista. A complexidade espacial do algoritmo de busca binária é O(1).

Aplicações da Busca Binária:

 O algoritmo de busca binária é usado nas bibliotecas de Java, C++,


etc.

 É usado em outro programa adicional como encontrar o menor


elemento ou o maior elemento na matriz.

 É usado para implementar um dicionário.

151
Diferença entre pesquisa linear e pesquisa binária

Busca Linear Busca Binária


Inicia a pesquisa a partir do primeiro Pesquise a posição do elemento
elemento e compara cada elemento pesquisado encontrando o
com um elemento pesquisado elemento do meio da matriz
Não precisa da lista classificada de Precisa da lista ordenada de
elemento elementos
Pode ser implementado em array e Não pode ser implementado
lista encadeada diretamente na lista encadeada
Natureza iterativa Divide e conquiste
Fácil de usar Difícil de implementar em
comparação com a busca linear
Menos linhas de código Mais linhas de código
Preferido para um conjunto de dados Preferido para um conjunto de
de tamanho pequeno dados de tamanho grande

152
Referencias
ANANY, Levitin. Introduction to design & analysis of algorithms. 3. ed.
Pearson, 2012.

ASCENCIO, A. F. G. Estrutura de dados: algoritmos, análise da complexidade


e implementação em Java e C/C++. São Paulo: Pearson, 2010. p. 432.

BJARNE, Stroustrup. The C++ Programming Language. 4. ed. Addison


Wesley, 2013.

DIGIDOD. Disponível em: <https://www.digidop.fr/en/blog/algorithms-


search-engine-google/>. Acesso em: 16 jan. 2023.

EDELWEISS N.; GALANTE, R. Estruturas de dados. Porto Alegre: Bookman,


2009. p. 261.

EDUREKA. Disponível em: <https://www.edureka.co/blog/data-structures-in-


python//>. Acesso em: 15 jan. 2023.

GEEKS FOR GEEKS. Disponível em:


<https://www.geeksforgeeks.org/introduction-to-arrays-data-structure-
and-algorithm-tutorials//>. Acesso em: 15 jan. 2023.

GEEKS FOR GEEKS. Disponível em:


<https://www.geeksforgeeks.org/insertion-sort//>. Acesso em: 26 jan. 2023.

GEEKS FOR GEEKS. Disponível em: <https://www.geeksforgeeks.org/merge-


sort//>. Acesso em: 26 jan. 2023.

GEEKS FOR GEEKS. Disponível em: <https://www.geeksforgeeks.org/time-


complexity-and-space-complexity//>. Acesso em: 15 jan. 2023.

GEEKS FOR GEEKS. Disponível em: <https://www.geeksforgeeks.org/worst-


average-and-best-case-analysis-of-algorithms//>. Acesso em: 15 jan. 2023.

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.

GOODRICH, M. T.; TAMASSIA, R. Estruturas de dados e algoritmos em Java.


5. ed. Porto Alegre: Bookman, 2013. p. 712.

IME. Disponível em:


<https://www.ime.usp.br/~pf/algoritmos_para_grafos/aulas/graphdatastruct
s.html>. Acesso em: 10 jan. 2023

IME. Disponível em:


<https://www.ime.usp.br/~pf/algoritmos_para_grafos/aulas/dfs.html>.
Acesso em: 10 jan. 2023

JON, Kleinberg; TARDOS, Eva Tardos. Algorithm Design. Addison-Wesley


Longman Publishing Co., Inc. 2005, Boston, MA, USA.

LAFORE, R. Estruturas de dados e algoritmos em Java. Rio de Janeiro:


Ciência Moderna, 2004. p. 681.

LIVE SCIENCE. Disponível em: <https://www.livescience.com/63154-ada-


lovelace-first-algorithm-auction.html/>. Acesso em: 15 jan. 2023.

MATHWORKS. Disponível em:


<https://www.mathworks.com/help/matlab/math/directed-and-undirected-
graphs.html>. Acesso em: 10 jan. 2023.

MEDIUM. Disponível em: <https://favtutor.com/blogs/searching-


algorithms/>. Acesso em: 26 jan. 2023.

MEDIUM. Disponível em: <https://medium.com/@mk.ranjan/understand-


sorting-in-real-quick-6b850a985630/>. Acesso em: 26 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.

MEDIUM. Disponível em: <https://medium.com/karuna-sehgal/an-


introduction-to-bubble-sort-d85273acfcd8/>. Acesso em: 26 jan. 2023.

MEDIUM. Disponível em: <https://medium.com/karuna-sehgal/a-quick-


explanation-of-quick-sort-7d8e2563629b/>. Acesso em: 26 jan. 2023.

MEDIUM. Disponível em: <https://medium.com/karuna-sehgal/a-simplified-


explanation-of-merge-sort-77089fe03bb2/>. Acesso em: 26 jan. 2023.

MEDIUM. Disponível em: <https://medium.com/nerd-for-tech/introduction-


to-selection-sort-19de8e72c89f/>. Acesso em: 26 jan. 2023.

MEDIUM. Disponível em: <https://medium.com/star-gazers/introduction-to-


insertion-sort-756821945e3e/>. Acesso em: 26 jan. 2023.

MEDIUM. Disponível em: <https://www.freecodecamp.org/news/search-


algorithms-linear-and-binary-search-explained//>. Acesso em: 26 jan. 2023.

MEDIUM. Disponível em: <https://www.volusion.com/blog/search-


algorithms//>. Acesso em: 26 jan. 2023.

MICHAEL T. Goodrich; TAMASSIA, Roberto. Algorithm Design: Foundations,


Analysis and Internet Examples. 2. ed. John Wiley & Sons, Inc., New York, NY,
USA. 2009.

NIVIO, Ziviani. Projeto de Algoritmos com implementações em Java e C++.


Thomson Learning, 2007.

PREISS, B. R. Estruturas de dados e algoritmos: padrões de projetos


orientados a objetos com Java. Rio de Janeiro: Elsevier, 2001. p. 566.

155
PYTHON ACADEMY. Disponível em:
<https://pythonacademy.com.br/blog/list-comprehensions-no-python/>.
Acesso em: 15 jan. 2023.

ROBERT Sedgewick; WAYNE, Kevin. Algorithms. 4. ed. Addison Wesley, 2011.

ROCK CONTENT. Disponível em: <https://rockcontent.com/blog/artificial-


intelligence-algorithm//>. Acesso em: 14 jan. 2023.

SCIENTIFICAMERICAN. Disponível em:


<https://www.scientificamerican.com/article/algorithms-for-quantum-
computers//>. Acesso em: 14 jan. 2023.

SILVA, O. Q. Estrutura de dados e algoritmos usando C: fundamentos e


aplicações. Rio de Janeiro: Ciência Moderna, 2007. p. 460.

SIMPLILEARN. Disponível em: <https://www.simplilearn.com/tutorials/data-


structure-tutorial/bfs-algorithm/>. Acesso em: 15 jan. 2023.

STATISTICS. Disponível em:


<https://www.statistics.com/glossary/asymptotic-
efficiency/#:~:text=For%20an%20unbiased%20estimator%2C%20asympt
otic,an%20%22asymptotically%20efficient%20estimator%22./>. Acesso
em: 14 jan. 2023.

SUPERWITS. Disponível em: <https://www.superwits.com/library/design-


analysis-of-algorithm/course-content-
daa/polynomialvsexponentialrunningtime/>. Acesso em: 14 jan. 2023.

SZWARCFITER, J. L.; MARKENZON, L. Estruturas de dados e seus


algoritmos. 3. ed. Rio de Janeiro: LTC, 2010. p. 302.

TECH TARGET. Disponível em:


<https://www.techtarget.com/whatis/definition/algorithm/>. 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.

TOWARD DATA SCIENCE. Disponível em:


<https://towardsdatascience.com/an-overview-of-quicksort-algorithm-
b9144e314a72#/>. Acesso em: 15 jan. 2023.

TOWARDS DATA SCIENCE. Disponível em:


<https://towardsdatascience.com/an-overview-of-quicksort-algorithm-
b9144e314a72/>. Acesso em: 26 jan. 2023.

W3 SCHOOLS. Disponível em: <https://www.w3schools.com/python/>.


Acesso em: 14 jan. 2023.

WEISS, M. A. Data Structures and Algorithm Analysis in Java. 2. ed.


Addison-Wesley. 2007. Site do autor:
http://users.cis.fiu.edu/~weiss/#dsaajava2.

WIKIPEDIA. Disponível em: <https://en.wikipedia.org/wiki/>. Acesso em: 26


jan. 2023.

WIKIPEDIA. Disponível em:


<https://en.wikipedia.org/wiki/Sorting_algorithm/>. Acesso em: 26 jan. 2023.

WIKIPEDIA. Disponível em: <https://pt.wikipedia.org/wiki/Bubble_sort/>.


Acesso em: 26 jan. 2023.

WIKIPEDIA. Disponível em: <https://pt.wikipedia.org/wiki/Insertion_sort/>.


Acesso em: 26 jan. 2023.

WIKIPEDIA. Disponível em: <https://pt.wikipedia.org/wiki/Merge_sort/>.


Acesso em: 26 jan. 2023.

WIKIPEDIA. Disponível em: <https://pt.wikipedia.org/wiki/Quicksort/>.


Acesso em: 26 jan. 2023.

157
WIKIPEDIA. Disponível em: <https://pt.wikipedia.org/wiki/Selection_sort/>.
Acesso em: 26 jan. 2023.

ZIVIANI, N. Projeto de algoritmos com implementações em Java e C ++.


São Paulo: Thomson Learning, 2007. p. 621.

ZIVIANI, N. Projeto de Algoritmos, com implementações em Java e C++.


Editora Thompson, 1. ed. 2006. Site do autor:
http://www.dcc.ufmg.br/algoritmos-java/.

158

Você também pode gostar