Baixe o app para aproveitar ainda mais
Prévia do material em texto
Algoritmos AvanAlgoritmos Avanççadosados Professor Alessandro Larangeiras UNIDADE I ANÁLISE DE ALGORITMO: NOTAÇÃO O 1.1 Algoritmo 1.2 Estrutura de Dados 1.2.1 Revisão de Programas em C++ envolvendo Vetores, Matrizes, Ponteiros, Registros (Struct) e Funções. 1.3 O que é Analise de Algoritmos 1.4 Sobre o Elemento da Análise Assintótica - Notação O 1.4.1 Notação O 1.4.2 Sobre a função 1.4.3 Operações com a Notação O CONTEÚDO UNIDADE II RECURSIVIDADE 2.1 Definições recursivas 2.2 Como implementar recursividade 2.3 Quando não usar recursividade 2.4 Desenvolvendo algoritmos com recursividade UNIDADE III ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO (MERGESORT) 3.1 Definição 3.2 Dividir para conquistar 3.3 Problema da intercalação 3.4 O algoritmo de ordenação por intercalação mergesort. 3.5 Análise da complexidade do algoritmo mergesort. CONTEÚDO UNIDADE IV ALGORITMO DE ORDENAÇÃO RÁPIDA (QUICKSORT) 4.1 Definição 4.2 Ordenação rápida 4.3 O algoritmo de ordenação rápida quicksort 4.4 Análise da complexidade do algoritmo quicksort CONTEÚDO UNIDADE V ESTRUTURAS DE DADOS DOS TIPOS ÁRVORE BINÁRIA E ÁRVORE AVL 5.1 Árvores, Árvores Binárias e Árvores Binárias de Busca 5.2 Implementando as Árvores Binária 5.3 Percorrendo uma Árvore Binária de Busca 5.4 Percurso em Árvore 5.5 Inserção 5.6 Remoção 5.7 Balanceando uma Árvore 5.7.1 O Algoritmo DSW 5.7.2 Árvores AVL CONTEÚDO UNIDADE VI ALGORITMOS EM GRAFOS 6.1 Conceitos de grafos. 6.2 Representação de grafo 6.3 Algoritmos de busca 6.4 Algoritmo do caminho mínimo 6.4.1 Encontrar o melhor caminho do vértice origem ao vértice destino CONTEÚDO [...] [Algoritmo é a descrição de] um procedimento para resolver um problema[, feita em determinada linguagem] [...]. SEDGEWICK; WAYNE, 2011, p. 4. ALGORITMO Algoritmo Euclidiano em Linguagem de Programação Java public static int gcd(int p, int q) { if(q == 0) return p; int r = p % q; return gcd(q, r); } SEDGEWICK; WAYNE, 2011, p. 4. Algoritmo Euclidiano em Linguagem Natural Calcule o máximo divisor comum de dois inteiros não-negativos P e Q do seguinte modo: se Q for 0, a resposta é P, se não, divida P por Q e obtenha o resto R; a resposta será o máximo divisor comum de Q e R. ALGORITMO Os tipos de dados manipulados por um algoritmo podem ser classificados em dois grupos: atômicos[, ou simples,] e complexos[,] ou compostos [...]. (LAUREANO, 2008, p. 2). TIPOS DE DADOS ATÔMICO COMPLEXO Se um tipo de dado pode ser decomposto, então o tipo de dado é dito estruturado [...]. (LAUREANO, 2008, p. 2). ESTRUTURA DE DADOS TIPOS DE DADOS & ESTRUTURA DE DADOS HOMOGÊNEAS HETEROGÊNEAS contêm um único tipo de dados contêm diferentes tipos de dados ESTRUTURAS DE DADOS POR TIPO DE CONTEÚDO VETORES (LAUREANO, 2008, p. 3/7). ESTRUTURAS HOMOGÊNEAS UNIDIMENSIONAIS #include <iostream> int main() { cout << "Hello, World!" << endl; } ESTRUTURAS HOMOGÊNEAS UNIDIMENSIONAIS #include <iostream> using std::cout; using std::endl; int main() { //... double nota[5]; nota[0] = 7.6; nota[1] = 8; nota[2] = 5.5; nota[3] = 7.0; nota[4] = 1.0; for(int i = 0; i < 5; i++) { cout << "Nota do aluno n." << (i +1) << ": "; cout << nota[i] << endl; } } ESTRUTURAS HOMOGÊNEAS UNIDIMENSIONAIS #include <iostream> using std::cout; using std::endl; int main() { //... // double peso[] = { 75, 85.2, 21.5, 90.5, 55.3 }; double peso[5] = { 75, 85.2, 21.5, 90.5, 55.3 }; for(int i = 0; i < 5; i++) { cout << "Peso do paciente n." << (i +1) << ": "; cout << peso[i] << endl; } } ESTRUTURAS HOMOGÊNEAS UNIDIMENSIONAIS #include <iostream> using std::cout; using std::endl; int main() { //... int idade[3]; for(int i = 0; i < 3; i++) { cout << "Digite o valor da idade n." << (i +1) << ": "; cin >> idade[i]; } for(int i = 0; i < 3; i++) { cout << "Idade n." << (i +1) << ": " << idade[i] << endl; } } ESTRUTURAS HOMOGÊNEAS UNIDIMENSIONAIS #include <iostream> using std::cin; using std::cout; using std::endl; int main() { //... const int TAM_MAX_NOME = 3; char letras[TAM_MAX_NOME]; for(int i = 0; i < TAM_MAX_NOME; i++) { cout << "Digite a letra n." << (i +1) << ": "; cin >> letras[i]; } for(int i = 0; i < TAM_MAX_NOME; i++) { cout << "Letra n." << (i +1) << ": " << letras[i] << endl; } } ESTRUTURAS HOMOGÊNEAS UNIDIMENSIONAIS MATRIZES (LAUREANO, 2008, p. 3/7). ESTRUTURAS HOMOGÊNEAS MULTIDIMENSIONAIS #include <iostream> int main() { //... int matrizA[1][4]; matrizA[0][0] = 3; matrizA[0][1] = 2; matrizA[0][2] = 1; matrizA[0][3] = 5; for(int l = 0; l < 1; l++) { for(int c = 0; c < 4; c++) { cout << "Matriz A[" << (l +1) << "," << (c +1); cout << "]= " << matrizA[l][c] << endl; } } } ESTRUTURAS HOMOGÊNEAS MULTIDIMENSIONAIS #include <iostream> using std::cout; using std::endl; int main() { //... int matrizB[2][4] = { {1, 2, 3, 4}, {5, 6, 7, 8} }; for(int l = 0; l < 2; l++) { for(int c = 0; c < 4; c++) { cout << "Matriz B[" << (l +1) << "," << (c +1); cout << "]= " << matrizB[l][c] << endl; } } } ESTRUTURAS HOMOGÊNEAS MULTIDIMENSIONAIS #include <iostream> // using... int main() { //... int matrizC[2][4]; for(int l = 0; l < 2; l++) for(int c = 0; c < 4; c++) { cout << "Digite o elemento [" << (l +1) << ","; cout << (c +1) << "] da matriz C: "; cin >> matrizC[l][c]; } for(int l = 0; l < 2; l++) for(int c = 0; c < 4; c++) { cout << "Matriz C[" << (l +1) << "," << (c +1); cout << "]= " << matrizC[l][c] << endl; } } ESTRUTURAS HOMOGÊNEAS MULTIDIMENSIONAIS REGISTROS (LAUREANO, 2008, p. 17). ESTRUTURAS HETEROGÊNEAS //... struct Rectangle { int left; int top; int width; int height; }; int main() { Rectangle pool = { 30, 40, 70, 80 }; Rectangle yard; yard.left = 0; yard.top = 0; yard.width = 100; yard.height = 120; cout << "Yard width = " << yard.width << endl; cout << "Pool width = " << pool.width << endl; } ESTRUTURAS HETEROGÊNEAS #include <iostream> using std::cout; using std::endl; int main() { int numero, valor; int* p; // "DataType*" = ponteiro para DataType. // int *p; numero = 55; p = № // "&VarName" = Address-Of operator. cout<< "p = “ << p << "; " << "&numero = " << &numero <<endl; cout<< "*p = " << *p << "; " << "numero = " << numero <<endl; valor = *p; // "*VarName" = Indirection operator. cout << "valor = " << valor << endl; } PONTEIROS EM C++ #include <iostream> using std::cout; using std::endl; void troca(int a, int b) { int temp = a; a = b; b = temp; } int main() { int a = 2; int b = 3; cout<< "Antes da troca: A = " << a << ", B = " << b <<endl; troca(a, b); cout<< "Depois da troca: A = " << a << ", B = " << b <<endl; } FUNÇÕES EM C++: PASSAGEM DE ARGUMENTOS POR VALOR #include <iostream> using std::cout; using std::endl; void troca(int& a, int& b) { int temp = a; a = b; b = temp; } int main() { int a = 2; int b = 3; cout<< "Antes da troca: A = " << a << ", B = " << b <<endl; troca(a, b); cout<< "Depois da troca: A = " << a << ", B = " << b <<endl; } FUNÇÕES EM C++: PASSAGEM DE ARGUMENTOS POR REFERÊNCIA 1. Desenvolva um programa em C++ que: a) receba a quantidade de alunos matriculados em um curso de extensão da Estácio, informada pelo usuário; b) receba a nota de cada aluno, informada pelo usuário; c) calcule e exiba a média aritmética das notas dos alunos; d) calcule e exiba quantas notas são maiores ou iguais e quantas são inferiores a esta média. 2. Desenvolva um programa em C++ que leia uma matriz 3 x 3 e mostre os elementos da mesma matriz multiplicado pelo maior número carregado na matriz. EXERCÍCIOS int main() // Resposta do exercício #1 { int alunosQtd = 0; cout << "Quantidade de alunos: "; cin >> alunosQtd; double alunosNota[alunosQtd]; for(int i = 0; i < alunosQtd; i++) { cout << “Nota" << (i+1) << ": "; cin >> alunosNota[i]; } double media = 0; for(int i = 0; i < alunosQtd; i++) media += alunosNota[i]; media /= alunosQtd; cout << "Media: " << media << endl; int maioresQtd = 0; int menoresQtd = 0; for(int i = 0; i < alunosQtd;i++) { if(alunosNota[i] >= media) maioresQtd++; else menoresQtd++; } cout<< maioresQtd <<" maiores e "<< menoresQtd << " menores"; return 0; } ANÁLISE DE ALGORITMOS Ciência da Computação não cuida só de programação; há ciência real envolvida. Entre as muitas coisas que os cientistas fazem está a análise. Algoritmos podem ser analisados [...]. TOAL, 2018a. ANÁLISE DE ALGORITMOS A compexidade de um algoritmo reflete o esforço computacional requerido para executá-lo. Esse esforço computacional mede a quantidade de trabalho, em termos de tempo de execução ou da quantidade de memória requerida. As principais medidas de complexidade são tempo e espaço, relacionadas à velocidade e à quantidade de memória, respectivamente, para a execução de um algoritmo. TOSCANI; VELOSO, 2012, p. 14. ANÁLISE DE ALGORITMOS Análise de algoritmos é a ciência de se determinar a quantidade de tempo e espaço requeridos por computado- res [(complexidade computacional)] para executar vários procedimentos. VRAJITORU; KNIGHT, 2014, p. 1. Ou seja, análise de algoritmos é o estudo da complexidade computacional de algoritmos, de modo a aprimorar sua eficiência. ANÁLISE DE ALGORITMOS É importante ser capaz de medir, ou no mínimo fazer afirmações educadas sobre, a complexidade de espaço e tempo de um algoritmo. Isto possibilitará comparar os méritos de duas abordagens alternativas para a solução de um problema e determinar também se uma solução proposta atenderá as restrições de recurso requeridas antes de se investir dinheiro e tempo de codificação. TOAL, 2018a. A análise é feita antes da codificação. A perfilação (também chamada de benchmarking [[análise comparativa]]) é feita depois de o código estar escrito. TOAL, 2018a. ANÁLISE DE ALGORITMOS [...] [Uma maneira de se determinar a complexidade de um algoritmo] é a da medida empírica [(baseada na experiência)]. Podemos pensar em medir experimentalmente a quantidade de trabalho (tempo ou memória) requerida por um algoritmo executado em um computador específico. Entretanto, uma medida empírica é fortemente dependente, tanto do programa quanto da máquina usada para implementar o algoritmo. [...] Apesar de a comparação de programas reais, rodando em computadores reais, ser uma fonte de informação importante, os resultados são inevitavelmente afetados pela programação e pelas características das máquinas. Uma alternativa útil vem de se fazer uma análise matemática do algoritmo de maneira geral. Essa análise, além de ser independente da implementação, permite, muitas vezes, antecipar o cálculo da complexidade para a fase de projeto do algoritmo [e] [...] é útil para avaliar as dificuldades intrínsecas da resolução computacional de um problema. TOSCANI; VELOSO, 2012, p. 14. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Algoritmo: procura o maior valor de um array A, com N elementos, armazenando-o em M. int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Instruções simples (executadas diretamente pela CPU, ou algo próximo disso): • atribuição de valor a variável; • acesso ao valor de determinado elemento de um array; • comparação entre dois valores; • incremento de um valor; • operações aritméticas (adição, multiplicação etc.). • as instruções possuem o mesmo custo; • comandos condicionais possuem custo zero. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Algoritmo: procura o maior valor de um array A, com N elementos, armazenando-o em M. int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 1 instrução: acessar o valor de a[0] e atribuir a m. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Algoritmo: procura o maior valor de um array A, com N elementos, armazenando-o em M. int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 1 instrução: inicialização do for (i = 0). 1 instrução: comparação i < n, mesmo que o tamanho do array seja zero. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Algoritmo: procura o maior valor de um array A, com N elementos, armazenando-o em M. int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 1 instrução: incremento, ao final do for (i++). 1 instrução: comparação i < n. Custo de execução do loop for = 2n instruções. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Algoritmo: procura o maior valor de um array A, com N elementos, armazenando-o em M. int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 1 instrução 2 instruções 2 instruções * N Custo dominante, ou pior caso = 3 + 2n instruções. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Função matemática do custo de execução deste algoritmo, considerando o array de input e o loop vazio: f(n) = 2n + 3 ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 1 instrução 1 instrução Problema: o if sempre será executado, mas nem sempre o valor de m será alterado. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Antes (loop for vazio), bastava saber o tamanho do array (n) para definir a função de custo: f(n) = 2n + 3. Agora (atribuição de m, conforme o if), deve-se considerar também o conteúdo do array. ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Por exemplo, considere dois arrays de mesmo tamanho: int a1[] = { 1, 2, 3, 4 }; int a2[] = { 4, 3, 2, 1 }; int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } • A1: mais instruções executadas (if sempre verdadeiro). • A2: atribuição nunca executada (if sempre falso). ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO Custo dominante, ou pior caso, de um algoritmo maior número de instruções executadas. = ANÁLISE MATEMÁTICA: CONTAGEM DE INSTRUÇÕES DE UM ALGORITMO int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } Pior caso: array com valores em ordem crescente: int a1[] = { 1, 2, 3, 4 }; 1 instrução 2 instruções 2 instruções * N 1 instrução * N 1 instrução * N Função matemática do custo de execução deste algoritmo (complexidade de tempo), considerando o array de input, no pior caso: f(n) = 3 + 2n + 2n = 4n + 3 ANÁLISE ASSINTÓTICA Para se analisar a complexidade de tempo deste algoritmo, todos os termos da função f(n) = 4n + 3 são necessários? int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } Análise assintótica é um método que descreve o comportamento de limites, considerando entradas de tamanhos grandes o suficiente a ponto de evidenciar a ordem de crescimento do tempo de execução do algoritmo. ANÁLISE ASSINTÓTICA Devemos manter apenas os termos da função que indicam o que ocorre quando o tamanho dos dados de entrada (N) cresce muito. int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } Se um algoritmo for mais rápido que outro para um grande conjunto de dados de entrada, muito provavelmente continuará sendo com um conjunto de dados de entrada menor. ANÁLISE ASSINTÓTICA int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 1 instrução 2 instruções 2 instruções * N 1 instrução * N 1 instrução * N Devemos descartar os termos que crescem lentamente e manter apenas os que crescem rápido, à medida que o tamanho de N aumenta. f(n) = 4n + 3 ANÁLISE ASSINTÓTICA int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 2 instruções * N 1 instrução * N 1 instrução * N Devemos descartar também as constantes que multiplicam o termo N, porque são custos que variam conforme a linguagem. Por exemplo: f(n) = 4n // Pascal. m := a[i]; // C. if((i > = 0) && (i < n)) m = a[i]; = • Pascal: 3 instruções (com etapa de verificação); • C: 1 instrução (semetapa de verificação). ANÁLISE ASSINTÓTICA f(n) = n Comportamento assintótico, comportamento de limite da função f(n), quando N tende ao infinito. O termo que possui maior expoente domina o comportamento de f(n), quando N tende ao infinito. ANÁLISE ASSINTÓTICA g(n) = 1000n + 500 h(n) = n2 + n + 1 Apesar de a funApesar de a funçção g(n) possuir constantes maiores ão g(n) possuir constantes maiores multiplicando seus termos, h(n) possui um valor de N multiplicando seus termos, h(n) possui um valor de N sempre maior, tornando os demais termos e sempre maior, tornando os demais termos e constantes pouco importantes.constantes pouco importantes. ANÁLISE ASSINTÓTICA g(n) = n h(n) = n2 Descartando os termos menos importantes das funções e mantendo apenas os termos de maior grau, temos: ANÁLISE ASSINTÓTICA Exemplos de funções de custo, com seu comportamento assintótico: * Se a função não possuir nenhum termo multiplicado por N, seu comportamento assintótico será constante (= 1). ANÁLISE ASSINTÓTICA Obtemos a função de custo de um programa simples apenas contando os loopings aninhados. Ou seja: • algoritmos sem looping: f(n) = 1 (número constante de instruções, exceto em caso de recursão); • algoritmos com loopings de 1 a N: f(n) = n (um conjunto de instruções constante antes e/ou depois do looping e um conjunto de instruções constante dentro do looping); • algoritmos com dois loopings aninhados: f(n) = n2; e assim por diante. NOTAÇÃO O A notação assintótica O (letra grega ômicron) é uma representação matemática que descreve a complexidade de tempo de um algoritmo no pior caso, definindo o limite superior do tamanho da entrada. Ou seja, o comportamento do algoritmo não pode ultrapassar o limite definido. NOTAÇÃO O int m = a[0]; for(int i = 0; i < n; i++) { if(a[i] >= m) { m = a[i]; } } 1 instrução 2 instruções 2 instruções * N 1 instrução * N 1 instrução * N Função matemática da complexidade de tempo deste algoritmo, no pior caso: f(n) = 3 + 2n + 2n = 4n + 3 O(g(n)) = n Assintoticamente, no pior caso (notação O): 1. Implemente e analise a seguinte função em C++: int calculaMenor (int a[], int n) { int menor = a[0]; for (int i = 1; i < n; i++) { if (a[i] < menor) { menor = a[i]; } } return menor; } EXERCÍCIOS RECURSIVIDADE Conforme o dicionário Priberam, recursar significa “examinar de novo” (RECURSAR, 2018). Recursão é o processo de definir algo em termos de si mesmo e é, algumas vezes, chamado de definição circular. Assim, pode-se dizer que o conceito de algo recursivo está dentro de si, que por sua vez está dentro de si e assim sucessivamente, infinitamente. O exemplo a seguir define o ancestral de uma pessoa: • Os pais de uma pessoa são seus ancestrais (caso base); • Os pais de qualquer ancestral são também ancestrais da pessoa inicialmente considerada (passo recursivo). (LAUREANO, 2008, p. 60). RECURSIVIDADE Definições como estas são normalmente encontradas na matemática. O grande apelo que o conceito da recursão traz é a possibilidade de dar uma definição finita para um conjunto que pode ser infinito [...]. Um exemplo aritmético: O primeiro número natural é zero. • O sucessor de um número natural é um número natural. Na computação o conceito de recursividade é amplamente utilizado, mas difere da recursividade típica por apresentar uma condição que provoca o fim do ciclo recursivo. Essa condição deve existir, pois, devido às limitações técnicas que o computador apresenta, a recursividade é impedida de continuar eternamente. (LAUREANO, 2008, p. 60). RECURSIVIDADE A função é recursiva se um comando no corpo da função a chama. Para uma linguagem de computador ser recursiva, uma função deve poder chamar a si mesma. (LAUREANO, 2008, p. 61). Possuem suporte a recursividade as linguagens C, C++, Java, Delphi, Visual Basic e muitas outras. RECURSIVIDADE Um exemplo simples [de recursividade] é a função fatorial, que calcula o fatorial de um inteiro. O fatorial de um número N é o produto de todos os números inteiros entre 1 e N. Por exemplo, 3 fatorial (ou 3!) é 1 * 2 * 3 = 6. (LAUREANO, 2008, p. 61). int fatorialI(int n) { int f = 1; for(int i = 1; i <= n; i++) f = f * i; return f; } RECURSIVIDADE int fatorialI(int n) { int f = 1; for(int i = 1; i <= n; i++) f = f * i; return f; } int main() { int numero = 3; cout << "O fatorial de " << numero << " e: " << fatorialI(numero) << endl; return 0; } RECURSIVIDADE Mas multiplicar n pelo produto de todos os inteiros a partir de n-1 até 1 resulta no produto de todos os inteiros de n a 1. Portanto, é possível dizer que fatorial [de]: 0! = 1 1! = 1 * 0! 2! = 2 * 1! 3! = 3 * 2! 4! = 4 * 3! Logo o fatorial de um número também pode ser definido recursivamente (ou por recorrência) através das seguintes regras (representação matemática) [...]: n! = 1, se n = 0 n! = n * (n-1)! , se n > 0 (LAUREANO, 2008, p. 61). RECURSIVIDADE int fatorialR(int n) { // condição de parada. if(n == 0) return 1; int f = fatorialR(n-1) * n; // chamada da função. return f; } int main() { int numero = 3; cout << "O fatorial de " << numero << " e: " << fatorialR(numero) << endl; return 0; } RECURSIVIDADE int fatorialR(int n) { /* ... */ } int main() { int numero; cout<< "Informe o numero: "; cin>> numero; cout<<endl; cout << "O fatorial de " << numero << " e: " << fatorialR(numero) << endl; return 0; } RECURSIVIDADE A operação de fatorial recursiva é um pouco mais complexa. [...] Para calcular o fatorial de 6, [por exemplo,] o computador tem de calcular primeiro o fatorial de 5 e só depois é que faz a multiplicação de 6 pelo resultado (120). Por sua vez, para calcular o fatorial de 5, vai ter de calcular o fatorial de 4. Resumindo, aquilo que acontece internamente é uma expansão seguida de uma contração: (LAUREANO, 2008, p. 62-63). A versão não-recursiva de fatorial deve ser clara. Ela usa um laço que é executado de 1 a n e multiplica progressivamente cada número pelo produto móvel. (LAUREANO, 2008, p. 62). fatorialR(6) 6 * fatorialR(5) 6 * 5 * fatorialR(4) 6 * 5 * 4 * fatorialR(3) 6 * 5 * 4 * 3 * fatorialR(2) 6 * 5 * 4 * 3 * 2 * fatorialR(1) 6 * 5 * 4 * 3 * 2 * 1 6 * 5 * 4 * 3 * 2 6 * 5 * 4 * 6 6 * 5 * 24 6 * 120 720 RECURSIVIDADE Freqüentemente, soluções recursivas são mais fáceis de entender e implementar corretamente que as iterativas correspondentes. Existe uma certa elegância e economia de raciocínio nas soluções recursivas que as tornam mais atraentes. Como disse o cientista de computação (e criador do interpretador GhostScript para a linguagem de descrição de gráficas PostScript) L. Peter Deutsch: “Iterar é humano, usar recursividade é divino”. (HORSTMANN, 2008, p. 513.). RECURSIVIDADE: QUANDO NÃO USAR O uso de algoritmos recursivos torna-se adequado quando o problema a ser resolvido, ou os dados tratados, são definidos em termos recursivos. Mas, definições de natureza recursiva, por si só, não garantem que um algoritmo recursivo seja o melhor caminho para resolver o problema. Quando o loop recursivo é muito grande, isto consome muita memória nas chamadas a diversos níveis de recursão, pois cada chamada recursiva aloca memória para os parâmetros e as variáveis locais e de controle. Em muitos casos, uma solução iterativa gasta menos memória e tem um desempenho mais eficiente do que uma solução recursiva. 1. Leia o artigo sobre recursividade, no website da TI EXPERT (disponível em: <http://www.tiexpert.net/programacao/c/recursividade.php>), e implemente o código da sequência de Fibonacci apresentado. 2. Implemente o algoritmo Euclidiano, descrito abaixo, em linguagem C++: Calcule o máximo divisor comum de dois inteiros não-negativos P e Q do seguinte modo: se Q for 0, a resposta é P, se não, divida P por Q e obtenha o resto R; a resposta será o máximo divisor comum de Q e R. 3. Implemente uma função recursivaque some os elementos de um array de inteiros. 4. Implemente uma função recursiva que, dados dois números inteiros n e p, calcula o valor de np. Considere: a) caso base: x0 = 1; b) passo da recursão: xn = x * xn-1. EXERCÍCIOS MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO Ordenação [(sorting)] é o processo de arranjar um conjunto de informações semelhantes em uma ordem crescente ou decrescente [...]. (LAUREANO, 2008, p. 100). Por exemplo, sua fatura do cartão de crédito apresenta transações por ordem de data - elas provavelmente foram postas assim por um algoritmo de ordenação. (SEDGEWICK; WAYNE, 2011, p. 243). Algoritmo de ordenação em ciência da computação é um algoritmo que coloca os elementos de uma dada sequência em uma certa ordem - em outras palavras, efetua sua ordenação completa ou parcial. As ordens mais usadas são a numérica e a lexicográfica. (LAUREANO, 2008, p. 100). MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO Existem várias razões para se ordenar uma sequência, uma delas é a possibili- dade [de] se acessar seus dados de modo mais eficiente. (LAUREANO, 2008, p. 100). Nos primórdios da computação, a sabedoria comum era que até 30% de todos os ciclos de computação eram gastos com ordenação. Se essa fração é menor hoje, uma razão provável é que os algoritmos de ordenação se torna- ram relativamente eficientes, não que a ordenação tenha diminuído em impor- tância relativa. De fato, a ubiquidade do uso do computador nos inundou em dados e o primeiro passo para a organização dos dados é, muitas vezes, ordená-los [...]. (SEDGEWICK; WAYNE, 2011, p. 243). MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO [...] [O MergeSort] é um algoritmo de ordenação por intercala- ção ou segmentação[, baseado no paradigma divisão e conquista (divide-and-conquer)]. A idéia básica é a facilidade de criar uma seqüência ordenada a partir de duas outras também ordenadas. Para isso, o algoritmo divide a seqüência original em pares de dados, ordena-as; depois as agrupa em sequências de quatro elementos, e assim por diante, até ter toda a seqüência dividida em apenas duas partes. (LAUREANO, 2008, p. 115). MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO Os passos do algoritmo MergeSort são: 1. divisão: divide uma sequência em duas novas; 2. recursão: ordena recursivamente cada uma das sequências (dividindo novamente, se possível); 3. conquista: mescla (merge) as subsequências, para obter o resultado final. (GOODRICH; TAMASSIA, 2007, p. 432.; LAUREANO, 2008, p. 115). (MERGE SORT, 2018.). MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO Os passos do algoritmo MergeSort são: 1. divisão: divide uma sequência em duas novas; 2. recursão: ordena recursivamente cada uma das sequências (dividindo novamente, se possível); 3. conquista: mescla (merge) as subsequências, para obter o resultado final. (GOODRICH; TAMASSIA, 2007, p. 432.; LAUREANO, 2008, p. 115). (MERGE SORT, 2018.). MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO 5 2 9 0 1 2 5 2 9 0 1 2 5 2 0 1 2 5 0 1 2 0 5 1 9 2 MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO Simulação do funcionamento do algoritmo MergeSort: <http://math.hws.edu/eck/js/sorting/xSortLab.html>. MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO Merge Sort in 3 minutes: <https://www.youtube.com/watch?v=4VqmGXwpLqc>. MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO void mergeSort(int vetor[], int indiceEsquerdo, int indiceDireito) { if(indiceEsquerdo < indiceDireito) { int indiceMeio = ((indiceEsquerdo + indiceDireito) /2); mergeSort(vetor, indiceEsquerdo, indiceMeio); mergeSort(vetor, (indiceMeio +1), indiceDireito); merge(vetor, indiceEsquerdo, indiceMeio, indiceDireito); } } MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO void merge(int vetor[], int indiceEsquerdo, int indiceMeio, int indiceDireito) { int tamanhoEsquerdo = indiceMeio – indiceEsquerdo +1; int vetorEsquerdo[tamanhoEsquerdo]; int tamanhoDireito = indiceDireito – indiceMeio; int vetorDireito[tamanhoDireito]; int i, j; // Carrega os elementos do vetor esquerdo. for(i = 0; i < tamanhoEsquerdo; i++) vetorEsquerdo[i] = vetor[indiceEsquerdo + i]; // Carrega os elementos do vetor direito. for(j = 0; j < tamanhoDireito; j++) vetorDireito[j] = vetor[indiceMeio + 1 + j]; //... MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO //... i = 0; j = 0; int k = indiceEsquerdo; // Faz a ordenação entre os vetores esquerdo e direito, e // carrega o elemento ordenado no vetor principal. while((i < tamanhoEsquerdo) && (j < tamanhoDireito)) { if(vetorEsquerdo[i] <= vetorDireito[j]) { vetor[k] = vetorEsquerdo[i]; i++; } else { vetor[k] = vetorDireito[j]; j++; } k++; } //... MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO //... // Se restarem elementos no vetor esquerdo, carrega-os para o // vetor principal. while(i < tamanhoEsquerdo) { vetor[k] = vetorEsquerdo[i]; i++; k++; } // Se restarem elementos no vetor direito, carrega-os para o // vetor principal. while(j < tamanhoDireito) { vetor[k] = vetorDireito[j]; j++; k++; } } MERGESORT: ALGORITMO DE ORDENAÇÃO POR INTERCALAÇÃO #include <iostream> using std::cout; using std::endl; void imprimeVetor(int vetor[], int tamanho) { for(int i = 0; i < tamanho; i++) cout << " |" << vetor[i] << "| " << endl; } int main() { int n[3] = { 5, 2, 9 }; imprimeVetor(n, 3); cout << endl; mergeSort(n, 0, 2); imprimeVetor(n, 3); return 0; } 1. Dada a sequência de números: 3 4 9 2 5 1 8, ordene-a de modo crescente, utilizando o algoritmo MergeSort. 2. Considerando o programa implementado para responder a questão 1, modifique o algoritmo MergeSort para fazer a ordenação decrescente. 3. No algoritmo MergeSort, o que acontece se trocarmos "(indiceEsquerdo + indiceDireito) / 2" por "(indiceEsquerdo + indiceDireito - 1) / 2"? E o que acontece se trocarmos "(indiceEsquerdo + indiceDireito) / 2" por "(indiceEsquerdo + indiceDireito + 1) / 2"? Implemente e teste essas mudanças. EXERCÍCIOS QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA O QuickSort é um algoritmo de ordenação in-place (in loco, ou seja, trabalhando sobre o próprio vetor), também baseado no paradigma divisão e conquista (divide-and-conquer). (LAUREANO, 2008, p. 111; SEDGEWICK; WAYNE, 2011, p. 288). QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA O quicksort [...] funciona particionando um array em dois subarrays e, então, ordenando-os independentemente. O quicksort é complementar ao mergesort: no mergesort, nós quebramos o array em dois subarrays a serem ordenados e, então, os combinamos para formar um array ordenado inteiro; no no quicksortquicksort, n, nóós s reorganizamos o reorganizamos o arrayarray de tal modo que, quando os dois de tal modo que, quando os dois subarrayssubarrays estiverem ordenados, o estiverem ordenados, o arrayarray inteiro tambinteiro tambéém estarm estaráá ordenadoordenado. No caso do mergesort, nós fazemos duas chamadas recursivas, antes de manipular o array inteiro; no caso do no caso do quicksortquicksort, fazemos as chamadas recursivas , fazemos as chamadas recursivas depois de manipular o depois de manipular o arrayarray inteirointeiro. No mergesort, o array é dividido no meio; no no quicksortquicksort, a posi, a posiçção da quebra depende do conteão da quebra depende do conteúúdo do do do arrayarray. (SEDGEWICK; WAYNE, 2011, p. 288). QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA No quicksort, nós não buscamos mais o valor médio [(como no mergesort)], em vez disso, selecionamos um elemento, conforme uma estratégia (às vezes, aleatoriamente, às vezes, o da extrema esquerda, às vezes, o do meio), para particionar um array em subarrays. (HEINEMAN; POLLICE; SELKOW, 2009, p. 85). QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA Os passos do QuickSort são: 1. Divisão: selecione um elemento da sequência (S), chamado de pivô (P). Retire P de S e particione os demais elementos em duas subsequências distintas, L e G, contendo,respectivamente, os elementos <= a P e os elementos >= a P; 2. Recursão: ordene as subsequências L e G, recursivamente. 3. Conquista: coloque de volta os elementos de S em ordem, inserindo: os elementos de L, o P e os elementos de G. (GOODRICH; TAMASSIA, 2007, p. 442-443; LAUREANO, 2008, p. 111-112). (QUICKSORT, 2018.). QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA Simulação do funcionamento do algoritmo QuickSort: <http://math.hws.edu/eck/js/sorting/xSortLab.html>. QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA Método de Ordenação - QuickSort: <https://www.youtube.com/watch?v=oYmHH1-f_L0>. QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA void quickSort(int vetor[], int indiceEsquerdo, int indiceDireito) { int limiteEsquerdo = indiceEsquerdo; int limiteDireito = indiceDireito; int meio = ((limiteEsquerdo + limiteDireito) /2); int pivo = vetor[meio]; // ... QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA while(limiteDireito > limiteEsquerdo) { // Se conteúdo vetor no limite esquerdo < pivô, // não faz nada, porque ele está no lugar certo. while(vetor[limiteEsquerdo] < pivo) limiteEsquerdo++; // Incrementa p/ continuar verificação. // Se conteúdo vetor de limite direito > pivô, // não faz nada, porque ele está no lugar certo. while(vetor[limiteDireito] > pivo) limiteDireito--; // Decrementa p/ continuar verificação. if(limiteEsquerdo <= limiteDireito) // Se se cruzarem... { int temp = vetor[limiteEsquerdo]; // Realiza inversão. vetor[limiteEsquerdo] = vetor[limiteDireito]; vetor[limiteDireito] = temp; limiteEsquerdo++; // Reajusta os limites. limiteDireito--; } } QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA // Utiliza recursão p/ continuar ordenando // (atualiza a faixa que está sendo analisada). if(indiceEsquerdo < limiteDireito) quickSort(vetor, indiceEsquerdo, limiteDireito); if(limiteEsquerdo < indiceDireito) quickSort(vetor, limiteEsquerdo, indiceDireito); } QUICKSORT: ALGORITMO DE ORDENAÇÃO RÁPIDA #include <iostream> using std::cout; using std::endl; void imprimeVetor(int vetor[], int tamanho) { for(int i = 0; i < tamanho; i++) cout << " |" << vetor[i] << "| " << endl; } int main() { int n[3] = { 5, 2, 9 }; imprimeVetor(n, 3); cout << endl; quickSort(n, 0, 2); imprimeVetor(n, 3); return 0; } 1. Dada a sequência de números: 3 4 9 2 5 1 8, ordene-a de modo crescente, utilizando o algoritmo QuickSort. EXERCÍCIOS LINEAR Lista Linear RECAPITULAÇÃO: TIPOS DE ESTRUTURAS DE DADOS SEQUENCIAL ENCADEADO NÃO-LINEAR SEQUENCIAL Adaptado de BALIEIRO, 2015, p. 19. nó RECAPITULAÇÃO: TIPOS DE ESTRUTURAS DE DADOS ENCADEADO Adaptado de BALIEIRO, 2015, p. 136. SIMPLESMENTE ENCADEADA RECAPITULAÇÃO: TIPOS DE ESTRUTURAS DE DADOS RECAPITULAÇÃO: TIPOS DE ESTRUTURAS DE DADOS // EXEMPLO DE LISTA LINEAR // SIMPLESMENTE ENCADEADA. struct No { int info; struct No* proximo; }; No* cabeca = new No; cabeca->proximo = NULL; No* primeiro = new No; primeiro->info = 11; primeiro->proximo = NULL; cabeca->proximo = primeiro; No* segundo = new No; segundo->info = 12; segundo->proximoNo = NULL; primeiro->proximo = segundo; No* noDeTeste; noDeTeste = cabeca->proximo; int indice = 1; while(noDeTeste != NULL) { cout << "No numero: " << indice; cout << ", info: " << noDeTeste->info << endl; noDeTeste = noDeTeste->proximo; indice++; } Adaptado de BALIEIRO, 2015, p. 137-138. LINEAR RECAPITULAÇÃO: TIPOS DE ESTRUTURAS DE DADOS SEQUENCIAL ENCADEADO NÃO-LINEAR ÁRVORES As estruturas de dados do tipo ÁRVORE são não lineares, ou seja, os elementos que as compõem não estão armazenados de forma sequencial [ou encadeada] [...]. (ASCENCIO; ARAÚJO, 2010, p. 278). ÁRVORES Esquemas em árvores são utilizados para representar estruturas hierárquicas (árvores genealógicas, campeonatos de futebol ou organizações). Na ciência da computação, as árvores podem ser utilizadas para representar decisões, definições formais de linguagem ou mesmo [...] hierarquia entre elementos. [...] [Este tipo de estrutura de dados] herda as características das topologias em árvore[,] onde os dados estão dispostos de forma hierárquica ([ou seja,] um conjunto de dados é hierarquicamente subordinado a outro). (LAUREANO, 2008, p. 125). Árvore genealógica de Abraão, conforme Gênesis (GOODRICH; TAMASSIA, 2007, p. 246). ÁRVORES Uma árvore é composta por um elemento principal[,] chamado raiz, que possui ligações para outros elementos, [...] denominados galhos ou filhos. Estes galhos levam a outros elementos que também possuem outros galhos. O elemento que não possui galhos é conhecido como folha ou nó terminal. (LAUREANO, 2008, p. 125-126). ÁRVORES (BARANAUSKAS, 2007, slide 10). ÁRVORES (BARANAUSKAS, 2007, slide 11). (galhos) ÁRVORES (BARANAUSKAS, 2007, slide 12). ÁRVORES (BARANAUSKAS, 2007, slide 13-14). ÁRVORES (BARANAUSKAS, 2007, slide 15). ÁRVORES (BARANAUSKAS, 2007, slide 16). ÁRVORES (BARANAUSKAS, 2007, slide 19). ÁRVORES (BARANAUSKAS, 2007, slide 20). ÁRVORES (BARANAUSKAS, 2007, slide 29). ÁRVORES (BARANAUSKAS, 2007, slide 26). ÁRVORES (BARANAUSKAS, 2007, slide 41). ÁRVORES BINÁRIAS Árvore binária é uma estrutura de dados do tipo árvore, com grau 2. Uma árvore binária é um conjunto finito de elementos[, que] [...] pode estar vazio ou ser particionado em três subconjuntos distintos, sendo eles: [nó raiz, sub-árvore direita e sub-árvore esquerda] [...]. (ASCENCIO; ARAÚJO, 2010, p. 278). Árvore binária, representando a expressão: [a + (b / c)] * [d – (e * f)]. ÁRVORES BINÁRIAS As árvores binárias podem ser ilustradas de três formas distintas, conforme [abaixo] [...]. (ASCENCIO; ARAÚJO, 2010, p. 279). ÁRVORES BINÁRIAS Toda árvore binária possui as seguintes propriedades: a) Todos os nós de uma sub-árvore direita são maiores que o nó raiz. b) Todos os nós de uma sub-árvore esquerda são menores que o nó raiz. c) Cada sub-árvore é também uma árvore binária. d) O grau de um nó representa o seu número de sub-árvores [...]. e) Em uma árvore binária, o grau máximo de um nó é 2. (ASCENCIO; ARAÚJO, 2010, p. 279-280). ÁRVORES BINÁRIAS h) Nó pai [...]: nó acima e com ligação direta a outro nó. i) Nó filho [...]: nó abaixo e com ligação direta a outro nó. São raízes das sub-árvores. j) Nós irmãos [...]: são os nós que possuem o mesmo nó pai. k) Nó folha ou terminal [...]: nó que não possui filhos. (ASCENCIO; ARAÚJO, 2010, p. 280). ÁRVORES BINÁRIAS s) Árvore estritamente binária [...]: árvore em que todos os nós têm 0 ou 2 filhos. t) Expressão que representa o número de nós de uma árvore estritamente binária = 2n - 1, onde n é o número de nós folha [...]. (ASCENCIO; ARAÚJO, 2010, p. 283). ÁRVORES BINÁRIAS u) Árvore completa [...]: árvore em que todos os nós com menos de dois filhos ficam no último e no penúltimo nível. (ASCENCIO; ARAÚJO, 2010, p. 284). v) Árvore cheia [...]: árvore estritamente binária e completa. (ASCENCIO; ARAÚJO, 2010, p. 284). 1. Represente uma árvore binária, conforme a expressão aritmética: {[(3 + 6) * (4 - 1)] + 5}. Observação: os nós-folha representam os operandos e os nós internos, os operadores. 2. Para a árvore binária abaixo, responda: a) Qual o grau desta árvore? b) Quantos níveis possui essa árvore? c) Quantos nós e arestas possui esta árvore? d) Quantos nós não-folha possui esta árvore? e) Informe o grau de cada um dos nós. f) Indique os nós-folha. EXERCÍCIOS ÁRVORES BINÁRIAS DE BUSCA Uma árvore de busca binária (Binary search tree) é uma árvore binária onde a informação que o nó esquerdo possui é menor ou igual à informação da chave [e] [...] a informação que o nó direito possui é maior ou igual à informação da chave. O objetivo de organizar dados em árvores de busca binária é facilitar a tarefa de procura de um determinado valor. A partir da raiz e de posse da informação a ser encontrada, é possível saber qual o caminho (galho) a ser percorrido até encontrar o nó desejado. Para tanto, basta verificar se o valorprocurado é maior, menor ou igual ao nó que se está posicionando. (LAUREANO, 2008, p. 129). ÁRVORES BINÁRIAS DE BUSCA Deve-se observar que não existe uma única forma de organizar um conjunto de informações em uma árvore de busca binária, ainal, dependendo da escolha do nó raiz, obtêm-se árvores diferentes. Na figura [abaixo,] [...] os valores ({ 2, 3, 5, 5, 7, 8}) são organizados em árvores de busca de duas maneiras diferentes. (LAUREANO, 2008, p. 129). ÁRVORES BINÁRIAS DE BUSCA As duas árvores contêm exatamente os mesmos valores, porém possuem estruturas diferentes. Enquanto a árvore A está enraizada em um dos nós de valor 5, a árvore B está enraizada no nó de valor 2. Supondo que se está buscando o valor 8 nas árvores, as comparações seriam como se segue na tabela [ao lado] [...]. (LAUREANO, 2008, p. 130). ÁRVORES BINÁRIAS DE BUSCA Na árvore A, são realizadas menos comparações em relação à utilizada na árvore B. O melhor caso irá ocorrer quando a árvore estiver cheia, neste caso a procura de um item terá tempo proporcional a log n, enquanto o pior caso ocorrerá quando todos os nós da árvores apontarem somente para um dos lados, caso em que o tempo de processamento da procura será proporcional a n. (LAUREANO, 2008, p. 130). IMPLEMENTANDO ÁRVORES BINÁRIAS // Cada nó da árvore binária. struct No { int num; No* esq; No* dir; } void main() { No* raiz = NULL; No* novo = new No(); novo->num = 11; novo->dir = NULL; novo->esq = NULL; raiz = novo; } ASCENCIO; ARAÚJO, 2010, p. 303-305. PERCURSO EM ÁRVORE Existem três ordens para se percorrer uma árvore: • Pré-ordem (preorder): r, e, d; • Em ordem (inorder): e, r, d; • Pós-ordem (postorder): e, d, r PERCURSO EM ÁRVORE INSERÇÃO & REMOÇÃO Vide “Estácio Algoritmos Avançados Aula10.txt”. 1. Explique, por meio de desenho, como funciona o percurso em árvores. EXERCÍCIOS ÁRVORES AVL A árvore AVL, criada em 1962 por Adelson-Velsky e Landis, é uma árvore binária balanceada, ou seja, é uma árvore que obedece a todas as propriedades da árvore binária e em que cada nó apresenta diferença de altura entre as sub-árvores direita e esquerda de 1, 0 ou -1, como ilustra a [figura abaixo] [...]. (ASCENCIO; ARAÚJO, 2010, p. 329). GRAFOS Um grafo G é formado pelo par de conjuntos V [(vértices/nós)] e [A (arestas)] [...]. Dois vértices ligados por uma aresta são denominados adjacentes [...]. A representação geométrica dos grafos é feita marcando pontos distintos no plano para representar os vértices, e uma linha ligando dois pontos para representar a aresta. Três exemplos de grafos são apresentados na [figura abaixo] [...]. Os vértices podem ou não possuir nomes. Quando recebem nomes, estes podem ser números ou letras. Quando são nomeados por letras, é necessário fazer um mapeamento de cada nome de vértice para um número correspondente [...]. (ASCENCIO; ARAÚJO, 2010, p. 368). GRAFOS Laços, arestas múltiplas e multigrafo É possível encontrar em um grafo arestas [...] com as extremidades [...] iguais. Quando isso ocorre, a aresta é denominada laço. É também possível a existência de mais de uma aresta entre o mesmo par de vérticas, chamadas de arestas paralelas ou arestas múltiplas. Um grafo que possui arestas paralelas é denominado multigrafo. Na Figura 8.2(a) é apresetnado um grafo com laço, e nas Figura 8.2(b) é apresentado um multigrafo. (ASCENCIO; ARAÚJO, 2010, p. 369). GRAFOS Grafo trivial e grafo simples Um grafo com apenas um vértice é chamado de grafo trivial. Um grafo G é chamado grafo simples se não possui laço ou aresta múltipla. Os grafos da Figura 8.2 não são grafos simples, já os grafos da Figura 8.1 são. (ASCENCIO; ARAÚJO, 2010, p. 369). GRAFOS Grafo completo Um grafo G é chamado grafo completo quando existe uma aresta para cada par de vérticas distintos de G. Na Figura 8.3 são apresentados exemplos de grafos completos com 3, 4 e 5 vértices, respectivamente. (ASCENCIO; ARAÚJO, 2010, p. 369). GRAFOS Um grafo G(V,A) com |V| = n pode ser representado adequadamente por meio de matrizes ou listas. [...] Dado um grafo G(V,A), uma matriz de adjacências M é formada por n linhas e n colunas, sendo n o número de vértices do grafo. A matriz é preenchida da seguinte forma: A Figura 8.14 apresenta um grafo não direcionado e sua matriz de adjacências. (ASCENCIO; ARAÚJO, 2010, p. 375). ASCENCIO, Ana Fernanda G.; ARAÚJO, Graziela S. Estruturas de Dados: Algoritmos, Análise de Complexidade e Implementações em Java e C++. São Paulo: Pearson Prentice Hall, 2010. BALIEIRO, Ricardo. Estrutura de Dados. Rio de Janeiro: SESES, 2015. BARANAUSKAS, José Augusto. Árvores. Universidade de São Paulo, Departamento de Física e Matemática, 2007. 74 slides: color. Disponível em: <http://dcm.ffclrp.usp.br/~augusto/teaching/aedi/AED-I-Arvores.pdf>. Acesso em: 18 set. 2018. GOODRICH, Michael T.; TAMASSIA, Roberto. Estruturas de Dados e Algoritmos em Java. Tradução Bernardo Copstein, Leandro Bennto Pompermeier. 4. ed. Porto Alegre: Bookman, 2007. HEINEMAN, George T.; POLLICE, Gary; SELKOW, Stanley. Algorithms in a Nutshell. Sebastopol: O’Reilly, 2009. HORSTMANN, Cay. Conceitos de Computação com o Essencial de C++. Tradução Carlos Arthur Lang Lisbôa e Maria Lúcia Blanck Lisbôa. 3. ed. São Paulo: Bookman, 2008. REFERÊNCIA BIBLIOGRÁFICA LAUREANO, Marcos. Estrutura de Dados com Algoritmos e C. Rio de Janeiro: Brasport, 2008. MERGE SORT. Wikipédia. Disponível em: <https://pt.wikipedia.org/wiki/Merge_sort>. Acesso em: 03 set. 2018. QUICKSORT. Wikipédia. Disponível em: <https://pt.wikipedia.org/wiki/Quicksort>. Acesso em: 11 set. 2018. RECURSAR. Dicionário Priberam da Língua Portuguesa online, 2008-2013. Disponível em: <https://www.priberam.pt/dlpo/recursar>. Acesso em: 28 ago. 2018. SEDGEWICK, Robert; WAYNE, Kevin. Algorithms. 4th ed. Boston: Addison-Wesley, 2011. TOAL, Ray. Algorithm Analysis. Notes. Loyola Marymount University, College of Science and Engineering, Computer Science Division. Disponível em: <http://cs.lmu.edu/~ray/notes/alganalysis/>. Acesso em: 15 ago. 2018a. TOSCANI, Laira V.; VELOSO, Paulo A. S. Complexidade de Algoritmos: Análise, Projeto e Métodos. 3. ed. Porto Alegre: Bookman, 2012. 13 v. REFERÊNCIA BIBLIOGRÁFICA VRAJITORU, Dana; KNIGHT, William. Practical Analysis of Algorithms. [S.l.]: Springer, 2014. REFERÊNCIA BIBLIOGRÁFICA
Compartilhar