














Estude fácil! Tem muito documento disponível na Docsity
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Prepare-se para as provas
Estude fácil! Tem muito documento disponível na Docsity
Prepare-se para as provas com trabalhos de outros alunos como você, aqui na Docsity
Encontra documentos específicos para os exames da tua universidade
Prepare-se com as videoaulas e exercícios resolvidos criados a partir da grade da sua Universidade
Responda perguntas de provas passadas e avalie sua preparação.
Ganhe pontos para baixar
Ganhe pontos ajudando outros esrudantes ou compre um plano Premium
Apostila de um professor da UFLA que contém vários tópicos sobre algoritimos.
Tipologia: Notas de estudo
1 / 22
Esta página não é visível na pré-visualização
Não perca as partes importantes!















Abril de 2007 Lavras-MG
Existem várias maneiras de se medir a complexidade de um algoritmo, duas medidas básicas utilizadas são: tempo e espaço. A complexidade de espaço é medida através da quantidade de memória necessária para a execução do programa, por exemplo se a estrutura critica for um vetor, um algoritmo que utilize dois vetores terá complexidade de espaço 2n. Geralmente utiliza-se a complexidade de tempo para comparar algoritmos já que hoje em dia o espaço não é tão problemático quanto o tempo de execução. A complexidade de tempo de um algoritmo é medida pelo número de vezes que ele executa uma operação critica definida pelo usuário. Apesar de existirem várias notações de complexidade definiremos apenas O,Ω,Θ pois estas são suficientes para o que precisamos. Antes de começarmos as definições de notação, precisamos definir dominação assintótica. A definição de dominação assintótica é a seguinte: uma função g(n) domina assintoticamente outra função f(n) se existem duas constantes C e M tais que , para todo n >= M, temos |g(n)| <= C x |f(n)|, ou seja, uma função domina assintoticamente outra se após um determinado ponto m, a função multiplicada por uma constante é sempre maior que a outra. A figura abaixo exemplifica a situação:
1.3.1 Notação O(f(n)) – Big-O Esta notação é a mais comum quando se estuda complexidade. A definição utiliza a definição de dominação assintótica citada acima. A definição da notação é a seguinte: Uma função g(n) é O(f(n)) se existem constantes positivas c e m tais que g(n) <= c x f(n) para todo n >= m. Isso quer dizer que quando uma função complexidade de um algoritmo é O(f(n)), f(n) domina assintoticamente a função do algoritmo. Existem algumas classes de problemas que são as seguintes:
A complexidade de um algoritmo pode ser avaliada ainda de três formas diferentes com relação à entrada dos dados. Existem três casos diferentes de entrada que são os seguintes:
1.3.2 Notação Ω(f(n)) A notação Ω é a notação inversa da notação O, pois delimita uma cota assintótica inferior. Podemos então definir Ω como: uma função g(n) é Ω (f(n)) se existem constantes positivas c e m tais que g(n) >= c x f(n) para todo n >= m. Existe uma pequena diferença entre as duas notações que é a seguinte, se dizemos que um algoritmo é Ω (f(n)) simplesmente ele segue a definição acima, porém quando dizemos que um algoritmo é Ω (f(n)) associado a um problema, por exemplo no caso de ordenação por comparação, definimos o limite inferior matematicamente provado do problema associado a afirmação.
1.3.4 Notação Θ(f(n)) A notação Θ define o limite assintótico exato, ou seja, define uma função f(n) que quando multiplicada por duas constantes c1 e c2 dominará assintoticamente a função em questão superiormente e inferiormente. A definição é a seguinte : : uma função g(n) é Ω (f(n)) se existem constantes positivas c1,c2 e m tais que c1 x f(n) <= g(n) <= c2 x f(n) para todo n >= m.
1.4 Conclusão Após serem apresentados os conceitos básicos de complexidade de algoritmos, algumas notações com seus significados, características e exemplos, podemos concluir que a complexidade é um dado muito importante dos algoritmos e portanto será inserido ao apresentarmos cada algoritmo no material. A notação escolhida para isto é a big-oh devido a sua facilidade de entendimento e simplicidade de cálculo.
Capítulo 2 -
O algoritmo acima não é o mais otimizado, porém nos exercícios serão cobrados variações deste algoritmo, conduzindo o aluno a produzir algoritmos melhores dependendo da necessidade.
Capítulo 1 -.3.. Complexidade do algoritmo
Figura – Desempenho da Busca Seqüencial em Função do Tamanho da Estrutura
2.. Busca Binária
A busca em uma estrutura de dados será muito mais eficiente se os dados estiverem ordenados. No mundo real pode-se associar a busca binária a busca realizada por uma dada página em um livro. Por exemplo, seja um livro de 500 páginas, devidamente ordenadas, porém com muitas páginas faltando. Suponha-se que deseja-se encontrar a página 124. O melhor procedimento para encontrar essa página será abrir o livro ao meio. Se a página procurada, número 124, for maior do que a página aberta, então deve-se ignorar todas as páginas que estão do lado esquerdo e concentrar nas páginas que estão do lado direito. Caso contrário, se a página procurada, número 124, for menor do que a página aberta, então deve-se ignorar todas as páginas que estão do lado direito e concentrar nas páginas que estão do lado esquerdo. De maneira repetitiva pode-se repetir o passo mencionado. Considerando as páginas que se deve concentrar, por exemplo as páginas da esquerda, então deve-se abrir esse conjunto de páginas ao meio e repetir o processo até encontrar a página desejada ou não houver mais páginas a se considerar. Na computação, esse algoritmo consiste em comparar a chave dada pelo usuário com o valor que está na posição do meio de um vetor. Se o valor for igual, então encontrou-se o valor desejado. Caso contrário, existem dois casos. No primeiro caso, se a chave for menor do que o valor que está na posição do meio do vetor, então descarta-se toda a metade até o fim do vetor e concentra-se a busca na primeira metade do vetor. No segundo caso, se a chave for maior do que o valor que está na posição do meio do vetor, então descarta-se as posições do início até a metade do vetor e concentra-se a busca na segunda metade do vetor. Esse processo deve ser repetido até se encontrar o valor desejado ou não houver mais valores no vetor a procurar. Se o valor for encontrado então retorna-se a posição do vetor em que a chave foi achada. Caso contrário, retorna-se o valor -1 para sinalizar que o valor procurado não está presente na estrutura. O algoritmo da busca seqüência pode ser descrito, em forma de função, da seguinte forma:
A busca binária pode ser transformada em busca ternária, quaternária ou outra N-ária qualquer. Quanto maior a quantidade de dados, mais conveniente será utilizar um N maior. Porém, deve-se tomar o cuidado de se calcular adequadamente o N a ser utilizado. Caso contrário, se houver um vetor de um milhão de posições e aplicar-se um algoritmo de busca em vetor N-ária com N igual a um milhão, então, na prática, transformou-se o algoritmo de busca em vetor N-ária em um algoritmo seqüencial. Neste caso, o número de comparações será no mínimo de uma comparação e no máximo de um milhão de comparações.
Capítulo 1 -.4.. Complexidade do algoritmo
Figura – Desempenho da Busca Binária em Função do Tamanho da Estrutura
3.. Critérios para Escolha do Algoritmo
Supondo que você está envolvido no desenvolvimento de uma solução e precisa definir qual dos algoritmos de busca em vetor utilizar, o principal critério a ser considerados para se escolher o algoritmo de maneira técnica e consciente, diz respeito à Ordenação da estrutura. Quando a estrutura estiver ordenada, o algoritmo de busca binária deve ser utilizado, caso contrário, utiliza-se o seqüencial. Analisando os gráficos de complexidade dos algoritmos, podemos entender melhor essa escolha:
Figura – Comparação de Desempenho entre os Algoritmos de Busca Seqüencial e Binária
Capítulo 3 -
Algoritmos de Ordenação
Capítulo 1 -.5.. Introdução
Um algoritmo de ordenação tem por objetivo colocar uma coleção de dados em uma ordem pré-estabelecida. A ordenação é importante para facilitar a localização de informações como, por exemplo, o telefone de uma pessoa numa lista telefônica. Essa atividade seria impraticável se os nomes dos assinantes não estivessem ordenados alfabeticamente. Os principais algoritmos de busca existentes podem ser agrupados como:
A ordenação pode ser vista como uma atividade que, a partir de uma seqüência de N dados fornecidos como entrada, fornece uma saída com a mesma estrutura de dados, porém em alguma ordem pré-definida pelo programador. Essa ordem pode ser, por exemplo, crescente ou decrescente, utilizando algum campo do registro da estrutura de dados de entrada. A questão da ordenação é fundamental na computação. Ao longo da história, vários pesquisadores já propuseram diferentes algoritmos de ordenação, buscando reduzir o tempo necessário para realizá-la. Neste curso serão abordados os seguintes algoritmos de ordenação:
6.. Ordenação por Seleção (Select Sort)
Representa um dos métodos mais simples de compreender e executar. Suponha uma arrumadeira de hotel com várias chaves nas mãos, sendo que cada chave possui uma plaqueta com o número de um apartamento. Para facilitar seu trabalho do dia a dia, a arrumadeira mantém essas chaves organizadas em ordem crescente, a fim de evitar procurar uma por uma a cada novo apartamento a ser arrumado. Imagine que, sem querer, essas chaves caem no chão. Para continuar seu trabalho, a arrumadeira precisa catá-las do chão de modo que elas estejam devidamente ordenadas em sua mão após pegar todas as chaves. Um bom método consiste em procurar a chave de menor número, dentre as chaves que estão no chão, e, então, colocá-la na mão. Depois disto, repete-se o processo, procurando-se a chave de menor número, dentre as que estão no chão, e colocando-a na mão imediatamente atrás da primeira chave catada. E assim sucessivamente, repete-se o processo, procurando-se a chave de menor número, dentre as que estão no chão, e colocando-a na mão imediatamente atrás da última chave catada. Na computação, pode-se notar que seu comportamento, de modo genérico, consiste em achar o menor número e colocá-lo na primeira posição. Em seguida, acha-se o segundo menor número e coloca-o na segunda posição. E assim sucessivamente até não mais haver valores a serem ordenados. Dessa forma, no final de sua execução a estrutura de dados conterá um conjunto de dados devidamente ordenado, na ordem crescente ou decrescente, de acordo com o tipo de ordenação desejada.
primeira carta. Caso seja verdade, então a primeira carta é empurrada para a direita de forma a abrir espaço para que a segunda carta, menor do que a primeira, entre no eu devido lugar. Na seqüência, o jogador segura a terceira carta e a compara com a segunda. Caso seja menor, então empurra-se a segunda carta para o lugar da terceira e prossegue-se comparando se a terceira carta é menor do que a primeira. Caso seja, então a primeira carta também é empurrada para a direita, abrindo vaga para que a terceira carta entre na primeira posição por ser a menor carta dentre as comparadas. Isto é feito sucessivamente, até que não haja mais cartas para serem comparadas. Na computação, pode-se notar que o comportamento desse algoritmo, de modo genérico, consiste em achar comparar uma carta X com suas anteriores. Quando a carta X for menor, então a carta maior deve ser empurrada para a direita, a fim de garantir vaga para a carta X. A cada comparação realizada, deve-se empurrar a carta comparada para a direita, caso ela seja maior do que X. Quando a carta X for comparada com outra carta menor do que ela, então a carta X deve ser inserida imediatamente após essa maior carta. Após comparar a última carta com as demais, inserindo-a no seu devido lugar, a ordenação estará completada. Este algoritmo apresenta melhores resultados em estruturas com grau de ordenação elevado ou quando inserimos dados novos em estruturas já ordenadas. O algoritmo pode ser representado da seguinte forma:
Capítulo 1 -.7.. Complexidade do algoritmo
O maior custo deste algoritmo está em tentar se descobrir onde abrir espaço para inserir a carta que se deseja colocar na ordem. Pelo fato dos valores a esquerda já estarem ordenados, pode-se melhorar o desempenho do algoritmo de ordenação por inserção, aplicando-se o algoritmo de busca binário para localizar o ponto onde o valor deverá ser inserido.
8.. Ordenação por Bolhas (Bouble Sort)
É um algoritmo tão simples de se compreender e executar quanto os anteriores. Na computação, pode-se notar que o comportamento desse algoritmo, de modo genérico, consiste em percorrer a estrutura de dados comparando-se as posições justapostas (lado a lado). A cada comparação verifica-se se o elemento da direita é menor do que o elemento à esquerda. Caso seja menor, então eles estão em ordem contrária, por isto devem
ser trocados de posição um com o outro. Caso seja maior, então a ordem está correta, por isto, mantêm-se suas posições. Esse procedimento de comparar todos os elementos dois a dois, deve ser repetido N vezes, onde N representa o tamanho do vetor. No final desse procedimento, a estrutura estará ordenada. O algoritmo pode ser representado da seguinte forma:
Capítulo 1 -.8.. Complexidade do algoritmo
Porém pelo fato de percorrer a estrutura várias vezes, não é recomendado para algoritmos que tenham necessidade de serem rápidos.
9.. Ordenação Por Fusão (Merge Sort)
Este algoritmo é um pouco mais complexo que os algoritmos acima. Desenvolvido por John von Neumann em 1945, o algoritmo consiste na idéia de divisão e conquista. A entrada é feita em um vetor ( ou também em uma lista por exemplo ) com n elementos e cria-se um vetor auxiliar para que a ordenação seja armazenada. Se n for ímpar, divide-se em chão(n/2) e chão(n/2+1), se n for par tem-se duas divisões com n/2 elementos. Todas elas começando do primeiro elemento ao meio e do meio ao ultimo elemento. A figura abaixo ilustra os passos do algoritmo para um vetor de valores aleatórios.
Figura – Ilustração do Funcionamento do Algoritmo de Ordenação por Fusão
Na prática o algoritmo consiste de duas funções básicas denominadas Fusão e Fundir. A função Fusão divide o vetor em partes menores e em seguida chama a função Fundir para unir de maneira ordenada as partes dividas. A função Fundir compara os elementos do vetor dividido e os ordena e de juntar os elementos ordenados, para as partes do vetor como mencionado acima e como ilustrado na figura.
Capítulo 1 -.9.. Complexidade do algoritmo
Complexidade de tempo: Θ( n log2 n ) Complexidade de espaço: Θ( n log2 n )
É possível implementar o merge sort utilizando somente um vetor auxiliar ao longo de toda a execução, tornando assim a complexidade de espaço adicional igual a Θ(n). É possível também implementar o algoritmo com espaço adicional Θ(1).
10.. Ordenação Rápida (Quick Sort)
Em 1960, o estudante Hoare visitou a Universidade de Moscou e, durante esta visita, inventou o algoritmo de ordenação chamado Quick Sort, publicado anos depois com as devidas alterações. Seu funcionamento também baseia-se no princípio de dividir para conquistar, da mesma forma que o algoritmo de ordenação por fusão. Na computação, pode-se notar que o comportamento desse algoritmo, de modo genérico, consiste em dividir o vetor em partes menores, sendo que a cada divisão, os menores elementos devem ficar a esquerda da posição onde será feita a divisão, e os maiores elementos à direita. No final dessa divisão, obtém-se vários vetores de apenas um elemento. Após isto, o algoritmo passa a montar as partes de maneira ordenada. Ao término dessa montagem o vetor está ordenado. Na prática o algoritmo consiste de duas funções básicas denominadas SelecionaPivo e OrdenaRápido. A função SelecionaPivo sorteia uma posição do vetor e o elemento dessa posição é denominado de pivô. O procedimento OrdenaRápido compara todos os elementos do vetor com o pivô. Se o elemento for menor do que o pivô então o elemento deve ser colocado a esquerda desse pivô. Se o elemento for maior do que o pivô então o elemento deve ser colocados a direita desse pivô. A idéia básica do algoritmo é dividir a estrutura de dados em duas partes através de um pivô. Feito isto percorre-se a estrutura para antes do pivô e para depois procurando elementos maiores que o pivô e menores que o pivô respectivamente. Quando encontrados, os elementos são trocados de lugar já que estão na parte errada da divisão da estrutura. Tal procedimento é feito até que os auxiliares de busca se cruzem em algum ponto do vetor. Abaixo temos o algoritmo em linguagem de alto nível estruturado:
Função SelecionaPivo(v N : inteiro ):inteiro
Variáveis
pos:inteiro;
início
{A função Sorteio escolhe aleatoriamente um valor de 1 até N. sendo que os valores têm a mesma probabilidade de serem sorteados} retorne sorteio( N ; DistribuiçãoUniforme);
fim.
Procedimento OrdenaRápido(Var vetor:Vetor; inicio, fim:inteiro)
Variáveis antes, depois, pivô : inteiro;
inicio
antes = inicio depois = fim pivô = SelecionaPivo( N );
repita enquanto (vetor[antes] < pivô) antes <- antes + 1 enquanto (vetor[depois] > pivô) depois <- depois - 1 se (depois >= antes) então auxiliar = vetor[antes]; vetor[antes] = vetor[depois] vetor[depois] = auxiliar depois <- depois - 1 antes <- antes + 1 fim se; até (antes > depois) se (depois > inicio) OrdenaRápido(vetor, inicio, depois); se (antes < fim) OrdenaRápido(vetor, antes, fim); fim.
A figura na página seguinte ilustra a situação em um vetor aleatório:
Figura – Ilustração do Funcionamento do Algoritmo de Ordenação Rápida
10...1.. Complexidade do algoritmo
Quicksort is a well-known sorting algorithm developed by C. A. R. Hoare that, on average, makes Θ(n log n) comparisons to sort n items. However, in the worst case, it makes Θ(n2) comparisons. Typically, quicksort is significantly faster in practice than other Θ(n log n) algorithms, because its inner loop can be efficiently implemented on most architectures, and in most real-world data it is possible to make design choices which minimize the possibility of requiring quadratic time. Quicksort is a comparison sort and, in efficient implementations, is not a stable sort.
11.. Critérios para Escolha do Algoritmo
Supondo que você está envolvido no desenvolvimento de uma solução e precisa definir qual dos algoritmos de busca em vetor utilizar, o principal critério a ser considerados para se escolher o algoritmo de maneira técnica e consciente, diz respeito à Ordenação da estrutura. Quando a estrutura estiver ordenada, o algoritmo de busca binária deve ser utilizado, caso contrário o seqüencial. Analisando os gráficos de complexidade dos algoritmos, podemos entender melhor essa escolha:
12.. Exercícios Resolvidos
Práticas de programação, dicas sobre como comentar o código,
Práticas para testar o
Biblioteca é um arquivo que contém uma estrutura de dados e um conjunto de funções e procedimentos que podem ser reusados por um programa de computador. Por exemplo, ao escrever um programa em Pascal, um programador não precisa programar os comandos writeln e read , todas as vezes que cria um novo programa. Ao invés disto, o programador declara a linha de comando “uses crt;”, que nada mais faz do que dizer ao programa que ele se utilizará da biblioteca chamada crt, e é dentro desta biblioteca que se encontra programada a função read e o procedimento writeln. Outro uso para as Unit é para definir Tipos Abstratos de Dados, por exemplo, uma fila, pilha, vetor, árvores, grafo, dentre outros, onde a estrutura de dados e as funções e procedimentos são colocados na mesma biblioteca, podendo ser reutilizado por outros programas.
Quando um programador deseja escrever um programa em Pascal, ele deve obedecer a seguinte estrutura: program
Quando você quiser escrever uma biblioteca você deve obedecer a seguinte estrutura: Unit
Interface <Declaração das funções que a biblioteca conterá>
Implementation
<Código das funções declaradas em “Interface”>
End. (final da Unit}
Unit ROTINAS; {O nome da biblioteca DEVE ser o mesmo do arquivo, sem a extensão “.pas”}
Interface { Contém o protótipo de todas as funções dessa biblioteca} Function COMP_CIRC (R:real) : real; {calcula comprimento de um circulo de raio R} Function AREA_CIRC (R:real) : real; {calcula área de um circulo de raio R}
Implementation {Contém o código dos procedimentos e das funções declaradas} Const PI=3.1416; {Qualquer função ou procedimento enxerga esta constante, bem como