Baixe o app para aproveitar ainda mais
Prévia do material em texto
Kesede Rodrigues Julio Suzete Freitas da Silva Apostila de Estrutura de Dados em C versão: 20080909 Sumário 1 Introdução......................................................................................................................................3 1.1 Construindo Estruturas Heterogêneas....................................................................................3 2 Recursão........................................................................................................................................4 2.1 Introdução...............................................................................................................................4 2.2 – Exercícios............................................................................................................................7 3 Ponteiro e Alocação dinâmica.......................................................................................................8 3.1 Vetores de Ponteiros.............................................................................................................10 3.2 Matrizes de Ponteiro.............................................................................................................11 1. Indireção Simples...............................................................................................................11 2. Indireção Múltipla..............................................................................................................12 3.3 Exercícios.............................................................................................................................14 4 Listas............................................................................................................................................15 4.1 Listas Através de Vetores.....................................................................................................15 4.2 Listas através de Ponteiros...................................................................................................18 4.3 Exercícios.............................................................................................................................19 5 Pilha.............................................................................................................................................20 5.1 Pilha através de Vetores.......................................................................................................20 5.2 Pilha através de Ponteiros.....................................................................................................23 5.3 Exercícios.............................................................................................................................23 6 Fila...............................................................................................................................................25 6.1 Fila através de Vetores.........................................................................................................25 6.2 Fila através de Ponteiros.......................................................................................................27 6.3 Exercícios.............................................................................................................................28 7 Métodos de Classificação............................................................................................................29 7.1 Seleção Estrita......................................................................................................................29 7.2 Troca - Bolha........................................................................................................................29 7.3 Troca – Quick Sort...............................................................................................................30 7.4 Intercalação...........................................................................................................................32 7.5 Inserção – Direta...................................................................................................................33 7.6 Inserção - Shell.....................................................................................................................34 7.7 Distribuição – classificação por caixas.................................................................................35 7.8 Distribuição – classificação por radicais (raízes, Radix)......................................................36 7.9 Cálculo de Endereço (hashing).............................................................................................37 8 Métodos de Busca........................................................................................................................39 8.1 Definição..............................................................................................................................39 8.2 Método de Busca Linear ou Seqüencial...............................................................................39 8.3 Método de Busca Binária.....................................................................................................39 8.4 Método de Busca Hashing....................................................................................................40 9 Grafo............................................................................................................................................41 9.1 Definições.............................................................................................................................41 10 Árvores......................................................................................................................................43 10.1 Definições...........................................................................................................................43 10.2 Árvores Binárias.................................................................................................................44 10.3 Percursos em Árvores Binárias...........................................................................................46 10.4 Árvores Binárias de Busca (ABB)......................................................................................47 10.5 Exercícios...........................................................................................................................53 1 Introdução Nesta apostila, discutiremos algumas das muitas formas de alocar informações na memória, a fim de facilitar pesquisas. Cada aplicação tem sua estrutura mais apropriada e portanto é responsabilidade do programador entender o funcionamento de cada uma delas para melhor aplicá-las. Para melhor trabalhar com estas estruturas devemos construir “Tipos Abstratos de Dados”, que nada mais são do que uma generalização dos “Tipos Simples de Dados”, estes podem ser entendidos como a menor parte tratável que um componente na memória (Ex.: int, float, char etc). Os TAD's são, portanto, agregações destes tipos juntamente com suas funcionalidades, ou seja, operações que podem ser executadas sobre eles. 1.1 Construindo Estruturas Heterogêneas Antes de começarmos a falar de TAD's, precisamos saber como trabalhar com estruturas na memória. Em uma estrutura simples de vetor/matriz não temos a possibilidade de alocar valores de diferentes tipos, uma vez que declaramos apenas uma variável para ser o vetor/matriz, e em C, cada variável está vinculada a um único tipo. Para podermos alocar estruturas com valores de tipos diferentes precisamos de uma estrutura de variável, que nada mais é que o agrupamento destas variáveis. Definição: Permite vincular, na memória, variáveis de tipos diferentes. Sintaxe: struct <nome da estrutura>{ <tipo> <variável>; <tipo> <variável>; .}; Temos a opção de deslocarmos também o nome da estrutura para o final. Desta forma, retiraríamos o nome de onde ele se encontra e colocaríamos após o “}”e o “;”. Podemos também declarar a nossa estrutura com um tipo, assim colocamos a palavra “typedef” antes da palavra “struct” e tratamos, a partir daí, nossa estrutura como um tipo. Para utilizarmos uma estrutura em nossos programas, devemos declarar uma variável do tipo estrutura. Assim, a linha de definição abaixo nos permite usar a estrutura em nosso programa através do nome da variável. <nome da estrutura> <variável>; Exemplo : struct{ int ra; char nome[30]; float nota; }aluno; aluno alu; Algumas considerações devem ser relevadas, assim como: 1. Variável estrutura pode receber, de uma só vez, todo o conteúdo de outra variável estrutura. 2. Uma estrutura pode conter outras estruturas, ou seja, podemos ter estruturas aninhadas. 3. Podem ser passadas como parâmetro, por valor ou por referência. 4. Variável do tipo estrutura pode ser retornada de funções. 5. Podemos declarar matrizes do tipo estrutura. 2 Recursão 2.1 Introdução A técnica de recursão é aplicável quando a solução de um problema envolve sub-soluções de mesmo procedimento, ou seja, a aplicação da mesma lógica repetidamente para cada parte do problema, resolve o problema como um todo. Um procedimento recursivo terá pelo menos, dois passos fundamentais: 1. Um passo onde o resultado é imediatamente conhecido 2. Outro passo onde teremos a chamada do mesmo procedimento. Na prática, em C, teremos uma função chamando ela mesma, mudando apenas os valores dos parâmetros. Um caso clássico é o calculo do fatorial, onde o fatorial de um numero será a multiplicação dele pelo fatorial do seu antecessor. Para resolver o fatorial de 1, não precisaremos da recursão, no entanto, valores de fatorial acima de 1 sempre precisará de, pelo menos, uma chamada à própria função. Para resolvermos o fatorial de 5 temos que, primeiro, resolver o fatorial de 4. Para resolvermos o fatorial de 4, temos que, primeiro resolver o fatorial de 3, e assim por diante. As chamadas recursivas sempre são chamadas com argumentos diferentes, caso contrário, teremos chamadas recursivas infinitas. Digite o Exemplo 2.1:- Faca um programa que receba um numero qualquer do usuário, e que contenha uma função que calcule e retorne o fatorial deste numero, recebido como parâmetro pela função. Mostre o resultado para o usuário. 1 #include <stdio.h> Permite inclusão de funções de i/o 2 #include <conio.h> Permite inclusão da função getch() 3 int fat(int valor); Declara o protótipo da função 4 int main(){ Abre função principal 5 int val; Declara variável val 6 scanf(“%d”,&val); Recebe o valor de val do usuário 7 printf("Fatorial = " %d”, fat(val)); Chama a função fat() e imprime o seu retorno. O valor de “val” é enviado à função fat() como parâmetro 8 getch(); Pára a execução do programa 9 } Fecha a função principal 10 int fat(int valor){ Abre a função fat() e recebe o valor enviado “val” (linha 7) na variável “valor” 11 if (valor==1){ Verifica se o “valor” é 1 12 return 1; Caso seja, retorna 1 para a função chamadora 13 } Fecha o if 14 else{ Verifica se “valor” é diferente de 1 15 return valor * fat(valor-1); Caso seja, chama novamente a função fat(), envia o resultado da subtração (valor – 1). A multiplicação de fat() por “valor” só será realizada quando o if da função for verdadeiro. Isto é melhor explicado no DE (Diagrama de Execução) 16 } Fecha o else 17 } Fecha a função fat() Portanto, uma solução recursiva consiste na resolução sucessiva de sub-problemas menores até chegarmos ao problema mais simples, o qual tem solução imediata. Uma forma de mostrar a execução do programa passo-a-passo, principalmente quando temos função envolvida na solução do problema, é através do DE - “Diagrama de Execução”. Este diagrama permite fazer uma “teste-de-mesa” no programa mostrando os retorno de cada função envolvida na solução. Vamos demostrar o diagrama utilizando o Exemplo 2.1 que resolve o fatorial de um número digitado pelo usuário. Cada retângulo refere-se a chamada de uma função, sendo que o primeiro refere-se à função “main()”. Nele nós declaramos as variáveis, seus valores e executamos o programa até que cheguemos na função printf(). Aqui temos uma chamada da função “fat()”. Supondo que o valor digitado em “val” seja “5” (val=5), desenhamos outro retângulo, interno ao primeiro, referente a chamada da função “fat()”. Aqui declaramos as variáveis locais, neste caso apenas “valor”, e atribuímos à ela o valor enviado “5”, como supomos. Neste caso então, o if é negado, pois valor é igual a 5 e não igual a 1. Entramos, portanto no else e encontramos um return com a chamada da função fat(), só que agora com o parâmetro valor-1. Repare que o argumento agora vale “4” (5-1), pois isto foi resultado de uma subtração. Acompanhamos novamente a execução da função (o if é negado e entramos no else) e novamente chegamos a chamada recursiva de “fat()”. Desenhamos outro retângulo referente a nova chamada e definimos o novo valor do parâmetro. Seguimos desta forma até chegarmos no momento em que o argumento (“valor”) valerá 1. Neste caso, a chamada recursiva não acontece, pois a condição do “if” será verdadeira. Logo, o retorno será 1. Este valor é retornado para a última chamada recursiva (return 2 * fat(1)) e então a multiplicação é executada (“2 * 1”). O produto é retornado para a chamada recursiva anterior e então é executada a multiplicação (“3 * 2”). Isto se repete até que o último resultado (5 * fat(4)) seja retornado para a main(), e assim temos o valor do fatorial de 5, pronto para ser impresso (120). Um bom exercício é mudar o valor para 7 e refazer DE (o resultado terá que ser 5040). val=5 fat(5) 120 valor=5 5*fat(4) 24 valor=4 4*fat(3) 6 valor=3 3*fat(2) 2 valor=2 2*fat(1) 1 valor=1 2.2 – Exercícios 1. Efetue, recursivamente, a soma dos 10 primeiros números inteiros. Faça o diagrama de execução. 2. Mostre, recursivamente, os números de 1 a 10. Faça o diagrama de execução. 3. Mostre, recursivamente, os números de 10 a 1. Faça o diagrama de execução. 4. Conte, recursivamente, quantos caracteres existem em um string, sabendo que um string finaliza com ’\0´. A string será digitada pelo usuário. Faça o diagrama de execução. 5. Mostre, na tela, os 10 primeiros termos da seqüência de fibonacci, recursivamente. A regra da seqüência diz que um termo é sempre resultado da soma dos dois anteriores, e os dois primeiros termos são iguais a 1 (um). 3 Ponteiro e Alocação dinâmica São variáveis que permitem a alocação de endereços de outras variáveis. Uma variável ponteiro não contem um valor comum (tipo int, char, float etc), apenas aponta para ela, ou seja, guarda o endereço da variável que contém o valor. Exemplo.: Digite o Exemplo 3.1 :- Faça um programa que declare uma variável inteira (incializada com 10) e através de um ponteiro, imprima seu conteúdo. 1 int main(){ Abre main() 2 int a=10; Declara variavel inteira 3 int *pa; Declara variavel ponteiro para int 4 pa=&a; Atribui endereço de “a” para a vairavel ponteiro “pa”, logo, “pa” passa a ser uma referencia de “a” 5 printf(“Conteudo de a = %d”, *pa); Imprime o conteudo de “a” através de “pa” 6 system(“pause”); Pára o programa 7 } Fecha main() Neste caso, “*pa” é o mesmo que “a”, pois pa aponta para a. Operadores de ponteiros: & :- associado a uma variável devolve seu endereço. * :- associado a uma variável ponteiro devolve o conteúdo da variável que está sendo apontada. Quando atribuímos um ponteiro para outro, estamos atribuindo o endereço nele contido. Para fazer com que o ponteiro aponte para a próxima posição devemos somar 1 na variável ponteiro, isto faz com que seja acrescido uma unidade de tipo ao valor do endereço. Exemplos: Digite o Exemplo 3.2: A função deste programa é para avaliação das posições e conteúdos da memória em função de modificações feitas pelo programa. Faça um minucioso estudo do resultado. 1 #include <stdio.h> #include<stdlib.h> Inclui protótipos 2 int main(){ Abre main() 3 int x=5,y=6; Declara variaveis inteiras 4 int *px, *py; Declara variaveis ponteiro para int 5 px=&x; Atribui endereço de “x” para variavel ponteiro “px”. Portanto, “px” passa a apontar para “x” 6 py=&y; Atribui endereço de “y” para variavel ponteiro “py”. Portanto, “py” passa a apontar para “y” 7 if (px<py){ printf("py-px = %u\n",py-px); } else{ printf("px-py = %u\n", px-py); } Verifica se o conteudo (endereço de “x”) de “px” é menor que o conteúdo (endereço de “y”)de “py”. Imprime a subtração do maior pelo menor 8 printf("px = %u, *px = %d, &px = %u\n", px,*px,&px); Imprime: conteúdo de “px” (endereço de “x”), o conteudo de onde “px” aponta (conteúdo de “x”) e o endereço da variável ponteiro “px” 9 printf("py = %u, *py = %d, &py = %u\n", py,*py,&py); Imprime: conteúdo de “py” (endereço de “y”), o conteudo de onde “py” aponta (conteúdo de “y”) e o endereço da variável ponteiro “py” 10 px++; Soma uma unidade de inteiro na variavel ponteiro “px”. Com isso “px” não aponta mais para “x”. Caso “px” seja 100, valerá 102 após a soma. Caso fosse ponteiro para float, valeria 104. 11 printf("px = %u, *px = %d, &px = %u\n", px,*px,&px); Imprime: conteúdo de “px” (endereço de “x”), o conteudo de onde “px” aponta (conteúdo de “x”) e o endereço da variável ponteiro “px” 12 py=px+3; Atribui para “py” a soma de 3 unidades de tipo mais o conteúdo de “px”. Com isso “py” não aponta mais para “y”. 13 printf("py = %u, *py = %d, &py = %u\n", py,*py,&py); Imprime: conteúdo de “py” (endereço de “y”), o conteudo de onde “py” aponta (conteúdo de “y”) e o endereço da variável ponteiro “py” 14 printf("py-px = %u\n",py-px); Imprime a subração do endereço contido em “py” pelo endereço contido em “px” 15 system(“pause”); Pára o programa 16 } Fecha main() 3.1 Vetores de Ponteiros Todo vetor, na realidade, é um ponteiro. Sendo assim, podemos declará-las e manipulá-las como tal. A grande vantagem é a velocidade de execução, muito mais rápida através de ponteiros do que com índices, porém o seu uso desordenado torna o programa incompreensível. Digite o Exemplo 3.3:- Faça um programa que receba as notas de 5 alunos através de ponteiros e mostre a media. 1 #include <stdio.h> #include <stdlib.h> Inclui protótipos 2 int main(){ Abre main() 3 float *notas; Declara variavel ponteiro 4 notas=(float *) malloc(5*sizeof(float)); Aloca 20 (5*4) bytes na memória e atribui o endereço do primeiro byte alocado para a variável ponteiro “notas” 5 for (int i=0;i<5;i++){ scanf("%f", notas+i); } Carrega vetor a partir do usuário. (notas + i) é o endereço onde será alocado o que o usuário digitar 6 for (i=0;i<5;i++){ printf("%f\n", *(notas+i)); } Imprime todas as notas. *(notas+i) é o conteúdo do endereço (notas+i) 7 12. free(notas); Libera o espaço para outras tarefas usarem 8 system(“pause”); Pára o programa 9 } Fecha main() Aqui temos duas novas funções: malloc() e free(). Malloc() é utilizada para alocar espaço na memória e retornar o endereço do primeiro byte. A função free() libera o espaço que foi reservado por malloc(). A este recurso chamamos de "Alocação Dinâmica", pois o espaço na memória é criado em tempo de execução. 3.2 Matrizes de Ponteiro 1. Indireção Simples Da mesma forma que o vetor, podemos manipular matrizes, através de ponteiros, linearmente. Assim, para que todas as posições sejam acessadas utilizamos uma aritmética simples de endereços. Digite o Exemplo 3.4:- Faça um programa que carregue, a partir do usuário, uma matriz 3x3 alocada dinamicamente por indireção simples, e mostre em seguida todos os valores carregados. 1 #include <stdio.h> #include <stdlib.h> Inclui protótipos 2 #define L 3 #define C 3 Define macros L e C 3 int main(){ Abre main() 4 int *mat,i,j; Declara variaveis 5 mat=(int *) malloc(L*C*sizeof(int)); Aloca 18 bytes (3*3*2) na memoria e atribui o endereço do primeiro byte para a variavel mat 6 for (i=0;i<L;i++){ for (j=0;j<C;j++){ scanf ("%d",mat+(i*C) +j); } } Carrega a matriz a partir do usuário. mat+(i*C)+j é o endereço onde será alocado cada valor digitado. A matriz é linear. 7 for (i=0;i<L;i++){ for (j=0;j<C;j++){ printf(“%d\n”,*(mat+(i*C)+j)); } } Mostra os valores alocados na matriz. *(mat+(i*C)+j) é o proprio valor que o usuário digitou 8 system(“pause”); Pára o programa 9 free(mat); Libera memoria 10 } Fecha main() 2. Indireção Múltipla Outra forma de construirmos matrizes é através da declaração de ponteiro para ponteiro. Isso faz com que nosso programa fique mais flexível, pois poderemos classificar as linhas através de uma simples troca de ponteiros. Digite o Exemplo 3.5:- Faça um programa que carregue, a partir do usuário, uma matriz 3x3 alocada dinamicamente por indireção múltipla, e mostre em seguida todos os valores carregados. 1 #include <stdlib.h> #include <stdio.h> Inclui protótipos 2 #define LIN 3 #define COL 3 Define macros LIN e COL 3 int main(){ Abre main() 4 int **mat, i, j; Declara variavel ponteiro de ponteiro para int 5 mat = (int **) malloc(LIN*sizeof(int *)); Aloca 6 bytes na memoria e atribui o endereço do primeiro byte para a variavel mat. Este ponteiro irá apontar para um vetor de ponteiros 6 for (i=0;i<LIN;i++){ *(mat+i)= (int *) malloc(COL*sizeof(int)); } Aloca 6 bytes para cada posição do vetor de ponteiros, ou seja, cada posição alocada anteriormente irá apontar para um vetor de inteiros. 7 for (i=0;i<LIN;i++){ for (j=0;j<COL;j++){ scanf("%d",*(mat+i)+j); } } Carrega os valores a partir do usuário. Percorre todas as colunas que cada posicao que o vetor ponteiro aponta, carregando assim toda a matriz. 8 for (i=0;i<LIN;i++){ for (j=0;j<COL;j++){ Imprime os conteúdos de toda matriz printf("%d\n",*(*(mat+i)+j)); } } 9 system(“pause”); Pára o programa 10 for (i=0;i<LIN;i++){ free(*(mat+i)); } Libera os vetores coluna 11 free(mat); Libera o vetor linha 12 } Fecha main() 3.3 Exercícios 1. Faça um programa que mostre se um determinado valor foi encontrado ou não em um vetor de inteiros. O usuário deve digitar: o valor a ser encontrado, o tamanho do vetor (a ser criado dinamicamente) e os valores de cada posição do vetor. 2. Faça uma função que aloque e preencha um vetor com tamanho escolhido pelo usuário. O preenchimento deve ser feito com números aleatórios (função rand()) e não deve conter números repetidos. 3. Considerando que a variação de tensão foi percebida em várias residências de um determinado bairro, desenvolva um programa capaz de receber a média destes valores referentes a cada residência em uma matriz com tamanho determinado pelo usuário, sendo que cada linha da matriz representará uma rua e a quantidade de casas será a mesma em todas as ruas. O programa deverá mostrar qual a rua de tensão mais estável (desvio padrão). Utilize indireção simples para alocação da matriz. 4. Escreva um programa que gere uma matriz em cada posição guarde a soma de suas coordenadas. As dimensões da matriz deverão ser digitadas pelo usuário e o programa deverá informar os valores gerados. 5. Escreva um programa que leia uma matriz, a partir do usuário, troque a posição de duas linhas escolhidas pelo usuário e mostre a matriz resultante. Utilize indireção múltipla para alocação da matriz. 4 Listas Uma lista linear é uma seqüência de informações organizadas seqüencialmente (lógica ou fisicamente). Uma lista pode ser organizada estática ou dinamicamente na memória. Na forma estática utilizamos a estrutura de vetor para armazenarmos as informações e as ligações e na dinâmica utilizamos uma estrutura que será alocada na memória sempre que precisarmos de um novo espaço. Podemos atribuir várias operações para uma lista, por exemplo: 1. Inicializar (esvaziar) a lista 2. Inserir um elemento na lista 3. Retirar um elemento da lista4. Verificar se a lista está vazia 5. Imprimir a lista Nós veremos duas formas de implementação: através de vetores e através de ponteiros. 4.1 Listas Através de Vetores Neste caso, teremos a declaração de um vetor de um determinado tamanho. Este tamanho deve ser suficiente para alocar toda a lista, prevendo aumentos futuros, pois neste tipo de alocação (estática), não podemos alterar este tamanho em tempo de execução. Assim, nossa lista estará alocada em posições contíguas na memória. A lista sempre terá um início no índice 0 (zero) e um fim que nem sempre coincidirá com o final do vetor. Podemos alocar elementos em qualquer posição da lista, e quando isso acontecer no meio da lista, devemos deslocar todos os elementos que estarão à direita dele, afim de “abrir” espaço para alocação. Da mesma forma teremos que deslocar elementos sempre que retirarmos elementos do meio da lista. Este procedimento de deslocamento evita a existência de “espaços vazios” no meio da lista. a1 a2 a3 ... an ... 0 1 2 ... n- 1 ... Tam_Vet O primeiro elemento da nossa lista na figura acima é “a1”, o último é “an” alocados em um vetor de tamanho Tam_Vet. Teremos sempre uma variável para controlar o inicio da lista e o fim da lista, sempre que acrescentarmos uma elemento a lista, a variável que controla o final da lista será incrementado, e será decrementado sempre que for retirado um elemento. Quando o final da lista for maior que Tam_Vet consideramos a lista cheia, ou seja, não podemos inserir mais nenhum elemento e quando o final da lista for igual ao início, consideramos a lista vazia, e portanto, não podemos retirar nenhum elemento. primeir o últim o Digite o Exemplo 2.1:- Faça um TAD de uma lista capaz de controlar a inserção, retirada e impressão dos elementos da lista. Faça também um programa capaz de testar o TAD. Este exemplo deve ser dividido em dois arquivos: um para o TAD (terá extensão .h) e outro com o programa principal(terá a extensão .cpp). Inclua o arquivo “.h” no arquivo “.cpp” e execute o .cpp. Esta é uma maneira simples de implementação de um TAD. // arquivo .h // Lista atraves de Vetor #include <iostream.h> #include <conio.h> #define INICIO_VET 0 #define TAM_VET 10 Struct TipoLista{ int item[TAM_VET]; int primeiro, ultimo; }; void Inicializa(TipoLista *lista){ lista->primeiro=INICIO_VET; lista->ultimo=lista->primeiro; } int Vazia(TipoLista lista){ return (lista.primeiro==lista.ultimo); } void Insere(int elem, TipoLista *lista){ if (lista->ultimo+1 > TAM_VET){ cout << "Lista cheia."; } else{ lista->item[lista->ultimo]=elem; lista->ultimo++; } } void Retira_Pos(int p, TipoLista *lista, int *item){ int aux; if (Vazia(*lista) || p >= lista->ultimo){ cout << "Erro: Posicao nao existe"; return; } *item=lista->item[p-1]; for (aux=p; aux<lista->ultimo;aux++){ lista->item[aux-1]=lista->item[aux]; } lista->ultimo--; } void Imprime(TipoLista lista){ int aux; for (aux=lista.primeiro;aux<(lista.ultimo);aux++){ cout << lista.item[aux] << endl; } } // arquivo .cpp #include “Tlista.h” int main(){ TipoLista l; int elem; clrscr(); Inicializa(&l); elem=10; Insere(elem,&l); elem=20; Insere(elem,&l); elem=30; Insere(elem,&l); elem=40; Insere(elem,&l); elem=50; Insere(elem,&l); Retira_Pos(1,&l,&elem); Imprime(l); getch(); } 4.2 Listas através de Ponteiros Desta forma, podemos ter elementos na memória sempre que precisarmos dele, pois sua alocação é criada em tempo de execução através da função malloc(). Cada elemento deve ter informação do próximo e do anterior. O exemplo acima define uma estrutura que contém um valor inteiro e dois ponteiros para a mesma estrutura. Estes ponteiros servirão para “ligar” a lista, a variável “ant” apontará sempre para a estrutura anteriormente alocada e a variável “prox” apontará para a próxima estrutura da seqüência ou para NULL (nulo), caso seja a última estrutura da lista. struct no{ no *ant; int valor; no *prox; }; ant Valor prox Digite o Exemplo 10.2 :- Faça um programa que carregue uma lista ligada na memória de valores inteiros. Após o carregamento da lista, mostre-a. #include <stdio.h> #include <stdlib.h> struct no{ no *ant; int valor; no *prox; }; int main(){ no *inicio, *fim, *novo, *aux, *proximo, *anterior; int aux_valor, cont, achou; // inicializado em zero para forcar entrada no looping // Inclusao na lista scanf("%i",&aux_valor); if (aux_valor!=-1){ inicio = (no *) malloc(sizeof(no)); inicio->valor=aux_valor; inicio->prox=inicio->ant=NULL; fim=novo=aux=inicio; } while (aux_valor != -1){ scanf("%i",&aux_valor); if (aux_valor!=-1){ novo = (no *) malloc(sizeof(no)); novo->valor=aux_valor; aux->prox=novo; novo->ant=aux; fim=aux=novo; novo->prox=NULL; } } // Consulta geral da lista aux=inicio; while (aux->prox!=NULL){ printf("\n%i",aux->valor); aux=aux->prox; } printf("\n%i",aux->valor); // Remoção do 7o. elemento de uma // lista com N elementos aux=proximo=anterior=inicio; cont=1; achou=0; while (aux->prox!=NULL){ if (cont==7){ printf("vai remover"); anterior->prox=proximo; proximo->ant=anterior; free(aux); achou=1; break; } cont++; anterior=aux; aux=aux->prox; proximo=aux->prox; } if (achou==0){ printf("Não existe o setimo elemento"); } else{ // Consulta geral da lista aux=inicio; while (aux->prox!=NULL){ printf("\n%i",aux->valor); aux=aux->prox; } printf("\n%i",aux->valor); } } A maneira mais simples de se trabalhar com lista é através de vetores (lista seqüencial). Podemos ter dois tipos de listas de acesso restrito (Pilha e Fila), ou seja, com posição definida para colocar e para retirar elementos da lista. 4.3 Exercícios Lista através de Vetores 1. Acrescente a função de inserir um elemento em uma posição específica no TAD lista através de vetores. void Insere_Pos(int p, TipoLista *lista, TipoItem *item){ 2. Acrescente a função de retirar um elemento qualquer no TAD lista através de vetores. void Retira_Elem(TipoLista *lista, TipoItem *item){ 3. Acrescente a função de listar a lista em ordem contrária, ou seja, do último para o primeiro elemento. void Mostra_Invertido(TipoLista *lista){ Lista através de Ponteiros 4. Crie um TAD lista, agora utilizando ponteiros. A TAD deve conter a função Inserir um elemento, Retirar um elemento, Listar todos os elementos em ordem contrária. 5. Acrescente ao TAD lista através de ponteiros para Retirar um elemento da lista e para consultar um elemento na lista. 21 5 Pilha Uma pilha, nada mais é que uma lista linear, porém com restrição de acesso (inserção e retirada), por isso ela é também chamada de lista restrita. Uma pilha funciona como uma pilha de pratos: acrescenta-se sempre o próximo prato no topo da pilha e ao fazermos a retirada, retiramos o último prato que colocamos (lifo: last in first out). Veremos dois modos de implementação: Seqüencial e ligada com ponteiros. As operações básicas do TAD Pilha, são: inserção (empilha), remoção (desempilha), verifica se vazia, verifica se cheia e retorna topo. 5.1 Pilha através de Vetores Para alocar uma pilha utilizando vetores, devemos construir uma estrutura que contenha as informações dos elementos da pilha, como também o índice do topo. Assim, typedef struct{ int info[TamMax]; int topo; }Pilha; 22 Digite o Exemplo 3.1:- Faça uma TAD pilha implementada com vetor e que contenha funções para empilhar, desempilhar, mostrar o conteúdo da pilha e retornar o elemento do topo. O TAD deve ter extensão .h. // TAD Pilha com Vetor #include <iostream.h> #include <conio.h> #define TamMax 10 typedef struct{ int info[TamMax]; int Topo; }Pilha; void Inicializa(Pilha *p){ p->Topo=0; } int Vazia(Pilha p){ if (p.Topo==0){ return 1; } else{ return 0; } } int Cheia(Pilha p){ if (p.Topo==TamMax){return 1; } else{ return 0; } } int Empilha(Pilha *p, int elem){ if (Cheia(*p)){ return 0; } else{ p->info[p->Topo]=elem; p->Topo++; return 1; } } int Desempilha(Pilha *p){ if (Vazia(*p)){ return 0; } else{ return p->info[p->Topo--]; } } int RetTopo(Pilha p){ return p.info[p.Topo-1]; } int MostraPilha(Pilha p){ int i; if (Vazia(p)){ return 0; } else{ for (i=0;i<p.Topo;i++){ cout << p.info[i] << endl; } getch(); return 1; } } 23 Digite o Exemplo 3.2:- Faça um programa que, usando a TAD Pilhav.h, empilhe os valores 10, 20, 30, 40 e 50 e desempilhe dois elementos, logo em seguida. Após todos os empilhamentos e a cada desempilhamento mostre o valor do topo e o conteudo atual de toda pilha. // Este programa testa a TAD Pilha com Vetor // Empilha-se 5 elementos e desempilha-se 2 elementos #include "d:\aulas\aposti~1\lingua~1\pilhav.h" void main(){ Pilha p; Inicializa(&p); Empilha(&p,10); Empilha(&p,20); Empilha(&p,30); Empilha(&p,40); Empilha(&p,50); clrscr(); cout << endl << "Topo = " << RetTopo(p) << endl; MostraPilha(p); Desempilha(&p); cout << endl << "Topo = " << RetTopo(p) << endl; MostraPilha(p); Desempilha(&p); cout << endl << "Topo = " << RetTopo(p) << endl; MostraPilha(p); } 24 5.2 Pilha através de Ponteiros Digite o Exemplo 3.3:- Faça um programa que mostre um menu de escolha para o usuário com as opcoes de inclusão, consulta e retirada de elementos em uma pilha alocada dinamicamente. Construa um TAD Pilha para isso. //Inclusao e consulta em uma Pilha #include <iostream.h> #include <conio.h> #include <stdlib.h> struct no{ no *prox; int val; }; void insere(no **t, int elem); void consulta(no *t); int retira(no **t); int menu(); void main(){ no *topo=NULL; int opcao=0, elem; while (opcao!=4){ opcao=menu(); switch (opcao){ case 1: cout << "Digite o valor a ser inserido: "; cin >> elem; insere(&topo, elem); break; case 2: consulta(topo); break; case 3: elem=retira(&topo); if (elem!=-1){ cout << elem; } getch(); break; case 4: exit(1); } } } void consulta(no *t){ if (t==NULL){ cout << "Pilha vazia"; } else{ do{ cout << t->val << endl; t=t->prox; }while (t!=NULL); } getch(); } int menu(){ int op; clrscr(); cout << "Pilha" << endl; cout << "1. Insere" << endl; cout << "2. Consulta" << endl; cout << "3. Retira" << endl; cout << "4. Sai" << endl; cin >> op; return op; } void insere(no **t, int elem){ no *novo; novo=(no *) malloc(sizeof(no)); novo->val=elem; if ((*t)==NULL){ (*t)=novo; (*t)->prox=NULL; } else{ novo->prox=*t; (*t)=novo; } } int retira(no **t){ int elem; no *aux; if ((*t)==NULL){ cout << "Pilha vazia"; } else{ elem=(*t)->val; aux=(*t); (*t)=(*t)->prox; free(aux); return elem; } return -1; } 5.3 Exercícios Implementação de Pilha por Vetores 1. Ofereça um serviço que retorne o tamanho da pilha. 25 2. Ofereça um serviço que mostre os 3 próximos elementos a serem desempilhados, se houverem. Pilha através de Ponteiros 3. Faça um programa que controle o “morto” do baralho (monte de cartas onde os jogadores podem comprar cartas durante um jogo). Esta seqüência obedece uma pilha onde a carta sempre será retirada do topo da pilha. A estrutura deverá conter o naipe e o valor de cada carta. Faça um TAD que construa o morto e retire carta dele. 26 6 Fila A fila funciona como uma fila de banco, sempre que alguém chega, ocupa o último lugar e quando alguém sai, sai do começo da fila (fifo: first in first out). 6.1 Fila através de Vetores A implementação de fila por vetor requer uma variável de controle para o inicio e uma para o fim da fila. Sempre que inserirmos um elemento, inseriremos no índice “fim”, e sempre que retirarmos um elemento, retiraremos no índice “início”. Digite o Exemplo 4.1: Faça um TAD fila, contendo a estrutura da fila (vetor fila, inicio e fim) e funções para: Inserir, Retirar e Listar elementos. // TAD de Fila atraves de Vetor #include <iostream.h> #include <conio.h> #define TAM_VET 10 typedef struct{ int item[TAM_VET]; int inicio, fim; }TipoFila; void Inicializa(TipoFila *fila){ fila->inicio=0; fila->fim=fila->inicio; } int Vazia(TipoFila fila){ return (fila.inicio==fila.fim); } int Insere(int elem, TipoFila *fila){ if (fila->fim == TAM_VET){ cout << "Fila cheia."; return 0; } else{ fila->item[fila->fim]=elem; fila->fim++; return 1; } } int Retira(TipoFila *fila, int *item){ if (Vazia(*fila)){ cout << "Fila Vazia."; return 0; } *item=fila->item[fila->inicio]; fila->inicio++; return 1; } int Imprime(TipoFila fila){ int aux; if (Vazia(fila)){ cout << "Fila Vazia."; return 0; } for (aux=fila.inicio;aux<fila.fim;aux++){ cout << fila.item[aux] << endl; } return 1; } 27 Digite o Exemplo 4.2: Faça um programa que teste o TAD fila, inserindo os elementos 10, 20, 30, 40 e 50 e depois retirando um elemento para, logo em seguida, mostrar a listagem da fila. // Programa para testar a TAD Fila atraves de Vetores #include <conio.h> #include "d:\aulas\aposti~1\lingua~1\fila.h" int main(){ TipoFila f; int elem; clrscr(); Inicializa(&f); elem=10; Insere(elem,&f); elem=20; Insere(elem,&f); elem=30; Insere(elem,&f); elem=40; Insere(elem,&f); elem=50; Insere(elem,&f); Retira(&f,&elem); cout << "Elem. retirado = " << elem << endl; Imprime(f); getch(); } 28 6.2 Fila através de Ponteiros Digite o Exemplo 4.3:- Faça um programa que mostre um menu de escolha para o usuário com as opções de inclusão, consulta e retirada de elementos em uma fila alocada dinamicamente //Inclusao e consulta em uma lista #include <iostream.h> #include <conio.h> #include <stdlib.h> struct no{ no *prox; int val; }; void insere(no **i,no **f, int elem); void consulta(no *i); int retira(no **i); int menu(); void main(){ no *inicio=NULL, *fim=NULL; int opcao=0, elem; while (opcao!=4){ opcao=menu(); switch (opcao){ case 1: cout << "Digite o valor a ser inserido: "; cin >> elem; insere(&inicio,&fim, elem); break; case 2: consulta(inicio); break; case 3: elem=retira(&inicio,&elem); if (elem!=-1){ cout << elem; } getch(); break; case 4: exit(1); } } } void consulta(no *i){ if (i==NULL){ cout << "Fila vazia"; } else{ do{ cout << i->val << endl; i=i->prox; }while (i!=NULL); } getch(); } void insere(no **i,no **f, int elem){ no *novo; novo=(no *) malloc(sizeof(no)); novo->val=elem; if ((*f)==NULL){ (*f)=(*i)=novo; (*f)->prox=NULL; } else{ (*f)->prox=novo; (*f)=novo; (*f)->prox=NULL; } } int retira(no **i, int *elem){ int elem; if ((*i)==NULL){ return 0; } else{ *elem=(*i)->val; (*i)=(*i)->prox; return 1; } } int menu(){ int op; clrscr(); cout << "Fila" << endl; cout << "1. Insere" << endl; cout << "2. Consulta" << endl; cout << "3. Retira" << endl; cout << "4. Sai"<< endl; cin >> op; 29 return op; } 6.3 Exercícios Fila através de Ponteiros 1. Faça um programa que controle o atendimento aos clientes em uma farmácia. O atendimento obedece uma fila organizada por ordem de chegada. O programa deve permitir a inclusão, consulta e atendimento de clientes na fila. As informações do cliente deve ser cic, nome e telefone. 30 7 Métodos de Classificação 7.1 Seleção Estrita O método de seleção estrita compara o primeiro elemento com todos os demais, colocando em uma variável auxiliar o índice do valor menor. Terminando as comparações, o primeiro elemento deve ficar no lugar daquele de menor valor encontrado e vice-versa. O mesmo deve ser feito com o segundo elemento. O vetor só estará classificado quando o processo chegar ao último elemento do vetor. A tabela abaixo exemplifica o processo acima descrito. Passo Elementos do Vetor Índice do elemento selecionado (i) Índice do menor (im) 0 1 2 3 Original 44 55 12 42 25 0 2 1 12 55 44 42 25 1 4 2 12 25 44 42 55 2 3 3 12 25 42 44 55 3 3 Fim 12 25 42 44 55 Veja abaixo a implementação da função do método de seleção direta. void seleção_direta(int *v, int t) { int im,aux, i, j; for(i=0;i<t-1;i++) { im=i; for(j=i+1;j<t;j++) { if(v[j]<v[im]) { im=j; } // fim do if } // fim do for j aux=v[i]; v[i]=v[im];v[im]=aux; } // fim do for i } // fim da funcao seleção_direta 7.2 Troca - Bolha 31 O método de seleção por troca chamado Bolha compara dois elementos realizando a troca de suas posições caso o elemento da esquerda seja maior que o da direita. A verificação acontece em todo o vetor obedecendo a seqüência (0,1), (1,2), (2,3) …., (n-1,n). Esse processo ou interação deve se repetir até que nenhuma troca aconteça. Interações Passo Elementos do Vetor Índice do elemento da esquerda Índice do elemento da direita 0 1 2 1 Original 44 55 12 42 25 0 1 1 44 55 12 42 25 1 2 44 12 55 42 25 2 3 3 44 12 42 55 25 3 4 44 12 42 25 55 0 1 2 1 12 44 42 25 55 1 2 2 12 42 44 25 55 2 3 12 42 25 44 55 3 4 4 12 42 25 44 55 0 3 1 12 42 25 44 55 1 2 2 12 25 42 44 55 2 3 12 25 42 44 55 3 4 4 12 25 42 44 55 0 4 1 12 25 42 44 55 1 2 2 12 25 42 44 55 2 3 12 25 42 44 55 3 4 4 12 25 42 44 55 Nenhuma troca - fim Veja abaixo a implementação da função do método de troca “Bolha”. void troca_bolha(int *v, int t) { int i, aux, trocou=1; while(trocou==1) { trocou=0; for(i=0;i<t-1;i++) { if(v[i]<v[i+1]) { aux=v[i]; v[i]=v[i+1]; v[i+1]=aux; trocou=1; } // fim do if } // fim do for i } // fim do while } // fim da funcao troca_bolha 7.3 Troca – Quick Sort Um grande número de aplicações se utiliza deste algorítmo devido a sua rapidez de execução. O algorítmo foi inventado por Hoare em 1960. Seu procedimento se baseia na máxima: “dividir para conquistar”. Assumindo que queiramos ordenar crescentemente uma lista, escolhemos um elemento qualquer para ser o “pivô”. Ele deve ser colocado na posição correta 32 utilizando-se um algoritmo qualquer. Uma sugestão é contar o número de elementos que são menores que o pivô. Por exemplo, se há 5 elementos menores que o pivô, seu lugar será no índice 5 do vetor, sendo necessário realizar uma troca entre o pivô e o elemento que estiver na posição 5. Assim, a lista fica dividida em duas partes: direita e esquerda. Percorremos a parte esquerda comparando cada elemento com o elemento pivô, interrompendo a comparação quando encontrarmos um elemento maior que o pivô, o qual deveria estar do lado direito do pivô. Então percorremos o lado direito e executamos o mesmo procedimento, agora interrompendo a comparação quando o elemento for menor que o pivô, devendo estar do lado esquerdo do pivô. Com estes dois elementos em mãos, trocamos suas posições. Continuamos com este procedimento até que todos os elementos a esquerda do pivô sejam menores que ele e os elementos da direita sejam maiores que ele. Desta forma, podemos aplicar o mesmo procedimento para o vetor da esquerda e da direita, escolhendo um pivô para cada um deles, até que toda a lista esteja ordenada. Veja o exemplo em que a lista que se quer classificar é composta dos elementos (44, 55, 12, 42, 25, 60, 20). O elemento escolhido para ser pivô é o primeiro (44). Há 4 elementos menores, portanto sua posição no vetor é a quinta, ou no índice 4. Divisões Elementos do Vetor Índice do pivô Índice da esquerda Índice da direita0 1 2 3 4 5 Original 44 55 12 42 25 60 20 Valor do pivô escolhido = 44 Troca do pivô Divisão 1 25 55 12 42 44 60 20 4 0 6 1 25 20 12 42 44 60 55 Lado esquerdo. Valor do novo pivô = 25 Troca do pivô Divisão 2 12 20 25 42 44 60 55 2 0 3 2 12 20 25 42 44 60 55 Nenhuma troca. Valor do novo pivô = 12 Troca do pivô Divisão 3 12 20 25 42 44 60 55 Com 2 elementos, na troca do pivô o lado já estará classificado. 3 12 20 25 42 44 60 55 Lado direito. Valor do novo pivô = 60 Troca do pivô Divisão 4 12 20 25 42 44 55 60 Com 2 elementos, na troca do pivô o lado já estará classificado. Veja abaixo a implementação da função do método de troca “QuickSort”, retirada do livro de Niklaus Wirth. #include <stdio.h> #include <stdlib.h> #include <iostream.h> #include <conio.h> #define TAM 10 void sort(int L, int R) { int i,j,w,x; i=L; j=R; 33 int a[TAM]; int k; void sort(int L, int R); int main() { for (k=0;k<TAM;k++) // digitacao dos elementos { cout << "\n elemento " << k << " = "; cin >> a[k]; } cout << "\n vetor = "; for (k=0;k<TAM;k++) // mostra o vetor digitado { cout << a[k] << ","; } getch(); sort(0, TAM-1); // chama a classificacao cout << "\nVetor Classificado \n"; for (k=0;k<TAM;k++) // mostra o vetor classificado { cout << a[k] << ","; } getch(); } x=a[(L+R)/2]; do{ while(a[i]<x){ i++; } while(x<a[j]){ j--; } if(i<=j){ w=a[i]; a[i]=a[j]; a[j]=w; i++; j--; } }while(i<=j); if(L<j){ sort(L,j); } if(i<R){ sort(i,R); } } 7.4 Intercalação Trata-se de intercalar dois vetores já classificados, formando um terceiro também classificado. A classificação dos dois vetores menores pode ser feita utilizando qualquer método. A tabela abaixo demonstra o método de intercalação e, em seguida a implementação do mesmo. Segmentos Elementos do Vetor Vetor 1 12 55 57 63 71 Vetor 2 5 15 30 57 Vetor resultado 5 12 15 30 55 57 57 63 71 intercala(int *v1, int tv1, int *v2, int tv2, int *resultado) { // tv1 e tv2 sao os tamanhos dos vetores int i, j, k, fim; i = j = k = fim = 0; while(k<tv1+tv2) { if((i<tv1) && (j<tv2)) { if(v1[i]<v2[j]) else { if(i<tv1) { for(;i<tv1;i++) { resultado[k]=v1[i]; k++; } // fim do for i } // fim do if 34 { resultado[k]=v1[i]; k++; resultado[k]=v2[j]; k++; i++; j++; } else { resultado[k]=v2[j]; k++; resultado[k]=v1[i]; k++; j++; i++; } } else { for(;j<tv2;j++) { resultado[k]=v2[j]; k++; } // fim do for j } // fim do else } // fim do else } // fim do while } // fim da funcao intercala 7.5 Inserção – Direta Esse método considera 2 segmentos dentro do mesmo vetor, sendo o primeiro segmento formado apenas pelo primeiro elemento. Os demais elementos passam a fazer parte do primeiro segmento, quando, um a um são inseridos nele respeitando a ordem desejada. Na tabela abaixo, os números em azul pertencem ao primeiro segmento. 380 260 340 220 200 420 280 320 300 260 380 340 220 200 420 280 320 300 260 340 380 220 200 420 280 320 300 220 260 340 380 200 420 280 320 300 200 220 260 340 380 420 280 320 300 200 220 260 340 380 420 280 320 300 200 220 260 280 340 380 420 320 300 200 220 260 280 320 340 380 420 300 200 220 260 280 300 320 340 380 420 void insercao_direta(int *vet, int t) { int i, j, k, aux; // i trabalha com o primeiro // segmento j=1; // j e o inicio do segundo segmento while(j<t) { for(i=0;i<j;i++) aux=vet[j]; for(k=j;k>i;k--) // desloca elementos // para insercao { vet[k]=vet[k-1]; } vet[i]=aux; } 35 { if(vet[i]>vet[j]) // encontrou posição // para insercao { } j++; } } // fim da funcao insercao_direta 7.6 Inserção - Shell Também conhecido como classificação de incremento decrescente, esse método utiliza um incremento. Por exemplo, se tivermos um vetor com 10 elementos e usarmos o incremento 4, as posições que serão ordenadas são (0,4,8), (1,5,9), (2,6), (3,7). Cada um desses conjuntos será classificado por inserção direta. Em seguida, o incremento a ser usado será o 2. Os conjuntos serão os seguintes: (0,2,4,6,8), (1,3,5,7,9). Após classificados, o incremento 1 deve ser usado, tratando todo o vetor como um único conjunto. Uma sugestão para a escolha dos incrementosé f(x)=2x. Nessa função x deve variar de 0 até o número de incrementos que se deseja. Para 4 incrementos temos (1,2,4,8). A tabela abaixo exemplifica o método. Incremento Vetor 4 25 57 48 12 3 10 5 20 40 30 2 3 10 5 12 25 30 48 20 40 57 1(inserção direta) 3 10 5 12 25 20 40 30 48 57 3 5 10 12 20 25 30 40 48 57 O programa do quadro abaixo implementa o método de classificação Shell usando a inserção direta para organizar os sub-vetores. #include <stdio.h> #include <stdlib.h> #include <conio.h> #include <iostream.h> #include <math.h> void insercao_shell(int *vet, int t, int nincr); #define TAM 13 #define X 2 int main() void insercao_shell(int *vet, int t, int nincr) { int i, j, k, aux, incr; // i trabalha com o // primeiro segmento do{ incr=pow(2,nincr); // base 2 e expoente // nincr j=incr; // j e o inicio do segundo // segmento 36 { int vet[TAM]; int i; clrscr(); cout << "\n Digite 13 numeros"; for (i=0;i<TAM;i++) { cout << "\n Elemento " << i << " = "; cin >> vet[i]; } cout << "\n O vetor digitado foi:"; for (i=0;i<TAM;i++) { cout << "\n Elemento " << i << " = " << vet[i]; } cout << "\n Tecle enter para continuar"; getch(); insercao_shell(vet,TAM,X); cout << "\n O vetor classificado e:"; for (i=0;i<TAM;i++) { cout << "\n Elemento " << i << " = " << vet[i]; } cout << "\n Tecle enter para continuar"; getch(); } nincr--; while(j<t) { for(i=0;i<j;i=i+incr) { if(vet[i]>vet[j]) // encontrou posição // para insercao { aux=vet[j]; for(k=j;k>i;k=k-incr) // desloca // elementos { vet[k]=vet[k-incr]; } vet[i]=aux; } } j=j+incr; } }while(nincr>=0); } // fim da funcao insercao_shell 7.7 Distribuição – classificação por caixas Esse método assume o índice de um vetor como sendo o valor do dado enquanto que o conteúdo do vetor deve conter o número de vezes que o dado ocorre. Para intervalos muito grandes, grandes espaços podem ser desperdiçados. Veja o exemplo abaixo classificando os valores (5,3,1,2,3,10,3,5,8,1): Vetor com os valores de entrada. 5 3 1 2 3 10 3 5 8 1 Vetor de contadores. O índice representa os valores de entrada. 0 2 1 3 0 2 0 0 1 0 1 0 1 2 3 4 5 6 7 8 9 10 37 void distribuicao_caixa(int *vet, int t) { int cont[50]; int i,j; for(j=0;j<50;j++) // zerando contadores { cont[j]=0; } for(i=0;i<t;i++) // contando os valores // de entrada { cont[vet[i]]++; } i=0; for(j=0;j<50;j++) // remontando o vetor // classificado { while(cont[j]>0) { vet[i]=j; cont[j]--; i++; } } } // fim da funcao distribuicao_caixa 7.8 Distribuição – classificação por radicais (raízes, Radix) O Radix sort permite a classificação de números e de textos, sendo que para a primeira o processo é iniciado pelo dígito menos significativo (unidade). Vamos trabalhar um exemplo de classificação numérica. Para cada dígito utilizado na notação decimal (0 – 9), é gerada uma fila. Estas são alimentadas com base no resultado da análise dos números da lista a ser classificada. Para a lista de valores (30, 125, 9, 23, 15, 18), analise o processo demonstrado na tabela abaixo. Após a análise da unidade a lista de valores fica assim: 30, 23, 125, 15, 18, 9 Após a análise da dezena a lista de valores fica assim: 9, 15, 18, 23, 125, 30 Após a análise da centena a lista de valores fica assim: 9, 15, 18, 23, 30, 125 O processo termina porque o maior valor possui seu dígito mais significativo na casa da centena. Dígito Filas 0 1 2 3 4 5 6 7 8 Unidade 30 23 125 18 9 1 5 38 Dezena 09 15 23 30 18 12 5 Centena 009 125 015 018 023 030 Para implementação do processo demonstrado acima, é aconselhável o uso de fila com alocação dinâmica de memória. 7.9 Cálculo de Endereço (hashing) Trata-se do cálculo do endereço de um elemento a partir do próprio valor da chave. A função que calcula esse endereço é chamada função de espalhamento. Quando duas chaves diferentes produzem o mesmo endereço, diz-se que aconteceu uma “colisão”. Como dois valores não podem ser guardados no mesmo espaço de memória, é preciso resolver esse problema produzindo outro endereço que esteja livre. Quem monta a função de espalhamento deve conhecer os dados com que vai trabalhar. A melhor função é a que mais espalha os dados, gerando menos colisão. Divisão, enlaçamento, extração, transformação de raiz e função meio-quadrado são alguns tipos de função de hashing. Alguns métodos de tratamento de colisão são: endereçamento em balde, encadeamento e endereçamento aberto. O exemplo a seguir é bastante simples e pertence ao tipo “extração”, que trabalha com parte da chave. As colisões são tratadas usando o método de encadeamento. Considere os seguintes dados de entrada: 20, 5, 3, 41, 23. O cálculo do endereço considera a casa da dezena do número digitado. Esse número é o endereço em um vetor de 0 a 9. As colisões são tratadas com uma lista ligada usando o método de inserção direta. No exemplo, acontecem duas colisões nos números 3 e 23. Cada um deve ser colocado no índice calculado, porém, em ordem crescente na lista gerada. Na verdade, o vetor é formado por 10 ponteiros para início de listas. Dependendo do volume de dados e da quantidade de dígitos que abrange, essa função não gera um bom espalhamento. índice 0 1 2 3 4 5 6 7 8 9 valores 3 20 41 39 5 2 3 Observe o trecho de programa abaixo que implementa o exemplo de hashing demonstrado acima. struct lista { int elem; lista* prox; }; lista* primeiros[10]; void insere(int num, lista *prim[]) { lista *novo, *anterior, *proximo; int i, achei; i=num%100; i=i/10; novo=(lista*) malloc(sizeof(lista)); novo->elem=num; if(prim[i]==NULL) { prim[i]=novo; prim[i]->prox=NULL; } else { if(num < prim[i]->elem) { novo->prox=prim[i]; prim[i]=novo; } else { achei=0; anterior=prim[i]; proximo=prim[i]->prox; while(achei==0) { if(proximo==NULL) { anterior->prox=novo; novo->prox=NULL; achei=1; } else { if(num<proximo->elem) { novo->prox=proximo; anterior->prox=novo; achei=1; } else { anterior=proximo; proximo=proximo->prox; } } } // fim do while } } } // fim do insere 40 8 Métodos de Busca 8.1 Definição Só faz sentido guardar aquilo que se deseja usar. Tão importante quanto guardar uma informação é poder recuperá-la. Imagine aquela gaveta em que você guarda papéis com recados e telefones, recibos, correspondência bancária, cartinhas apaixonadas e só você sabe o que mais. Um dia, a vida de alguém depende de uma ligação sua e o número do telefone dessa pessoa está nessa gaveta. É bem provável que você encontre o papel desejado, mas não há garantias de que isso acontecerá antes do velório. 8.2 Método de Busca Linear ou Seqüencial É o mais simples dos métodos de busca e é usado principalmente quando os dados não estão classificados. O algoritmo desse método é finalizado quando o que se deseja é encontrado ou quando se chega ao final da lista. 8.3 Método de Busca Binária Esse método é aplicado em fontes de dados classificados. Consiste em dividir a lista de dados ao meio. Casoo que se busca não seja o elemento central, verifica-se se está antes ou depois dele. O procedimento deve ser repetido na metade escolhida. Veja o exemplo abaixo: Valor procurado: 35 5 7 15 30 35 37 39 46 Valor é maior que o central metade esquerda centro metade direita 5 7 15 30 35 37 39 46 Valor é menor que o central descartados metade esquerda centro metade direita 5 7 15 30 35 37 39 46 Só sobrou o próprio valor procurado descartados achei descartados Segue abaixo a implementação referente à pesquisa binária. 41 void pesquisa_binaria(int *v, int e, int t) { int inicio,fim,meio; int achou=0; inicio=0; fim=t; while(achou==0) { meio=(fim-inicio)/2+inicio; if(v[meio]==e) { cout << "O elemento " << e << "esta na posicao " << meio; getch(); achou=1; } else { if(e<v[meio]) { fim=meio-1; } else { inicio=meio+1; } } if(inicio>fim) { cout << "Elemento nao encontrado"; getch(); achou=1; } } // fim do while } 8.4 Método de Busca Hashing O método de busca hashing faz uso do cálculo de endereço através do qual a chave foi armazenada. Um exemplo desse método é discutido e exemplificado na seção 5.9. 42 9 Grafo 9.1 Definições Grafos são estruturas de dados versáteis que abrangem grande número de situações que as estruturas vistas até agora não conseguem representar. Para compreendermos o grafo é necessário que estejam claras as seguintes definições: Vértice: também conhecido como nó, é graficamente representado por um círculo e em uma expressão pela letra V. Aresta: liga dois nós ou vértices. Graficamente é representada por uma reta e em uma expressão pela letra E (edges). Grafo simples: é um conjunto não vazio de vértices e um conjunto que pode ser vazio de arestas. Grafo completo: cada vértice deve estar ligado a todos os outros vértices por arestas. EG = V(2)G (conjunto de arestas do grafo G é igual ao conjunto dos pares não ordenados dos vértices do grafo G). G é um KN (G é um grafo completo). Vértices adjacentes: são ligados pela mesma aresta. 43 Aresta incidente: é aquela que liga dois vértices adjacentes. Grau de um vértice: o grau de um vértice V é o número de arestas incidentes com V. No exemplo abaixo, V tem grau 2. Vértice isolado: é o vértice de grau zero ou sem arestas. Caminho: o caminho de v1 a vn é uma sequência de arestas (v1,v2), (v3,v4),...,(vn-1,vn). Pode ser escrito v1,v2,v3,v4,...,vn-1,vn. Circuito: é quando o caminho tem início e fim no mesmo vértice. Ciclo: é um circuito onde todos os vértices são diferentes. (V,E): é o conjunto de vértices e de arestas de um grafo. VG: é o conjunto de vértices do grafo G. EG: é o conjunto de arestas do grafo G. n(G): é número de vértices do grafo G. m(G): é o número de arestas do grafo G. n(G )≡∣VG∣ m(G )≡∣EG∣ VG (2) : é o conjunto de todos os pares não ordenados do grafo G. EG=φ : indica que o grafo G é vazio. G é um Kn : indica que G é um grafo completo. G é um K n : indica que G é um grafo vazio com n vértices. A B V 44 10 Árvores 10.1 Definições Uma estrutura de dados em árvores tem sua organização semelhante a uma árvore (como seu próprio nome já diz), com sua raiz e folhas. A cada informação guardada na árvore chamamos de nó. Portanto, temos um único nó o qual podemos denominar “raiz”, sendo que, a partir dele podemos ter várias sub-árvores. Os nós de nivel inferior são chamados de nós filhos. Então, nós do mesmo pai são chamados de irmãos. Na árvore acima, temos 15 nós, sendo que o nó A é a “raiz”, tendo o nó B, C e D como “filhos”. Chamamos os nós K, L, F, G, H, M e O de “folhas” ou “nós terminais” pois não tem filhos, e os nós com filhos chamamos de “nós não terminais”. O nó A tem “nível” 1, os nós B, C e D são de nível 2, e assim por diante. A B C D E F G H I J K L M N O 45 A implementação de uma árvore não é trivial, devido a sua flexibilidade na quantidade de filhos de um determinado nó. No entanto, podemos converter uma árvore em árvore binária. 10.2 Árvores Binárias Uma Árvore Binária é uma árvore restrita. Nela, cada nó, não pode ter mais que dois filhos. Para converter uma Árvore em Árvore Binária, todos os irmãos de um respectivo nó estarão a sua direita e todos os seus filhos a esquerda. Assim, A árvore binária acima é a conversão da árvore apresentada na na Ilustração 10.1. Para que possamos implementar esta árvore devemos criar um TAD contendo uma estrutura para nó: filho esquerdo, filho direito e informação. Algumas operações para manipular seus nós poderiam ser: Insere nó, Cria árvore e consulta nós. A B C D E F G H I J K L M N O 46 Digite o exemplo 5.1:- Faça um TAD de uma arvore Binária com funções para Inserir e Consultar a árvore, assim como um programa que teste a TAD. // TAD - Arvore Binaria #include <iostream.h> #include <stdlib.h> #include <conio.h> typedef struct No{ int info; No *esq, *dir, *pai; }; No *raiz; No *CriaArv(int x){ No *p; p=(No*) malloc(sizeof(No)); p->info=x; p->esq=p->dir=p->pai=NULL; return p; } void InsereDir(No *p, int x){ if (p==NULL){ cout << "Arvore Vazia"; } else{ if (p->dir != NULL){ cout << "Insercao Incorreta"; } else{ p->dir=CriaArv(x); } } } int Info(No *p){ if (p!=NULL){ return p->info; } } No *FilhoEsq(No *p){ if (p!=NULL){ return p->esq; } } No *FilhoDir(No *p){ if (p!=NULL){ return p->dir; } } No *Pai(No *p){ if (p!=NULL){ return p->pai; } } No *Irmao(No *p){ No *aux; if (p!=NULL){ aux=p->pai; if (aux==NULL){ return NULL; // p eh raiz } if (aux->esq==p){ return p->dir; } else{ return aux->esq; } } } void InsereEsq(No *p, int x){ if (p==NULL){ cout << "Arvore Vazia"; } else{ if (p->esq != NULL){ cout << "Insercao Incorreta"; } else{ p->esq=CriaArv(x); } } } void main(){ No *filho_esq,*filho_dir; raiz=CriaArv(5); InsereEsq(raiz,3); InsereDir(raiz,7); filho_esq=FilhoEsq(raiz); filho_dir=FilhoDir(raiz); InsereEsq(filho_esq,2); InsereDir(filho_esq,4); InsereEsq(filho_dir,6); InsereDir(filho_dir,8); clrscr(); cout << raiz->info << endl; // filho_esq=raiz->esq; // filho_dir=raiz->dir; cout << filho_esq->info << endl; cout << filho_dir->info << endl; getch(); } Toda organização e armazenamento tem um único objetivo: pesquisa. Estudaremos a seguir alguns métodos clássicos de percursos em árvore. 47 10.3 Percursos em Árvores Binárias Existem alguns algoritmos clássicos para percorrer uma árvore. Todas elas se utilizam da técnica recursiva. A fim de estabelecer as regras dos percursos, assumiremos algumas convenções: E :- significa que percorreremos o galho esquerdo do nó em questão. P :- significa acessar (mostrar, consultar ou alterar) o nó. D :- significa que percorreremos o galho direito do nó em questão. Através destas três convenções temos 6 combinações possíveis, no entanto, descreveremos as possibilidades quando a busca se iniciar pelo galho esquerdo (3 combinações). PED :- pre order EPD :- in order EDP :- pos order Exemplo de percurso: PED :- A, B, E, K, L, C, F, G, D, H EPD :- K, L, E, B, F, G, C, H, D, A EDP :- L, K, E, G, F, H, D, C, B, A A B C D E F G H K L 48 Abaixo nós apresentamos os algoritmos que permitem estes resultados. void PreOrder(No *p){ if (p != NULL){ cout << p->elem; PreOrder(p->esq); PreOrder(p->dir); } } Digite o Exemplo 5.4:- Faça uma função que mostre os elementos de uma árvore binária pelo método pre order. void InOrder(No *p){ if (p != NULL){ InOrder(p->esq); cout << p->elem; InOrder(p->dir); } } Digite o Exemplo 5.3:- Faça uma função que mostre os elementos de uma árvore binária pelo método pre order. void PosOrder(No *p){ if (p != NULL){ PosOrder(p->esq); PosOrder(p->dir); cout << p->elem; } } Digite o Exemplo 5.5:-Faça uma função que mostre os elementos de uma árvorebinária pelo método pre order. 10.4 Árvores Binárias de Busca (ABB) Este tipo de estrutura obedece as mesmas propriedades das Árvores Binárias. Porém, mais três propriedades devem ser observadas. 1. A informação do nó esquerdo deve ser menor que a informação do seu nó pai. 2. A informação do nó direito deve ser maior que a informação do seu nó pai. 3. Não existem informações repetidas. Assim, se percorremos a ABB através do método in order, teremos uma lista ordenada das informações da árvore. Exemplo de ABB. 49 A ABB é bastante apropriada para fazermos inserção, remoção e pesquisa, sempre tendo o cuidado de manter a ordenação da árvore. Na inserção, basta verificar se o elemento a ser inserido é maior ou menor que o nó em questão, quando for maior percorre a sub-arvore direita, e quando for menor percorre a sub-arvore esquerda. O elemento é inserido quando não houver mais sub-arvore a percorrer, e caso seja igual, o elemento não pode ser inserido, pois não podemos inserir elementos repetidos em ABB. Na busca também percorremos a sub-arvore direita e esquerda, dependendo do resultado da comparação, assim como na inserção, até encontrar o elemento procurado. A remoção tem três casos a serem ponderados: nó sem filhos, nó com um filho (esquerdo ou direito) e nó com dois filhos. No primeiro caso, simplesmente removemos o nó (seu pai aponta para NULL). No segundo caso, fazemos o pai do nó a ser removido apontar para o filho do nó a ser removido, em seguida, removemos o nó. No terceiro caso, fazemos uma substituição do nó a ser removido pelo seu nó sucessor (nó mais a esquerda da sub-arvore direita do nó a ser removido) ou antecessor (nó mais a direita da sub-arvore esquerda do nó a ser removido). Ao substituirmos o nó, é A D F I J E G H B C K 50 preciso verificar em qual dos dois primeiros casos recaímos, pois o nó que irá substituir o nó removido pode ou não ter filhos (no caso, zero ou um filho). Exemplo de inserção: na árvore do exemplo acima, os elementos foram inseridos na seguinte ordem: D, F, B, A, C, I, E, H, G, J, K. Agora iremos inserir o elemento L. Exemplo de remoção de nó sem filhos (nó G), considerando a árvore acima. Exemplo de remoção de nó com um filho (nó K), considerando a árvore acima. A D F I J E G H B C K L A D F I J E H B C K L 51 Exemplo de remoção de nó com dois filhos (nó D), considerando a árvore acima. A D F I J E H B C L A E F I J H B C L 52 #include <stdio.h> #include <stdio.h> #include <stdlib.h> #include <conio.h> struct No{ int info; struct No *dir, *esq; }; struct No *raiz; void InsereABB(struct No **p, int elem){ if ((*p)==NULL){ (*p)= (struct No *) malloc(sizeof(struct No)); (*p)->info=elem; (*p)->dir=(*p)->esq=NULL; } else{ if (elem > (*p)->info){ InsereABB(&(*p)->dir,elem); } else{ if (elem < (*p)->info){ InsereABB(&(*p)->esq,elem); } else{ printf("Elemento ja existe"); } } } } struct No *BuscaABB(struct No *p, int elem){ if (p!=NULL){ if (elem > p->info){ return BuscaABB(p->dir,elem); } else{ if (elem < p->info){ return BuscaABB(p->esq,elem); } else{ return p; } } } else{ return NULL; } } Digite o exemplo 10.13:- Faça uma TAD de uma Arvore Binária de Busca (ABB). A informação de cada nó será um número inteiro. Deverá conter funções para inserir, remover e pesquisar cada nó, além de uma consulta geral mostrando as informações em ordem crescente. struct No *Troca(struct No **p, struct No *sucessor) { struct No *aux; if (sucessor->esq == NULL){ (*p)->info=sucessor->info; aux=sucessor; sucessor=sucessor->dir; free(aux); } else{ Troca(&(*p),(*p)->esq); } } void RemoveABB(struct No **p, int elem){ struct No *aux; if ((*p)!=NULL){ if (elem > (*p)->info){ RemoveABB(&(*p)->dir,elem); } else{ if (elem < (*p)->info){ RemoveABB(&(*p)->esq,elem); } else{ if ((*p)->esq==NULL){ aux=(*p); (*p)=(*p)->dir; free(aux); } else{ if ((*p)->dir==NULL){ aux=(*p); (*p)=(*p)->esq; free(aux); } else{ Troca(&(*p),(*p)->dir); } } } } } 53 else{ printf("Elemento nao existe"); } } void InOrder(struct No *p){ if (p!=NULL){ InOrder(p->esq); printf("\n%d",p->info); InOrder(p->dir); } } #include "d:\aulas\aposti~1\lingua~1\abb.h" int main(){ int op=1; int inf; struct No *p; raiz=NULL; while(op!=5){ printf("\nARVORE ABB\n\n"); printf("1. Insere na AB\n"); printf("2. Pesquisa na ABB\n"); printf("3. Remove na ABB\n"); printf("4. Lista ABB\n"); printf("5. Sai do programa\n"); scanf("%d",&op); switch(op){ case 1: printf("\nDigite inf. p/ insercao: "); scanf("%d",&inf); InsereABB(&raiz,inf); break; case 2: printf("\nDigite inf. p/ pesquisa: "); scanf("%d",&inf); p = BuscaABB(raiz,inf); if (p){ printf("\nElemento encontrado"); } else{ printf("\nElemento nao encontrado"); } break; case 3: printf("\nDigite inf. p/ remocao: "); scanf("%d",&inf); RemoveABB(&raiz,inf); break; case 4: printf("\nLista ABB"); InOrder(raiz); getch(); break; } } } 54 Digite o exemplo 10.14:- Faça um programa que inclua a TAD “ABB.h” e construa um menu de opções para testá-la. 10.5 Exercícios Árvores Binárias 1. Faça uma função recursiva que devolva a quantidade de nós de uma Árvore Binária. 2. Faça uma função recursiva que devolva a quantidade de folhas de uma Árvore Binária. 3. Faça uma função recursiva que devolva o irmão de um elemento passado como parâmetro. Árvores Binárias de Busca 4. Graficamente, construa uma ABB inserindo os seguintes nós. a) 57, 39, 69, 90, 30, 15, 14, 70, 78, 81, 93,12 b) 5, 40, 52, 57, 89, 30, 51, 70, 2, 31 c) 27, 3, 0, 34, 23, 45, -8, 40, 76, 90, 13, 1 5. Remova os nós 39, 14, 93, 69 da ABB do exercício 4a 6. Remova os nós 30, 2, 5 do exercício 4b 7. Remova os nós 1, 0, 76, 27 do exercício 4c 8. Faça um programa que controle sua agenda de amigos. A agenda guarda as seguintes informações: nome e email. Permita, através de um menu de opções, que o usuário insira amigos, consulte por nome, remova amigos e liste todos eles. Utilize ABB para a implementação e uma TAD Agenda.h contendo a estrutura e as funções para manipulação das informações. 1 Introdução 1.1 Construindo Estruturas Heterogêneas 2 Recursão 2.1 Introdução 2.2 – Exercícios 3 Ponteiro e Alocação dinâmica 3.1 Vetores de Ponteiros 3.2 Matrizes de Ponteiro 1. Indireção Simples 2. Indireção Múltipla 3.3 Exercícios 4 Listas 4.1 Listas Através de Vetores 4.2 Listas através de Ponteiros 4.3 Exercícios 5 Pilha 5.1 Pilha através de Vetores 5.2 Pilha através de Ponteiros 5.3 Exercícios 6 Fila 6.1 Fila através de Vetores 6.2 Fila através de Ponteiros 6.3 Exercícios 7 Métodos de Classificação 7.1 Seleção Estrita 7.2 Troca - Bolha 7.3 Troca – Quick Sort 7.4 Intercalação 7.5 Inserção – Direta 7.6 Inserção - Shell 7.7 Distribuição – classificação por caixas 7.8 Distribuição – classificação por radicais (raízes, Radix) 7.9 Cálculo de Endereço (hashing) 8 Métodos de Busca 8.1 Definição 8.2 Método de Busca Linear ou Seqüencial 8.3 Método de Busca Binária 8.4 Método de Busca Hashing 9 Grafo 9.1 Definições 10 Árvores 10.1 Definições 10.2 Árvores Binárias 10.3 Percursos em Árvores Binárias 10.4 Árvores Binárias de Busca (ABB) 10.5 Exercícios
Compartilhar