Baixe o app para aproveitar ainda mais
Prévia do material em texto
CENTRO PAULA SOUZA FACULDADE DE TECNOLOGIA DE FRANCA “Dr. THOMAZ NOVELINO” TECNOLOGIA EM ANÁLISE E DESENVOLVIMENTO DE SISTEMAS CAIRO DE PAULA VIEIRA COMPLEXIDADE ALGORÍTMICA FRANCA/SP 2020 CAIRO DE PAULA VIEIRA COMPLEXIDADE ALGORÍTMICA Ensaio Acadêmico apresentado à Disciplina de Estrutura de Dados, como parte dos requisitos necessários a aprovação da mesma. Docente: Fausto Gonçalves Cintra FRANCA/SP 2020 SUMÁRIO 1 Introdução ………………………………..………………………………… 3 2 Algoritmos .…………………………………………………………………. 4 3 Complexidade Algorítmica ………………………………………………. 6 3.1 Classes de Complexidade Assintótica………………………………….…. 6 3.1.1 Notação Teta (Θ) ……………………………...…………………………..... 7 3.1.2 Notação O (O) …………………………...……………………………..…… 8 3.1.3 Notação Ômega (Ω) …………………………...……………………………. 9 3.2 Análise de Pior Caso ……………………………………………………...... 9 4 Algoritmos de Ordenação …………………………………………….….. 11 4.1 Bubble Sort ………………………………………………..…………….…… 11 4.2 Selection Sort ……………………………………..……………….....……... 12 4.3 Merge Sort …………………………………………………….……………... 12 4.4 Quick Sort …………………………………………………….……………… 13 5 Análise Comparativa …………………………………………………….... 14 6 Discussão e Conclusão …………………………………………………... 15 Referências ……………………………………………………………………..……. 16 3 1 INTRODUÇÃO Hoje vivemos uma era em que a tecnologia computacional onipresente está mais firmemente estabelecida, afinal, a vivemos de perto nos diversos aspectos de nossas vidas cotidianas - para nos divertir, comprar, trabalhar, viajar, estudar, nos comunicar etc - que por sua vez, são mediados, produzidos e regulados por meio de dispositivos digitais e sistemas alimentados por softwares (GREENFIELD, 2006; KITCHIN & DODGE, 2011; MANOVICH, 2013; STEINER, 2012; apud KITCHIN, 2016). Os softwares são essencialmente compostos por algoritmos que por sua vez são conjuntos de etapas estruturadas para processar instruções e dados a fim de produzir uma saída. (GILLESPIE, 2014a; apud KITCHIN, 2016). Sem os algoritmos, não estaríamos vivendo do modo como vivemos hoje, haja visto que eles estão ligados à criptografia, recomendação de marketing, reconhecimento de padrões, compactação de dados, etc. (MACCORMICK, 2013; apud KITCHIN, 2016). Apesar de sua grande importância, seu estudo é frequentemente ignorado por diversas pessoas da área, desde universitários até profissionais já experientes. Os argumentos mais utilizados giram em torno do fato de que “as linguagens de programação atuais já implementam os algoritmos de forma mais eficiente” ou ainda “que os recursos de hardware disponíveis no mercado tornam essa questão desnecessária”. Todavia, como diz Cormen et al. (2009), por mais rápidos que os computadores sejam, eles não são infinitamente velozes e por mais baratas que as memórias sejam, elas não são gratuitas. Deste modo, se aprofundar no estudo dos algoritmos e estrutura de dados, analisando sua eficiência e eficácia, seja por uso de memória computacional ou análise do tempo para a execução de determinadas tarefas, torna-se imprescindível. O intuito deste ensaio acadêmico é discutir brevemente sobre os algoritmos e a complexidade algorítmica e apresentar uma tabela de comparação de tempo de quatro diferentes tipos de algoritmo de ordenação: Bubble Sort, Selection Sort, Merge Sort e Quick Sort. 4 2 ALGORITMOS Informalmente, um algoritmo é qualquer procedimento computacional bem definido que recebe algum valor, ou conjunto de valores como entrada e produz algum valor, ou conjunto de valores, como saída/resultado. Também podemos visualizar um algoritmo como uma ferramenta para resolver um problema computacional bem especificado (CORMEN et al., 2009). Um algoritmo pode ser especificado em uma língua natural como o Português, como um programa de computador ou mesmo como um design de hardware. O único requisito é que a especificação deve fornecer uma descrição precisa do procedimento computacional a ser seguido. Um exemplo bastante comum na prática é a classificação de uma determinada sequência de números de maneira crescente. Entrada: Sequência de n números (a1, a2, a3, …, an); Saída: A permutação (reordem) (a’1, a’2, a’3, …, a’n) da entrada de forma que a’1 ≤ a’2 ≤ a’3 ≤ … ≤ a’n. Ou seja, temos a instância de um problema que consiste na entrada e sua solução que consiste na saída. Dizemos que um algoritmo está correto se, para cada instância de entrada, ele trazer uma saída correta, em outras palavras, resolve o problema. A reordenação numérica não é de forma alguma o único problema computacional para o qual os algoritmos foram desenvolvidos. Como dito anteriormente, temos várias aplicações práticas de algoritmos como o Projeto Genoma Humano; o acesso à Internet; a possibilidade do comércio eletrônico; a melhor rota entre um trajeto de um ponto a outro (CORMEN et al., 2009). Como diz Steiner (2012), os algoritmos já têm controle sobre os fundos do mercado monetário, ações e contas de aposentadoria, com quem conversamos por telefone, quais músicas ouvimos, quais as chances de se conseguir órgãos para transplante e para milhões de pessoas, podem até decidir com quem elas se casarão (apud KITCHIN, 2016). A lista é exaustivamente grande, mas apresenta uma característica comum a muitos problemas algorítmicos. Como temos muitos desafios, temos também muitas soluções já estabelecidas na literatura e vivência prática, podendo algumas serem as mais eficientes para determinada questão, ou ainda constatar que para outras, não temos uma solução eficiente. 5 A principal medida usada para estabelecer a eficiência de um algoritmo é a velocidade, ou seja, o tempo necessário que um algoritmo leva para produzir seu resultado. Essa análise e metrificação é fundamental e objeto de estudo crescente na área da Computação, compreendendo não só a observações de algoritmos considerados “padrão ouro”, como também a investigação de novas soluções e suas complexidades. 6 3 COMPLEXIDADE ALGORÍTMICA Depois que um algoritmo é desenvolvido para resolver um problema computacional é inevitável fazer a pergunta: "Seria possível torná-lo melhor?". Geralmente é possível sim pensar em soluções cada vez mais eficientes, mas eventualmente, estamos diante de uma barreira quase invisível, que torna as melhorias muito difíceis de alcançar. Desta forma, depois de inúmeras tentativas, é natural se perguntar se o “ponto ótimo” de determinado algoritmo já foi alcançado e para provar esta hipótese, torna-se necessário embasar-se em técnicas matemáticas formais. A esse conjunto de modelos e técnicas, damos o nome de Complexidade Algorítmica (RALSTON; EDWIN; HEMMENDINGER, 2003). Vários autores divergem entre si sobre qual seria o método mais adequado para medir a complexidade. Embora a maioria tenha acordado que tempo e espaço computacional são os mais indicados. Kronsjö (1987) e Rosen (1999) definem a complexidade de tempo de um algoritmo como a quantidadede tempo usada por um computador para resolver um problema de um tamanho específico usando o algoritmo. Eles definem a complexidade espacial de um algoritmo como a quantidade de memória computacional necessária para implementar o algoritmo. Assim, a complexidade do espaço e do tempo está ligada às estruturas de dados específicas usadas para implementar o algoritmo. Além disso, eles descrevem a complexidade computacional de um algoritmo como o poder computacional necessário para resolver o problema. Isso é medido contando o número de operações elementares realizada pelo algoritmo, como por exemplo número de multiplicações e/ou adições necessárias para avaliar um polinômio, o número de comparações necessárias para ordenar uma sequência, o número de multiplicações necessárias para multiplicar duas matrizes, o número de instruções para alcançar determinado fim etc. (apud NASAR, 2016). 3.1 Classes de Complexidade Assintótica Contar o número de instruções é uma das maneiras de analisar a complexidade de um algoritmo - como já mencionado - todavia, seria uma tarefa muito maçante ficar contando o número de instruções para cada trecho de código, sem considerar o fato de que esse número varia muito dependendo da escolha da linguagem, compiladores e até o processador utilizado. 7 Também como já foi elucidado, outra forma de analisar a complexidade é executar determinado algoritmo e medir sua velocidade utilizando alguma unidade de tempo, no entanto, esse tipo de análise não é mais confiável, haja visto que a linguagem empregada no algoritmo, o programa em que está sendo executado, os dados de entrada, e a capacidade de processamento da máquina, variam de máquina para máquina e escolha do programador, tornando a classificação, de certa forma, não muito confiável (IQBAL et al., 2019). Com isso em mente, uma forma de driblar esses vieses é empregar a análise assintótica. Neste tipo de análise, leva-se em conta apenas grandes entradas de dados, afinal, para valores pequenos, a maioria dos algoritmos levará pouco tempo e o que faz um profissional escolher um algoritmo em detrimento de outro é o seu desempenho para grandes massas de dados. Ou seja, a análise assintótica se preocupa com o aumento do tempo de execução a medida que o tamanho da entrada aumenta indefinidamente (ASCENCIO; ARAÚJO, 2011). Matematicamente falando, quando analisamos qualquer algoritmo, geralmente obtemos uma fórmula para representar a quantidade de tempo necessária para a execução ou o tempo necessário para o computador executar as linhas de código do algoritmo, número de acessos à memória, número de comparações, variáveis temporárias que ocupam memória etc. Essa fórmula geralmente contém detalhes sem importância que realmente não nos dizem nada sobre o tempo de execução. Por exemplo, se algum algoritmo tiver uma complexidade de tempo de T(n) = (n2 + 3n + 4), que é uma equação quadrática. Para grandes valores de n, a parte 3n + 4 se tornará insignificante em comparação com a parte n2. Para n = 1000, n2 será 1000000 enquanto 3n + 4 será 3004, por exemplo (AHLAWAT, 2020). Portanto, um algoritmo que demora 200n2 será mais rápido que outro algoritmo que leva tempo n3, para qualquer valor de n maior que 200. Como estamos interessados apenas no comportamento assintótico do crescimento da função, o fator constante pode ser ignorado. Basicamente usa-se três tipos de notações assintóticas para representar o crescimento de qualquer algoritmo, à medida que a entrada aumenta: Notação Teta (Θ), Notação O (O) e Notação Ômega (Ω). 3.1.1 Notação Teta (Θ) A complexidade de tempo representada pela notação Θ é semelhante ao valor ou 8 intervalo médio dentro do qual será o tempo real de execução do algoritmo. Por exemplo, se para algum algoritmo a complexidade do tempo é representada pela expressão 3n2 + 5n, e usamos a notação Θ para representá-lo, a complexidade do tempo seria Θ(n2), ou seja, ignora-se o coeficiente constante e remove a parte insignificante, que é 5n. Figura 1 - Representação Gráfica da Notação Teta (Θ) Fonte: StudyTonight, 2020. No exemplo acima, a complexidade de Θ(n2) significa que o tempo médio de qualquer entrada n permanecerá no meio, k1 * n2 e k2 * n2, onde k1 e k2 são duas constantes, vinculando firmemente a expressão que representa o crescimento do algoritmo (AHLAWAT, 2020). 3.1.2 Notação O (O) Essa notação é conhecida como o limite superior do algoritmo ou o pior caso de um algoritmo. Ela nos diz que uma determinada função nunca excederá um tempo especificado para qualquer valor da entrada n. A questão é por que precisamos dessa representação quando já temos a notação Θ, que representa o tempo de execução fortemente vinculado a qualquer algoritmo? Vamos tomar como exemplo o algoritmo de Busca Linear, no qual percorremos os elementos de um vetor, um por um, para pesquisar um determinado número. Na pior das hipóteses, encontraremos o elemento ou o número que estamos procurando no final, o que 9 levará a uma complexidade de tempo n, onde n representa o número de elementos totais. Mas pode acontecer do elemento procurado ser o primeiro, nesse caso, a complexidade do tempo será 1. Assim, dizer que a complexidade de tempo na notação Θ para a Busca Linear é Θ(n), significa que o tempo necessário sempre estará relacionado a n, pois esse é o caminho para representar a complexidade média do tempo, mas quando usamos a notação O, queremos dizer que a complexidade do tempo é O(n), ou seja, a complexidade do tempo nunca excederá n, definindo o limite superior e, portanto, dizendo que ela pode ser menor ou igual a n, que é a representação correta (AHLAWAT, 2020). Por esse motivo, na maioria das vezes, a notação O é usada para representar a complexidade do tempo de qualquer algoritmo. 3.1.3 Notação Ômega (Ω) A notação Ω é usada para definir o limite inferior de qualquer algoritmo ou o melhor caso de qualquer algoritmo. Isso indica o tempo mínimo necessário para qualquer algoritmo para todos os valores de entrada, portanto, o melhor caso de qualquer algoritmo. Em palavras simples, quando representamos uma complexidade de tempo para qualquer algoritmo na notação Ω, queremos dizer que o algoritmo levará pelo menos esse tempo para completar sua execução (AHLAWAT, 2020). 3.2 Análise de Pior Caso Comparar algoritmos e determinar qual o melhor diante de determinado cenário, não é tarefa fácil, isso porque além dos pontos já mostrados, diferentes formas de entrada de dados influenciam diretamente nessa questão. Por exemplo, alguns algoritmos podem ter um desempenho melhor do que outros em dados já ordenados e pior em dados totalmente desordenados. Quando dois algoritmos têm desempenho incomparável, como podemos considerar um deles "melhor” que o outro? A análise de pior caso é uma ferramenta utilizada na análise de algoritmos, em que o desempenho geral de um algoritmo é resumido por seudesempenho no pior cenário possível em qualquer entrada de um determinado tamanho, assim, o “melhor” algoritmo é aquele com 10 melhor performance no pior caso, trazendo em termos populares, este tipo de análise seria como a "Lei de Murphy". Embora grosseira, a análise do pior caso pode ser extremamente útil, e é o paradigma dominante para a análise de algoritmos na ciência da computação teórica (ROUGHGARDEN, 2019). Para ilustrar melhor vamos usar como exemplo um array A de cinco elementos A = [10, 50, 30, 20, 40] em que desejamos ordenar de forma crescente. O pior caso seria a ordenação desse vetor na seguinte forma A = [50, 40, 30, 20, 10], enquanto o melhor caso seria o oposto, ou o vetor já ordenado A = [10, 20, 30, 40, 50]. 11 4 ALGORITMOS DE ORDENAÇÃO Os algoritmos de ordenação nada mais são que algoritmos que colocam os elementos de uma determinada entrada de dados em ordem, ou seja, efetua a ordenação de forma completa ou parcial. O problema de ordenação de dados foi um dos primeiros e mais estudados tópicos em Ciência da Computação, uma vez que seu uso é rotineiro no dia a dia prático. Eles podem ser utilizados em situações particulares de triagem, cálculos de processamento de dados, melhoria de desempenho da memória etc. Por serem tão comuns, diversos tipos desses algoritmos foram criados, cada um com suas especificidades e uns julgando-se serem mais rápidos do que outros, porém, todos com apenas um intuito, facilitar o trabalho dos profissionais da área (JUGÉ, 2020). Embora existam múltiplos algoritmos de ordenação, neste ensaio contemplaremos quatro deles: Bubble Sort, Selection Sort, Merge Sort e Quick Sort. 4.1 Bubble Sort O algoritmo de ordenação Bubble Sort é um dos algoritmos mais antigos e mais simples, contudo, sua criação é incerta. Estudos antigos mostram que um algoritmo de ordenação com funcionamento semelhante já era utilizado em 1956 sob o nome de “ordenação por troca”, todavia, o termo Bubble Sort data de 1962, quando foi usado por Kenneth E. Iverson (1962) em seu livro “Uma Linguagem de Programação” (apud ASTRACHAM, 2003). Seu funcionamento caracteriza-se na comparação de todos os elementos fornecidos um a um, classificando-os de acordo com o seu valor. Se os dados precisam ser ordenados em ordem crescente, por exemplo, o algoritmo começará comparando o primeiro elemento com o segundo; se o primeiro elemento for maior que o segundo, ele trocará a posição dos dois, em seguida, prosseguirá para comparar o segundo elemento com o terceiro, e assim por diante. Quanto ao tipo, ele é classificado como um algoritmo de permuta e quanto à complexidade de tempo é tido como O(n2) na análise de pior caso, O(n) na de melhor caso, O(n2) na complexidade média e O(1) na análise de espaço (AHLAWAT, 2020). 12 4.2 Selection Sort Assim como o Bubble Sort, a criação do Selection Sort também é incerta. Uma de suas primeiras aparições está no artigo “Os Princípios de Ordenação” de Bell (1958), o qual cita dentre diversos tipos de ordenação, a do tipo seleção (apud ASTRACHAM, 2003). Seu funcionamento também é simples e ocorre da seguinte forma: primeiro encontra-se o menor elemento da sequência e é feita a troca com o elemento da primeira posição; depois, encontra-se o segundo menor elemento e é feita a troca com o elemento da segunda posição; e assim sucessivamente até toda a entrada estar ordenada. Ele é classificado quanto ao tipo em um algoritmo do tipo seleção e permuta, e quanto à complexidade é categorizado em O(n2) na análise de pior caso, melhor caso e complexidade média e em O(1) na análise de espaço (AHLAWAT, 2020). 4.3 Merge Sort O Merge Sort é tão importante na história dos algoritmos de ordenação, quanto a ordenação é importante na história da computação. Uma descrição detalhada do algoritmo apareceu pela primeira vez em um relatório de Goldstine e Von Neumann (1948), tendo assim o segundo autor como o inventor da operação (apud KATAJAINEN; TRÄFF, 1997). O algoritmo segue a regra de “dividir e conquistar” para ordenar determinado conjunto, isso acontece por meio da recursividade, o que leva a uma ordenação mais rápida, mesmo que consuma um espaço maior na memória. A ideia é dividir um grande problema (sequência a ser organizada) em subproblemas menores (vetores menores da sequência) para que depois seja feita a combinação das soluções desses subproblemas em uma grande solução (sequência ordenada). O vetor a ser ordenado é dividido ao meio, depois em subvetores, até que cada um tenha apenas um elemento. Depois, a ideia é que esse subvetor faça o caminho inverso, se recompondo ao seu tamanho original, porém, de forma ordenada, para que por fim, tenhamos atingido o objetivo. Portanto, ele é classificado como um algoritmo de divisão e conquista e quanto à complexidade em O(n*log n) na análise de pior caso, melhor caso e complexidade média e em O(n) na análise de espaço (AHLAWAT, 2020). 13 4.4 Quick Sort O Quick Sort também é baseado no conceito de divisão e conquista e seu inventor foi Hoare, que o descreveu em um trabalho de 1962 (HOARE, 1962). Ele funciona da seguinte maneira: primeiramente é selecionado um elemento como pivô (geralmente o que ocupa a última posição) e o conjunto é então dividido. Os elementos são posicionados de forma que todos os elementos menores que o pivô fiquem à esquerda dele e os maiores, à direita. Ao fim do processo o pivô estará em sua posição final e haverá duas sub listas não ordenadas. Recursivamente, a ordenação acontece nos subconjuntos dos valores menores e depois, nos maiores. Como dito anteriormente, o algoritmo se enquadra na classificação de divisão e conquista e quanto à complexidade é categorizado em O(n2) na análise de pior caso, O(n*log n) na análise de melhor caso e complexidade média e também na análise de espaço (AHLAWAT, 2020). 14 5 ANÁLISE COMPARATIVA Abaixo temos a análise comparativa dos quatro algoritmos de ordenação aqui expostos para amostras de 1000, 10000, 25000 e 100000 nomes em um notebook com as seguintes configurações. Tabela 1 - Especificação do equipamento Sistema Operacional Ubuntu 20.04 LTS Processador Intel® Core i7-4500U CPU @ 1.80Ghz x 4 Arquitetura (32 ou 64 bits) 64 bits Memória RAM Total 8 GB Versão do Node.js 12.16.3 Fonte: Fausto Gonçalves Cintra, 2020. Tabela 2 - Análise de eficiência Tamanho da amostra Algoritmo 1000 10.000 25.000 100.000 Bubble Sort 18,487ms 0,018487s 2096,840ms 2,09684s 14666,064ms 14,666064s 246704,901ms 4,11174835 min Selection Sort 11,870ms 0,01187s 746,299ms 0,746299s 4731,262ms 4,731262s 80268,434ms 1,33780723333 min Merge Sort 10,897ms 0,010897s 22,097ms 0,022097s 40,818ms 0,040818s 116,118ms 1,9353 min Quick Sort 2,652ms 0,002652s 8,218ms 0,008218s 15,801ms 0,015801s 55,685ms 0,055685s Fonte: Fausto Gonçalves Cintra, 2020. 156 DISCUSSÃO E CONCLUSÃO Como observado na Análise Comparativa, poderíamos inferir que o algoritmo Quick Sort é o melhor a ser utilizado para a ordenação de um determinado conjunto de dados, no entanto, precisamos ter em mente, que o melhor algoritmo é aquele que possui a menor complexidade de tempo e espaço, de acordo com a máquina e linguagem utilizadas. Este ensaio foi apenas uma breve apresentação sobre a Complexidade Algorítmica e sua importância. Para entender mais sobre o assunto é necessário estudar a fundo os tópicos citados e além. Implementar algoritmos eficientes e eficazes são primordiais para construir aplicações de qualidade e que permitam uma boa experiência ao usuário. Embora os recursos de hardware vêm se tornando cada vez mais baratos e eficientes, a necessidade de entender como melhorar o dia a dia profissional é de extrema importância. Afinal, o conhecimento e técnica de algoritmos é algo que separa os excelentes programadores dos medianos. Claro que com a tecnologia computacional atual, é possível executar trabalhos sem saber muito sobre o assunto, mas conhecendo-o é possível fazer muito mais. 16 REFERÊNCIAS AHLAWAT, Abhishek. StudyTonight. Disponível em: https://www.studytonight.com/data-structures/aysmptotic-notations. Acesso em: 19 maio 2020. AHLAWAT, Abhishek. StudyTonight. Disponível em: https://www.studytonight.com/data-structures/bubble-sort. Acesso em: 19 maio 2020. AHLAWAT, Abhishek. StudyTonight. Disponível em: https://www.studytonight.com/data-structures/merge-sort. Acesso em: 19 maio 2020. AHLAWAT, Abhishek. StudyTonight. Disponível em: https://www.studytonight.com/data-structures/quick-sort. Acesso em: 19 maio 2020. AHLAWAT, Abhishek. StudyTonight. Disponível em: https://www.studytonight.com/data-structures/selection-sorting. Acesso em: 19 maio 2020. ASCENCIO, Ana Fernanda Gomes; ARAÚJO, Graziela Santos de. Estruturas de Dados: algoritmos, análise da complexidade e implementações em JAVA e C/C++. São Paulo: Pearson, 2011. ASTRACHAN, Owen. Bubble Sort: An Archaeological Algorithmic Analysis. ACM SIGCSE Bulletin. Durham, p. 1-5. jan. 2003. CORMEN, Thomas H. et al. Introduction to Algorithms. 3. ed. Cambridge: Mit Press, 2009. HOARE, C. A. R.. Quicksort. The Computer Journal, [s.l.], v. 5, n. 1, p. 10-16, 1 jan. 1962. IQBAL, Nadeem. et al. Formalization of Asymptotic Notations in HOL4. 2019 Ieee 4th International Conference On Computer And Communication Systems. Islamabad, p. 383-387. fev. 2019. JUGÉ, Vincent. Adaptive Shivers Sort: An Alternative Sorting Algorithm. SODA '03: Proceedings Of The Fourteenth Annual ACM-SIAM Symposium On Discrete Algorithms. Philadelphia, p. 1639-1654. jan. 2020. KATAJAINEN, Jyrki; TRÄFF, Jesper Larsson. A Meticulous Analysis of Mergesort Programs. Algorithms And Complexity, Berlin, v. 1203, n. -, p. 217-228, mar. 1997. KITCHIN, Rob. Thinking critically about and researching algorithms. Information, Communication & Society. County Kildare, p. 14-29. 25 fev. 2016. NASAR, Audrey A.. The history of Algorithmic complexity. The Mathematics Enthusiast. Missoula, p. 217-237. ago. 2016. 17 REILLY, Edwin D.; RALSTON, Anthony; HEMMENDINGER, David. Encyclopedia of Computer Science. 5. ed. California: Nature Publishing Group, 2003. ROUGHGARDEN, Tim. Beyond worst-case analysis. Communications Of The ACM, [s.l.], v. 62, n. 3, p. 88-96, 21 fev. 2019.
Compartilhar