Vous êtes sur la page 1sur 89

Universidade Regional Integrada do Alto Uruguai e das Misses - URI Fundao Regional Integrada FuRI Campus de Frederico Westphalen

Departamento de Engenharias e Cincia da Computao Curso de Cincia da Computao

Algoritmos e Estrutura de Dados II

Leandro Rosniak Tibola E-mail: tibola@fw.uri.br Pgina Pessoal: www.fw.uri.br/~tibola

Sumrio
1 Introduo...................................................................................................................................................4 1.1 Tipos Abstratos de Dados...................................................................................................................4 1.2 Objetivos da Estrutura de Dados.........................................................................................................4 1.3 Estruturas de Dados Homogneas.......................................................................................................4 1.3.1 Matrizes de Uma Dimenso ou Vetores......................................................................................4 1.3.2 Matrizes com Mais de Uma Dimenso........................................................................................7 2 Listas Lineares..........................................................................................................................................10 2.1 Fundamentos.....................................................................................................................................10 2.2 Formas de Armazenamento...............................................................................................................11 2.2.1 Alocao Esttica versus Dinmica...........................................................................................11 2.2.2 Alocao Seqencial..................................................................................................................12 2.2.3 Alocao Encadeada..................................................................................................................13 3 Pilhas.........................................................................................................................................................15 3.1 Organizao de Dados na pilha.........................................................................................................16 3.1.1 Declarando uma pilha................................................................................................................16 3.1.2 Inicializando uma pilha.............................................................................................................16 3.1.3 Verificando limites da pilha......................................................................................................17 3.1.4 Inserindo elementos na pilha.....................................................................................................17 3.1.5 Removendo elementos na pilha.................................................................................................18 3.1.6 Verificando o elemento do topo da pilha...................................................................................20 4 Filas...........................................................................................................................................................22 4.1 Implementao Seqencial de Filas..................................................................................................23 4.1.1 Declarando uma fila...................................................................................................................23 4.1.2 Inicializando uma fila................................................................................................................23 4.1.3 Verificando limites da fila.........................................................................................................24 4.1.4 Inserindo elementos na fila........................................................................................................24 4.1.5 Removendo elementos da fila....................................................................................................26 4.1.6 Problemas na Implementao Seqencial de Filas....................................................................27 4.2 Solucionando os Problemas da Implementao Seqencial.............................................................27 4.3 Implementao Circular para Filas...................................................................................................28 4.3.1 Declarando e inicializando uma fila..........................................................................................28 4.3.3 Verificando limites da fila.........................................................................................................29 4.3.4 Inserindo elementos na fila........................................................................................................29 4.3.5 Removendo elementos da fila....................................................................................................30 5 Classificao por Insero........................................................................................................................33 5.1 Mtodo da Insero Direta................................................................................................................33 5.1.1 Exemplo de Insero Direta......................................................................................................33 5.1.2 Implementao...........................................................................................................................34 5.1.3 Anlise do Desempenho............................................................................................................34 5.3.1 Exemplo Incrementos Decrescentes - Shellsort........................................................................37 5.3.2Implementao............................................................................................................................38 5.3.3Anlise do Desempenho.............................................................................................................38 6 Classificao por Trocas...........................................................................................................................39 6.1 Mtodo da Bolha - Bubblesort.........................................................................................................39 6.1.1 Exemplo.....................................................................................................................................39 2

6.1.2 Implementao...........................................................................................................................40 6.1.3 Anlise do Desempenho............................................................................................................40 6.2 Mtodo da Agitao - Shakesort.......................................................................................................41 6.3 Mtodo do Pente - Combsort............................................................................................................42 6.3.1 Exemplo.....................................................................................................................................42 6.3.2 Implementao...........................................................................................................................43 6.4 Mtodo de Partio e Troca - Quicksort...........................................................................................43 6.4.1 Descrio do Mtodo.................................................................................................................44 6.4.2 Implementao...........................................................................................................................46 6.4.3 Anlise do Desempenho............................................................................................................47 7 Classificao por Seleo..........................................................................................................................50 7.1 Mtodo de Seleo Direta.................................................................................................................50 7.1.1 Exemplo.....................................................................................................................................50 7.1.2 Implementao...........................................................................................................................50 7.1.3Anlise do Desempenho.............................................................................................................51 7.2.1 Implementao...........................................................................................................................54 7.2.2 Anlise do Desempenho............................................................................................................61 7.3 Mtodo de Seleo em rvore Amarrada - Threadedheapsort.........................................................61 8 Classificao por Distribuio de Chaves.................................................................................................65 8.1 Mtodo de Indexao Direta - Radixsort..........................................................................................66 8.1.1 Desempenho..............................................................................................................................70 9 Classificao por Intercalao..................................................................................................................71 9.1 Mtodo da Intercalao Simples - Mergesort...................................................................................71 9.1.1 Implementao...........................................................................................................................72 9.1.2 Anlise do Desempenho............................................................................................................74 10 Listas Ordenadas.....................................................................................................................................76 10.1 Implementao Encadeada para Listas Ordenadas.........................................................................76 10.1.1 A Operao de Insero...........................................................................................................77 10.1.2 A operao de Remoo..........................................................................................................79 10.1.3 A Operao de Pesquisa..........................................................................................................80 10.1.4 Imprimindo uma Lista Ordenada.............................................................................................81 10.1.5 Destruindo uma Lista Ordenada..............................................................................................81 10.2 Tcnicas de Encadeamento.............................................................................................................82 10.2.1 Nodos Cabea e Sentinela.......................................................................................................82 10.3 Encadeamento circular....................................................................................................................85 10.4 Encadeamento Duplo..................................................................................................................87 Bibliografia..................................................................................................................................................89

1 Introduo
Neste captulo so apresentados os conceitos fundamentais para o entendimentos das estruturas de dados abordados na apostila. Para sincronizao com a disciplina de Linguagem de Programao II, nos quatro primeiros captulos sero usados exemplos tanto na linguagem Pascal quanto na linguagem C e nos captulos seguintes na linguagem C.

1.1 Tipos Abstratos de Dados


Um tipo abstrato de dados formado por um conjunto de valores e por uma srie de operaes que podem ser aplicadas sobre ele. Operaes e valores, em conjunto, constituem um modelo matemtico que pode ser empregado para modelar e solucionar problemas do mundo real. Para que possamos realmente aplicar um modelo matemtico na resoluo de problemas por computador, preciso antes transform-lo num tipo de dados concreto, ou simplesmente tipo de dados. A transformao de um tipo de dados abstrato em um tipo de dados concreto chamada implementao. durante o processo de implementao que a estrutura de armazenamento dos valores especificada, e que os algoritmos que desempenharo o papel das operaes so projetados.

1.2 Objetivos da Estrutura de Dados


O estudo das estruturas de dados envolve dois objetivos complementares: Terico: identificar e desenvolver modelos matemticos, determinando que classes de problemas podem ser resolvidos com o uso deles. Prtico: criar representaes concretas de objetos e desenvolver rotinas capazes de atuar sobre estas representaes, de acordo com o modelo considerado. O primeiro objetivo considera um tipo abstrato de dados como um recurso a ser empregado durante a resoluo de problemas em geral; e o segundo considera a implementao deste tipo abstrato de dados como um problema em si, que pode ser resolvido atravs do uso de outros tipos de dados j disponveis, possivelmente predefinidos na linguagem escolhida para a implementao.

1.3 Estruturas de Dados Homogneas


As estruturas de dados homogneas permitem agrupar diversas informaes dentro de uma mesma varivel. Este agrupamento ocorrer obedecendo sempre ao mesmo tipo de dado, e por esta razo que estas estruturas so chamadas homogneas. A utilizao deste tipo de estrutura de dados recebe diversos nomes, como: variveis indexadas, variveis compostas, variveis subscritas, arranjos, vetores, matrizes, tabelas em memria ou arrays. Os nomes mais usados e que utilizaremos para estruturas homogneas so: matrizes (genrico) e vetores (matriz de uma linha e vrias colunas). 1.3.1 Matrizes de Uma Dimenso ou Vetores Este tipo de estrutura em particular tambm denominado por profissionais da rea como matrizes unidimensionais. Sua utilizao mais comum est vinculada criao de tabelas. Caracteriza-se por ser definida uma nica varivel vinculada dimensionada com um 4

determinado tamanho. A dimenso de uma matriz constituda por constantes inteiras e positivas. Os nomes dados s matrizes seguem as mesmas regras de nomes utilizados para indicar as variveis simples. A sintaxe do comando de definio de vetores a seguinte, em Pascal: var <nome_da_varivel> : array [ <coluna_inicial> .. <coluna_final> ] of <tipo_de_dado>; Exemplo: var M : array [1 .. 10] of integer; A sintaxe do comando de definio de vetores a seguinte, em C : <tipo_de_dado> <nome_da_varivel> [ <valor> ]; Exemplo em C: int M [10] ; int m [10] ; char Numero [5]; float numero [12]; Operaes Bsicas com Matrizes do Tipo Vetor Do mesmo modo que acontece com variveis simples, tambm possvel operar com variveis indexadas (matrizes). Contudo no possvel operar diretamente com o conjunto completo, mas com cada um de seus componentes isoladamente. O acesso individual a cada componente de um vetor realizado pela especificao de sua posio na mesma por meio do seu ndice. No exemplo anterior foi definida uma varivel M capaz de armazenar 10 nmero inteiros. Para acessar um elemento deste vetor deve-se fornecer o nome do mesmo e o ndice do componente desejado do vetor (um nmero de 1 a 10, no caso do Pascal e um nmero de 0 9 no caso do C). Para Pascal, por exemplo, M[1] indica o primeiro elemento do vetor, M[2] indica o segundo elemento do vetor e M[10] indica o ltimo elemento do vetor. J, para C, M[0] indica o primeiro elemento do vetor, M[1] indica o segundo elemento do vetor e M[9] indica o ltimo elemento do vetor. Portanto, no possvel operar diretamente sobre vetores como um todo, mas apenas sobre seus componentes, um por vez. Por exemplo, para somar dois vetores necessrio somar cada um de seus componentes dois a dois. Da mesma forma as operaes de atribuio, leitura e escrita de vetores devem ser feitas elemento a elemento. Atribuio de Uma Matriz do Tipo Vetor Tendo como base as instrues primitivas, o comando de atribuio ode ser definido como: Pascal: <nome_da_varivel> [ <ndice> ]:= <expresso> C: <nome_da_varivel> [ <ndice> ] = <expresso> No caso de vetores (variveis indexadas), alm do nome da varivel deve-se necessariamente fornecer tambm o ndice do componente do vetor onde ser armazenado o resultado da avaliao da expresso. Em Pascal temos o comando 'M[1] = 12;' enquanto em C seria 'M[0] := 12;'. 5

Leitura de Dados de Uma Matriz do Tipo Vetor A leitura de um vetor feita passo a passo, um de seus componentes por vez, usando a mesma sintaxe da instruo primitiva da entrada de dados, onde alm do nome da varivel, deve ser explicitada a posio do componente lido: Pascal: read ( <nome_da_varivel> [ <ndice> ] ); read ( M[1] ); C: scanf (''%i'', &<nome_da_varivel> [ <ndice> ] ); scanf( %i , &M[0]); Uma observao importante a ser feita a utilizao da construo ''for'' a fim de efetuar a operao de leitura repetidas vezes, em cada uma delas lendo um determinado componente do vetor. De fato esta construo muito comum quando se opera com vetores, devido necessidade de se realizar uma mesma operao com os diversos componentes dos mesmos. Na verdade, so raras as situaes que se deseja operar isoladamente com um nico componente do vetor. Escrita de Dados de Uma Matriz do Tipo Vetor A escrita de um vetor obedece mesma sintaxe da instruo primitiva de sada de dados e tambm vale lembrar que, alm do nome do vetor, deve-se tambm especificar por meio do ndice o componente a ser escrito: Pascal: write ( <nome_da_varivel> [ <ndice> ] ); Ex.: write (M[1]); C: printf (''%i'', <nome_da_varivel> [ <ndice> ] ); Ex.: printf(%i , M[0]); Remoo de Dados de Uma Matriz do Tipo Vetor Para a remoo de dados de um vetor deve observar uma regra: no podemos tornar vazia uma posio de um vetor que j tenha sido utilizada. Ela ser vazia, para nosso uso, no momento de sua criao, contendo, mesmo assim, um valor relativo a posio de memria utilizada, variando sua contedo de linguagem para linguagem. Para termos certeza de que seu contedo est dentro do conjunto com qual queremos trabalhar, podemos atribuir um valor nulo ao inicializar a varivel, assim: Pascal: <nome_da_varivel> [ <ndice> ] := <valor_nulo> C: <nome_da_varivel> [ <ndice> ] = <valor_nulo> Exemplos de Aplicao de Vetores O espectro de aplicao de vetores em algoritmos muito extenso, mas normalmente os vetores so usados em duas tarefas muito importantes no processamento de dados: pesquisa e classificao. 6

A pesquisa consiste na verificao da existncia de um valor dentro de um vetor. Trocando em midos, pesquisar um vetor consiste em procurar dentre seus componentes um determinado valor. A classificao de um vetor consiste em arranjar seus componentes numa determinada ordem, segundo um critrio especfico. Por exemplo, este critrio pode ser a ordem alfabtica de um vetor de dados caracter, ou ento a ordem crescente ou decrescente para um vetor de dados numricos. H vrios mtodos de classificao, mas o mais conhecido o mtodo da bolha de classificao (Bubble Sort). 1.3.2 Matrizes com Mais de Uma Dimenso Este tipo de estrutura tambm tem sua principal utilizao vinculada criao de tabelas. Caracteriza-se por ser definida uma nica varivel vinculada dimensionada com um determinado tamanho. A dimenso de uma matriz constituda por constantes inteiras e positivas. Os nomes dados s matrizes seguem as mesmas regras de nomes utilizados para indicar as variveis simples. A sintaxe do comando de definio de matrizes de duas dimenses a seguinte: Pascal: var <nome_da_varivel> : array [<linha_inicial> .. <linha_final> , <coluna_inicial> .. <coluna_final> ] of <tipo_de_dado> ; Ex.: var m : array [1 .. 5 , 1 .. 10] of integer; C: <tipo_de_dado> <nome_da_varivel> [<nro_linhas> ] [<nro_colunas> ] ; Ex.: int valor [5] [10]; Tambm possvel definir matrizes com vrias dimenses, por exemplo: Exemplo em Pascal: var N : array [1 .. 4] of integer; O : array [1 .. 4 , 1 .. 50 ] of integer; P : array [1 .. 4, 1 .. 50 , 1 .. 5] of integer; Q : array [1 .. 4, 1 .. 50 , 1 .. 5 , 1 .. 3 ] of integer; R : array [1 .. 4, 1 .. 50 , 1 .. 5 , 1 .. 3 , 1 .. 2 ] of integer; S : array [1 .. 4, 1 .. 50 , 1 .. 5 , 1 .. 3 , 1 .. 2 , 1 .. 2 ] of integer; Exemplo em C: int N int O int P int Q int R int S

[4]; [4] [50]; [4] [50] [5]; [4] [50] [5] [3]; [4] [50] [5] [3] [2]; [4] [50] [5] [3] [2] [2]; 7

A utilidade de matrizes desta forma muito grande. No exemplo acima, cada matriz pode ser utilizada para armazenar uma quantidade maior de informaes: .a matriz N pode ser utilizada para armazenar 4 notas de um aluno. .a matriz O pode ser utilizada para armazenar 4 notas de 50 alunos. .a matriz P pode ser utilizada para armazenar 4 notas de 50 alunos de 5 disciplinas. .a matriz Q pode ser utilizada para armazenar 4 notas de 50 alunos de 5 disciplinas, de 3 turmas. .a matriz R pode ser utilizada para armazenar 4 notas de 50 alunos de 5 disciplinas, de 3 turmas, de 2 colgios. .a matriz S pode ser utilizada para armazenar 4 notas de 50 alunos de 5 disciplinas, de 3 turmas, de 2 colgios, de 2 cidades. Operaes Bsicas com Matrizes de Duas Dimenses Do mesmo modo que acontece com os vetores, no possvel operar diretamente com o conjunto completo, mas com cada um de seus componentes isoladamente. O acesso individual a cada componente de uma matriz realizado pela especificao de sua posio na mesma por meio do seu ndice. No exemplo anterior foi definida uma varivel 'O' capaz de armazenar 50 nmero inteiros em cada uma das 4 linhas. Para acessar um elemento desta matriz deve-se fornecer o nome da mesma e o ndice da linha e da coluna do componente desejado da matriz (um nmero de 1 a 4 para a linha e um nmero de 1 a 50 para a coluna, neste caso). Em Pascal, por exemplo, O[1,1] indica o primeiro elemento da primeira linha da matriz, O[1,2] indica o segundo elemento da primeira linha da matriz, O[1,50] indica o ltimo elemento da primeira linha da matriz e O[4,50] indica o ltimo elemento da ltima linha da matriz Da mesma forma como vetores, no possvel operar diretamente sobre matrizes como um todo, mas apenas sobre seus componentes, um por vez. Por exemplo, para somar duas matrizes necessrio somar cada um de seus componentes dois a dois. Da mesma forma as operaes de atribuio, leitura e escrita de matrizes devem ser feitas elemento a elemento. Atribuio de Uma Matriz de Duas Dimenses Na atribuio de matrizes, da mesma forma que nos vetores, alm do nome da varivel deve-se necessariamente fornecer tambm o ndice do componente da matriz onde ser armazenado o resultado da avaliao da expresso. O ndice referente ao elemento composto por tantas informaes quanto o nmero de dimenses da matriz. No caso de ter duas dimenses, o primeiro nmero se refere linha e o segundo nmero se refere coluna da matriz em que se encontra a informao: Pascal: <nome_da_varivel> [ <linha> , <coluna> ] := <expresso> Ex.: m[1,1] := 15 ; m[1,10] := 10 ; m[3,5] := 20 ; m[5,10] := 35 ; C: <nome_da_varivel> [ <linha> ] [ <coluna> ] = <expresso> Ex.: m[1][1] = 15 ; m[1][10] = 10 ; m[3] [5] = 20 ; m[5][10] = 35 ;

Leitura de Dados de Uma Matriz de Duas Dimenses A leitura de uma matriz feita passo a passo, um de seus componentes por vez, usando a mesma sintaxe da instruo primitiva da entrada de dados, onde alm do nome da varivel, deve ser explicitada a posio do componente lido: Pascal: read ( <nome_da_varivel> [ <ndice_1>, ... , < ndice_n> ] ); Ex.: read ( O [2,4] ); C: scanf (''%i'', &<nome_da_varivel> [ <ndice_1> ] [ .. ] [ <ndice_n> ] ); Ex.: scanf(%i , &O[2][4]); Uma observao importante a ser feita a utilizao de construes Para aninhadas ou encadeada a fim de efetuar a operao de leitura repetidas vezes, em cada uma delas lendo um determinado componente da matriz. Esta construo muito comum quando se opera com matrizes, devido necessidade de se realizar uma mesma operao com os diversos componentes das mesmas. Escrita de Dados de Uma Matriz de Duas Dimenses A escrita de uma matriz obedece mesma sintaxe da instruo primitiva de sada de dados e tambm vale lembrar que, da mesma forma que com vetores, alm do nome da matriz, deve-se tambm especificar por meio do ndice o componente a ser escrito: Pascal: write ( <nome_da_varivel> [ <ndice_1> , ... , <ndice_n> ] ); Ex.: write ( O [2,4] ); C: printf (''%i'', <nome_da_varivel> [ <ndice_1> ] [ .. ] [ <ndice_n> ] );

Ex.: print (%i , O[2] [4]);

2 Listas Lineares
Uma das formas mais comumente usadas para se manter dados agrupados a lista. Afinal, quem nunca organizou uma lista de compras antes de ir ao mercado, ou ento uma lista dos amigos que participaro da festa? As listas tm-se mostrado um recurso bastante til e eficiente no dia-a-dia das pessoas. Em computao, no tem sido diferente: a lista uma das estruturas de dados mais empregadas no desenvolvimento de programas.

2.1 Fundamentos
Uma lista linear uma coleo L: [a1, a2, ... an ], n .>= 0, cuja propriedade estrutural baseia-se apenas na posio relativa dos elementos, que so dispostos linearmente. Se n = 0 dizemos que a lista vazia; caso contrrio, so vlidas as seguintes propriedades: a1 o primeiro elemento de L; an o ltimo elemento de L; ak , 1 < k < n, precedido pelo elemento ak - 1 e seguido pelo elemento ak + 1 em L. Em outras palavras, a caracterstica fundamental de uma lista linear o sentido de ordem unidimensional dos elementos que a compem. Uma ordem que nos permite dizer com preciso onde a coleo inicia-se e onde termina, sem possibilidade de dvida. Entre as diversas operaes que podemos realizar sobre listas, temos: acessar um elemento qualquer da lista; inserir um elemento numa posio especfica da lista; remover um elemento de uma posio especfica da lista; combinar duas listas em uma nica; particionar uma lista em duas; obter cpias de uma lista; determinar o total de elementos na lista; ordenar os elementos na lista procurar um determinado elemento na lista; apagar uma lista; outras ... Considerando-se somente as operaes de acesso, insero e remoo, restritas aos extremos da lista, temos casos especiais que aparecem muito freqentemente na modelagem de problemas a serem resolvidos por computador. Tais casos especiais recebem tambm nomes especiais: Pilha: lista linear onde todas as inseres, remoes e acessos so realizados em um nico extremo. Listas com esta caracterstica so tambm denominadas lista LIFO (Last-In/FirstOut) ou em portugus: UEPS (ltimo que Entra/Primeiro que Sai). Fila: lista linear onde todas as inseres so feitas num certo extremo e todas as remoes e acessos so realizados no outro extremo. Filas so tambm denominadas listas FIFO (First-In/First-Out) ou em portugus: PEPS (Primeiro que Entra/Primeiro que Sai). Fila Dupla: lista linear onde as inseres, remoes ou acessos so realizados em qualquer extremo. Filas Duplas so tambm denominadas DEQUE (Double-Ended QUEue), ou em portugus: fila de extremidade dupla. Uma Fila Dupla pode ainda gerar dois casos especiais: Fila Dupla de Entrada Restrita (se a entrada for restrita a um nico extremo) e Fila Dupla de Sada Restrita (se a remoo for restrita a um nico extremo). 10

2.2 Formas de Armazenamento


Ao desenvolver uma implementao para listas lineares, o primeiro problema que surge : como podemos armazenar os elementos da lista, dentro do computador? Sabemos que antes de executar um programa, o computador precisa carregar seu cdigo executvel para a memria. Da rea de memria que reservada para o programa, uma parte usada para armazenar as instrues a serem executadas e a outra destinada ao armazenamento dos dados. Quem determina quanto de memria ser usado para as instrues o compilador. Alocar rea para armazenamento de dados entretanto, responsabilidade do programador. A alocao de memria, do ponto de vista do programador, estar em uma das quatro categorias apresentadas a seguir: Seqencial Esttica Dinmica Esttica Seqencial Encadeada Esttica Encadeada

Dinmica Seqencial Dinmica Encadeada


Figura 2.1: Tipos de alocao de memria.

2.2.1 Alocao Esttica versus Dinmica Dizemos que as variveis de um programa tm alocao esttica se a quantidade total de memria utilizada pelos dados previamente conhecida e definida de modo imutvel, no prprio cdigo-fonte do programa. Durante toda a execuo, a quantidade de memria utilizada pelo programa no varia. Por outro lado, se o programa capaz de criar novas variveis enquanto executa, isto , se as reas de memria, que no foram declaradas no programa, passam a existir durante a sua execuo, ento dizemos que a alocao dinmica. Considerando que uma varivel nada mais que uma rea de memria, dizemos que sua alocao esttica se sua existncia j era prevista no cdigo do programa; de outra forma, dizemos que sua alocao dinmica. A seguir, vemos o uso de varivel esttica em contraste com varivel dinmica: { Alocao: mostra uso de variveis estticas X dinmicas em Pascal}
program alocacao; type var P: ptr; begin new(P); P^ := 12345; writeln(P^); end. ptr = ^integer; {aloca varivel estaticamente} {aloca varivel dinamicamente}

{ Alocao: mostra uso de variveis estticas X dinmicas em C}


# # # # include include include include <stdio.h> <string.h> <stdlib.h> <conio.h>

int var_int, *pt_int; char var_char, *pt_char; float var_float, *pt_float; int main() {

11

pt_int = (int *) malloc(sizeof(int)); pt_char = (char *) malloc(sizeof(char)); pt_float= (float *) malloc(sizeof(float)); var_int = 8 ; *pt_int = 9 ; var_char = 'a' ; *pt_char = 'A' ; var_float = 2.2 ; *pt_float = 3.3 ; printf("<<< Variaveis estaticas e dinamicas >>>\n"); printf("var_int : %i\n", var_int); printf("var_char : %c\n", var_char); printf("var_float: %f\n", var_float); printf("\nvalor - posicao\n"); printf("*pt_int : %i - &pt_int : %p\n", *pt_int, &pt_int); printf("*pt_char : %c - &pt_char : %p\n", *pt_char, &pt_char); printf("*pt_float: %f - &pt_float: %p\n", *pt_float, &pt_float); free(pt_int); free(pt_char); free(pt_float); scanf("%c",&var_char); return(0); }

memria usada pelo programa P : 1FFA

Endereo
12345 memria livre no sistema

1FFA

Figura 2.2: Alocao esttica e dinmica.

Olhando para o texto do programa 'alocao', identificamos a presena das variveis var_int, var_char e var_float. Dizemos, portanto, que estas so variveis alocadas estaticamente (cuidado para no confundir este conceito de varivel esttica/dinmica com aquele de varivel esttica/automtica existente em linguagens como ''C''). Entretanto, observando a figura 2.2, que representa a memria no momento da execuo, verificamos que mais uma varivel foi criada na posio de memria 1FFA. Esta uma varivel alocada dinamicamente (s vezes denominada varivel annima, pois no conhecemos seu nome, temos apenas seu endereo!), como o caso das variveis pt_int, pt_char e pt_float. Em C, as variveis dinmicas so alocadas atravs de diversos comandos, entre eles o comando malloc(). Este comando recebe como argumento o nmero de bytes de memria a serem alocados, verifica se existe uma rea dc memria disponvel e, se existir, aloca esta rea. Para liberar uma rea previamente alocada pelo comando malloc(), usamos o comando free(). Os atributos seqencial e encadeada s fazem sentido se a memria considerada for armazenar uma coleo de objetos, como o caso da memria necessria para armazenar uma lista linear, e sero discutidos a seguir. 2.2.2 Alocao Seqencial A forma mais natural de armazenar uma lista dentro do computador consiste em colocar os seus elementos em clulas de memria consecutivas, um aps outro. Assim, se cada clula tem um endereo nico E e utiliza k bytes, temos: 12

...

E-k ai-1

Figura 2.3: Esquema de alocao seqencial.

ai k

E+k ai+1

...

Endereo(ai ) = E ; Endereo(ai -1) = E-k ; Endereo(ai +1) = E+k . De forma um pouco mais genrica, assumindo que o elemento a1 encontra-se na clula de endereo B, temos a equao: Endereo(ai ) = B + (i-1)k. B+0k a1 B+1k a2 B+2k a3 B+(i-1)k ai k B+(i-1)k an

...

...

...

Figura 2.4: Esquema de acesso seqencial.

A maior vantagem no uso de uma rea seqencial de memria para armazenar uma lista linear que, dado o endereo inicial B da rea alocada e o ndice i de um elemento qualquer da lista, podemos acess-lo imediatamente, com um simples e rpido clculo. O ponto fraco desta forma de armazenamento aparece quando precisamos inserir ou suprimir elementos do meio da lista, quando ento um certo esforo ser necessrio para movimentar os elementos, de modo a abrir espao para insero, ou de modo a ocupar o espao liberado por um elemento que foi removido. 2.2.3 Alocao Encadeada Ao invs de manter os elementos agrupados numa rea contnua de memria, isto , ocupando clulas consecutivas, na alocao encadeada os elementos podem ocupar quaisquer clulas (no necessariamente consecutivas) e, para manter a relao de ordem linear, juntamente com cada elemento armazenado o endereo do prximo elemento da lista. Desta forma, na alocao encadeada, os elementos so armazenados em blocos de memria denominados nodos, sendo que cada nodo composto por dois campos: um para armazenar dados e outro para armazenar endereo. Dois endereos especiais devem ser destacados (como visto na figura 2.5): endereo do primeiro elemento da lista (L); endereo do elemento fictcio que segue o ltimo elemento da lista (nil). A alocao encadeada apresenta como maior vantagem a facilidade de inserir ou remover elementos do meio da lista. Como os elementos no precisam estar armazenados em posies consecutivas de memria, nenhum dado precisa ser movimentado, bastando atualizar o campo de ligao do elemento que precede aquele inserido ou removido. Por exemplo, para remover o elemento a2 da lista representada na figura 2.5, basta mudar o nodo no endereo 3FFA de (a1,1C34) para (a1,BD2F). Como apenas o primeiro elemento acessvel diretamente atravs do endereo L, a grande desvantagem da alocao encadeada surge quando desejamos acessar uma posio especfica dentro da lista. Neste caso, levemos partir do primeiro elemento e ir seguindo os campos de ligao, um a um, at atingir a posio desejada. Obviamente, para listas extensas, esta operao pode ter um alto custo em relao a tempo.

13

Endereo L=3FFA 1C34 BD2F ... 1000 ... 5670 14F6 5D4a

a1 a2 a3 ... ai

Contedo 1C34 BD2F AC12 ... 3A7B ... 14F6 5D4A nil

Primeiro elemento, acessvel a partir de L Note que o segundo elemento no ocupa um endereo consecutivo quele ocupado por a1 Cada nodo armazena um elemento e o endereo do prximo elemento da lista

... an-2 an-1 an

Figura 2.5: Esquema de alocao encadeada.

ltimo elemento da cadeia, o endereo nulo (nil) indica que o elemento an , no tem um sucessor

14

3 Pilhas
A pilha uma das estrutura de dados mais teis em computao. Uma infinidade de problemas clssicos da rea podem ser resolvidos com o uso delas. Uma pilha um tipo especial de lista linear em que todas as operaes de insero e remoo so realizadas numa mesma extremidade, denominada topo. Cada vez que um novo elemento deve ser inserido na pilha, ele colocado no seu topo; e em qualquer momento, apenas aquele posicionado no topo da pilha pode ser removido. Devido a esta disciplina de acesso, os elementos so sempre removidos numa ordem inversa quela em que foram inseridos, de modo que o ltimo elemento que entra exatamente o primeiro que sai. Da o fato destas listas serem tambm denominadas listas LIFO (Last-In/FirstOut) ou em portugus: UEPS (ltimo que Entra/Primeiro que Sai). Uma pilha ou lista LIFO uma coleo que pode aumentar ou diminuir durante a sua existncia. O nome pilha advm justamente da semelhana entre o modo como as listas LIFO crescem e diminuem e o modo como as pilhas, no mundo real, funcionam. O exemplo mais comum do quotidiano uma pilha de pratos, onde o ltimo prato colocado o primeiro a ser usado (removido da pilha). Uma pilha suporta 3 operaes bsicas, tradicionalmente denominadas como: Top (Visualizar o Topo): acessa o elemento posicionado no topo da pilha; Push (Inserir): insere um novo elemento no topo da pilha; Pop (Retirar): remove um elemento do topo da pilha. Sendo P uma pilha e x um elemento qualquer, a operao Push ( P, x ) aumenta o tamanho da pilha P , acrescentando o elemento x no seu topo. A operao Pop ( P ) faz com que a pilha diminua, removendo e retornando o elemento existente em seu topo. Das trs operaes bsicas, a nica que no altera o estado da pilha Top ( P ); ela simplesmente retorna uma cpia do elemento existente no topo da pilha sem remov-lo. Exerccio 1: Dadas as operaes sobre uma pilha P, preencha o quadro a seguir com estado da pilha e o resultado de cada operao: Operao Push (P, 10) Push (P, 15) Pop ( P ) Top ( P ) Pop ( P ) Push ( P, 5) Push ( P, 8) Pop ( P ) Push ( P, 13) Top ( P ) Pop ( P ) Pop ( P ) Push ( P, 3 ) Top ( P ) Estado da Pilha P: [] P: [ 10 ] P: [ 10, 15 ] P: [ 10 ] P: [ 10 ] Resultado 15 10

15

3.1 Organizao de Dados na pilha


Como os elementos da pilha so armazenados em seqncia, um sobre o outro, e a incluso ou excluso de elementos no requer movimentao de dados, o esquema de alocao seqencial de memria mostra-se bastante apropriado para implement-las. Neste esquema, a forma mais simples de se represntar uma pilha na memria consiste em : um vetor, que serve para armazenar os elementos contidos na pilha; um ndice, utilizado para indicar a posio corrente de topo da pilha. Nota que vetor o mecanismo oferecido pelas linguagen de programao nos premite alocar, de forma esttica, uma rea seqencial de memria, e o ndice o recurso necessrio para disciplinar o acesso aos elementos da pilha. 3.1.1 Declarando uma pilha Para declarar uma pilha, precisamos ento de duas variveis: um vetor com o tamanho necessrio para armazenar as informaes e uma varivel que indica o topo, como no exemplo abaixo, que implementa uma pilha de tamanho mximo igual a 10: Pascal:
Program Pilha; const MAX = 10; Var V: Array[1..MAX] of integer; topo: integer;

C:
#include <conio.h> #include <stdio.h> #include <stdlib.h> #define MAX 10 int pilha[MAX]; char x[5]; int topo; ...

3.1.2 Inicializando uma pilha Para inicializar uma pilha, deve-se inicializar o topo (no caso do C, usar um valor abaixo da primeira posio til do vetor, que a posio 0 - zero): Pascal:
Program Pilha; const MAX = 10; var V: array[1..MAX] of integer; topo: integer; begin topo := 0; ...

C:
#include <conio.h> #include <stdio.h>

16

#include <stdlib.h> #define MAX 10 int pilha[MAX]; char x[5]; int topo, op, nro, i; int main() { topo :=-1; op=0; nro=0; i=0; ...

3.1.3 Verificando limites da pilha Uma insero s pode ser feita na pilha se o topo for menor que o seu tamanho mximo, caso contrrio ocorre o que se chama de estouro de pilha ou stack overflow. Da mesma forma, uma retirada da pilha s pode ser feita se o topo for maior que zero, caso contrrio ocorre o que se chama de stack underflow. Para verificar se uma pilha est cheia, basta testar se o topo igual ao seu tamanho mximo. O teste para verificar se pode ser feita uma insero numa pilha que tem tamanho mximo igual a 10 mostrado no exemplo abaixo: Pascal:
if topo = MAX then begin writeln('Pilha cheia!'); end;

C:
if ( topo == MAX-1 ) printf("\nOverflow - pilha cheia !!!") ;

Para verificar se uma pilha est vazia, basta testar se o topo inferior a zero. O teste para verificar se pode ser feita uma retirada mostrado no exemplo abaixo: Pascal:
if topo = 0 then begin writeln('Pilha vazia!'); end;

C:
if ( topo == -1 ) printf("\nUnderflow - pilha vazia !!!");

3.1.4 Inserindo elementos na pilha Para inserir um elemento na pilha, precisamos saber qual o elemento a ser inserido e testar se a pilha est cheia. Se a mesma no est cheia, incrementamos o topo e na posio do topo inserir o novo elemento. O exemplo abaixo ilustra a insero de um elemento na pilha: Pascal:
Program Pilha; Uses Crt; const MAX = 10; Var

17

V: Array[1..MAX] of integer; topo: integer; x: integer; Begin topo := 0; write('Valor a inserir: '); readln (x); if topo = MAX then begin writeln('Pilha cheia!'); end else begin topo := topo + 1; V[topo] := x; end; ... end.

C:
#include <conio.h> #include <stdio.h> #include <stdlib.h> #define MAX 10 int pilha[MAX]; char x[5]; int topo, op, nro, i; int main() { topo=-1 ; op=0; nro=0; i=0; do { clrscr(); printf("\n1 - Inserir\n"); printf("2 - Retirar\n"); printf("3 - Consultar topo\n"); printf("4 - Mostrar pilha\n"); printf("9 - Sair\n"); printf("Escolha uma opcao -> "); scanf("%d", &op); if (op == 1 ) { printf("\nDigite um numero inteiro e positivo: "); scanf("%d",&nro); if ( topo == MAX-1 ) printf("\nOverflow - pilha cheia !!!") ; else { topo++; pilha[topo] = nro; printf("\nO valor %d foi inserido na posicao % d",nro,topo); } fflush(stdin); gets(x); } ...

3.1.5 Removendo elementos na pilha Para remover um elemento na pilha, precisamos saber se a pilha no est vazia. Se a mesma no est vazia, mostra-se o valor que est no vetor na posio apontada pelo topo e decrementa-se o topo. O exemplo abaixo ilustra a remoo de um elemento da pilha: 18

Pascal:
Program Pilha; Uses Crt; const MAX = 10; Var V: Array[1..MAX] of integer; topo: integer; x: integer; Begin topo := 0; write('Valor a inserir: '); readln (x); if topo = MAX then begin writeln('Pilha cheia!'); end else begin topo := topo + 1; V[topo] := x; end; if topo = 0 then begin writeln('Pilha vazia!'); end else begin writeln('O valor retirado : ', V[topo]); topo := topo - 1; end; end.

C:
#include <conio.h> #include <stdio.h> #include <stdlib.h> #define MAX 10 int pilha[MAX]; char x[5]; int topo, op, nro, i; int main() { topo=-1; op=0; nro=0; i=0; do { clrscr(); printf("\n1 - Inserir\n"); printf("2 - Retirar\n"); printf("3 - Consultar topo\n"); printf("4 - Mostrar pilha\n"); printf("9 - Sair\n"); printf("Escolha uma opcao -> "); scanf("%d", &op); if (op == 1 ) { printf("\nDigite um numero inteiro e positivo: "); scanf("%d",&nro); if ( topo == MAX-1 ) printf("\nOverflow - pilha cheia !!!") ; else { topo++; pilha[topo] = nro; printf("\nO valor %d foi inserido na posicao % d",nro,topo);

19

} fflush(stdin); gets(x); } if ( op == 2 ) { if ( topo == -1 ) printf("\nUnderflow - pilha vazia !!!"); else { topo-- ; printf("\nValor %d retirado com sucesso.",pilha[topo+1] ) ; } fflush(stdin); gets(x); }

3.1.6 Verificando o elemento do topo da pilha Para verificar o elemento que est no topo da pilha, precisamos saber se a pilha no est vazia. Se a mesma no est vazia, mostra-se o valor que est no vetor na posio apontada pelo topo sem decrementar o topo. O exemplo abaixo ilustra a verificao do elemento que est no topo da pilha: Pascal:
Program Pilha; Uses Crt; const MAX = 10; Var V: Array[1..MAX] of integer; topo: integer; x: integer; Begin topo := 0; write('Valor a inserir: '); readln (x); if topo = MAX then begin writeln('Pilha cheia!'); end else begin topo := topo + 1; V[topo] := x; end; if topo = 0 then begin writeln('Pilha vazia!'); end else begin writeln('O valor que est no topo : ', V[topo]); end; end.

C:
#include <conio.h> #include <stdio.h> #include <stdlib.h> #define MAX 10 int pilha[MAX]; char x[5]; int topo, op, nro, i;

20

int main() { topo=-1; op=0; nro=0; i=0; do { clrscr(); printf("\n1 - Inserir\n"); printf("2 - Retirar\n"); printf("3 - Consultar topo\n"); printf("4 - Mostrar pilha\n"); printf("9 - Sair\n"); printf("Escolha uma opcao -> "); scanf("%d", &op);

if (op == 1 ) { printf("\nDigite um numero inteiro e positivo: "); scanf("%d",&nro); if ( topo == MAX-1 ) printf("\nOverflow - pilha cheia !!!") ; else { topo++; pilha[topo] = nro; printf("\nO valor %d foi inserido na posicao % d",nro,topo); } fflush(stdin); gets(x); } if ( op == 2 ) { if ( topo == -1 ) printf("\nUnderflow - pilha vazia !!!"); else { topo-- ; printf("\nValor %d retirado com sucesso.",pilha[topo+1] ) ; } fflush(stdin); gets(x); } if ( op == 3 ) { if ( topo == -1 ) printf("\nImpossivel mostrar o topo, a pilha esta printf("\nValor %d no posicao %d .",pilha[topo],topo); fflush(stdin); gets(x); } else

vazia.");

Exerccios Utilizando procedimentos e/ou funes, fazer um programa em Pascal ou C que manipule uma pilha. O programa deve ter um menu principal com as seguintes opes: 1 - Inserir elementos na pilha 2 - Retirar elementos na pilha 3 - Verificar o elemento que est no topo da pilha 4 - Mostrar todos os elementos da pilha. 5 - Sair 21

4 Filas
Uma fila um tipo especial de lista linear em que as inseres so realizadas em um extremo, ficando as remoes restritas ao outro. O extremo onde os elementos so inseridos denominado final da fila, e aquele de onde so removidos denominado comeo da fila. Cada vez que uma operao de insero executada, um novo elemento colocado no final da fila. Na remoo, sempre retornado o elemento que aguarda h mais tempo na fila, ou seja, aquele posicionado no comeo. A ordem de sada corresponde diretamente ordem de entrada dos elementos na fila, de modo que os primeiros elementos que entram so sempre os primeiros a sair. Por este motivo, as filas so denominadas listas FIFO (First-In/First-Out) ou PEPS (Primeiro que Entra/Primeiro que Sai). Um exemplo bastante comum de filas verifica-se num balco de atendimento, onde pessoas formam uma fila para aguardar at serem atendidas. Naturalmente, devemos desconsiderar os casos de pessoas que furam a fila ou que desistem de aguardar! Diferentemente das filas no mundo real, o tipo abstrato de dados no suporta insero nem remoo no meio da lista.
Comeo Sada <= Final

O O O O O O O O O O O O O O O O O O O O O o o o o o o o <= Entrada
Figura : 4.1: Funcionamento de uma fila.

A palavra queue, da lngua inglesa, significa fila. Por tradio, as operaes bsicas que uma fila suporta so: Enqueue (Inserir): insere um novo elemento no final da fila; Dequeue (Retirar): remove um elemento do comeo da fila. Sendo F uma fila e x um elemento qualquer, a operao Enqueue ( F, x ) aumenta o tamanho da fila F , acrescentando o elemento x no seu final. A operao Dequeue ( F ) faz com que a fila diminua, removendo e retornando o elemento existente em seu comeo. Exerccio 1: Dadas as operaes sobre uma fila F, preencha o quadro a seguir com estado da fila e o resultado de cada operao: Operao Estado da Fila Resultado F: [ ] Enqueue ( F, 10 ) F: [ 10 ] Enqueue ( F, 15 ) F: [ 10, 15 ] Dequeue ( F ) F: [ 15 ] 10 Enqueue ( F, 5 ) Enqueue ( F, 8 ) Dequeue ( F ) Enqueue ( F, 13 ) Dequeue ( F ) Dequeue ( F ) Enqueue ( F, 21 ) Enqueue ( F, 50 ) Dequeue ( F ) Dequeue ( F ) Dequeue ( F )

22

4.1 Implementao Seqencial de Filas


Graficamente, representamos uma fila como uma coleo de objetos que cresce da esquerda para a direita, com dois extremos bem definidos: comeo e final: Final Comeo F: a b c d ...

Figura 4.2: Representao grfica de uma fila F:[a, b, c, d]

A partir da representao grfica percebemos que possvel implementar uma fila tendo trs recursos bsicos: espao de memria para armazenas os elementos (um vetor); uma referncia ao primeiro elemento da coleo (uma varivel); uma referncia primeira posio livre, aps o ltimo elemento da fila (uma varivel). 4.1.1 Declarando uma fila Para declarar uma fila, precisamos ento de trs variveis: um vetor com o tamanho necessrio para armazenar as informaes; uma varivel que indica o comeo da fila e uma varivel que indica o final da fila, como no exemplo abaixo, que implementa uma fila de tamanho mximo igual a 10: Pascal:
Program Fila; const MAX = 10; Var V: Array[1..MAX] of integer; comeco, final : integer;

C:
#include <stdio.h> #include <stdlib.h> #include <conio.h> #define MAX 10 int fila[MAX]; char x[5];

4.1.2 Inicializando uma fila

Para inicializar uma fila, deve-se atribuir o valor 1 s variveis que controlam o comeo e o final da fila:
Pascal:
Program Fila; const MAX = 10; Var V: Array[1..10] of integer; comeco, final : integer;

23

Begin comeco := 1; final := 1; ...

C:
#include <stdio.h> #include <stdlib.h> #include <conio.h> #define MAX 10 int fila[MAX]; char x[5]; int main() { int inicio=0, fim=0, op=0, nro=0, i=0;

4.1.3 Verificando limites da fila Uma insero s pode ser feita na fila se a fila no estiver cheia (final for menor que o seu tamanho mximo). Da mesma forma, uma retirada da fila s pode ser feita se existem elementos na fila (o comeo for diferente do final). Para verificar se uma fila est cheia, basta testar se o final maior que o seu tamanho mximo. O teste para verificar se pode ser feita uma insero numa fila que tem tamanho mximo igual a 10 mostrado no exemplo abaixo: Pascal:
if final > MAX then begin writeln('Fila cheia!'); end;

C:
if (fim == MAX) printf("Overflow - fila cheia !!!") ;

Para verificar se uma fila est vazia, basta testar se o comeo igual ao final. O teste para verificar se pode ser feita uma retirada mostrado no exemplo abaixo: Pascal:
if comeco = final then begin writeln('Fila vazia!'); end;

C:
if (inicio == fim) printf("Underflow - fila vazia !!!");

4.1.4 Inserindo elementos na fila Para inserir um elemento na fila, precisamos saber qual o elemento a ser inserido e testar se a fila est cheia. Se a mesma no est cheia, o elemento inserido na posio apontada pela varivel final, que deve ser incrementada depois da insero. O exemplo abaixo ilustra a insero de um elemento na fila: 24

Pascal:
Program Fila; const MAX = 10; Var V: Array[1..MAX] of integer; comeco, final : integer; x: integer; Begin comeco := 1; final := 1; write('Valor a inserir: '); readln (x); if final > MAX then begin writeln('Fila cheia!'); end else begin V[final] := x; final := final + 1 end; ... end.

C:
#include <stdio.h> #include <stdlib.h> #include <conio.h> #define MAX 10 int fila[MAX]; char x[5]; int main() { int inicio=0, fim=0, op=0, nro=0, i=0; { clrscr(); printf("\n<<< Fila >>> \n"); printf("1 - Inserir\n"); printf("2 - Retirar\n"); printf("3 - Mostrar fila\n"); printf("9 - Sair\n"); printf("Escolha uma opcao -> "); scanf("%d", &op); if ( op == 1 ) { if (fim == MAX) printf("Overflow - fila cheia !!!") ; else { printf("Digite um numero inteiro e positivo: "); scanf("%d",&nro); fila[fim]=nro; fim++; printf("O valor %d foi inserido na posicao % d",nro,fim); } fflush(stdin); gets(x); } do

25

4.1.5 Removendo elementos da fila Para remover um elemento da fila, precisamos saber se a fila no est vazia. Se a mesma no est vazia, mostra-se o valor que est no vetor na posio apontada pelo comeco e incrementa-se o comeo. O exemplo abaixo ilustra a remoo de um elemento da fila: Pascal:
Program Fila; const MAX = 10; Var V: Array[1..MAX] of integer; comeco, final : integer; x: integer; Begin comeco := 1; final := 1; write('Valor a inserir: '); readln (x); if final > MAX then begin writeln('Fila cheia!'); end else begin V[final] := x; final := final + 1 end; if comeco = final then begin writeln('Fila vazia!'); end else begin writeln('O valor retirado : ', V[comeco]); comeco := comeco + 1; end; end.

C:
#include <stdio.h> #include <stdlib.h> #include <conio.h> #define MAX 10 int fila[MAX]; char x[5]; int main() { int inicio=0, fim=0, op=0, nro=0, i=0; do { clrscr(); printf("\n<<< Fila >>> \n"); printf("1 - Inserir\n"); printf("2 - Retirar\n"); printf("3 - Mostrar fila\n"); printf("9 - Sair\n"); printf("Escolha uma opcao -> "); scanf("%d", &op); if ( op == 1 ) {

26

d",nro,fim);

if (fim == MAX) printf("Overflow - fila cheia !!!") ; else { printf("Digite um numero inteiro e positivo: "); scanf("%d",&nro); fila[fim]=nro; fim++; printf("O valor %d foi inserido na posicao % } fflush(stdin); gets(x);

if ( op == 2 ) { if (inicio == fim) printf("Underflow - fila vazia !!!"); else { fila[inicio]=0; inicio++; printf("Valor retirado com sucesso.") ; } fflush(stdin); gets(x); }

4.1.6 Problemas na Implementao Seqencial de Filas Vimos que cada vez que um elemento removido, o ndice que aponta o comeo da fila desloca-se uma posio direita (incrementado). Supondo que a fila tenha um tamanho mximo de 10, aps 10 inseres o ponteiro que aponta para o final da fila tem o valor 11 e a fila estar cheia, pois o final maior que o tamanho mximo representa fila cheia, no podendo ser inserido mais nenhum elemento. Aps 10 retiradas, o ponteiro que aponta para o comeo da fila ter o valor 11 e a fila estar vazia, pois o comeo igual ao final representa fila vazia. Resumindo, chegamos a uma situao extremamente indesejvel. Temos uma fila cheia e vazia ao mesmo tempo. Afinal como isto possvel? Chegamos concluso de que esta nossa implementao no muito eficiente, apresentando tanto desperdcio de memria quanto problemas de lgica.

4.2 Solucionando os Problemas da Implementao Seqencial


Eliminar o erro lgico, que sinaliza fila vazia e cheia ao mesmo tempo, bastante simples. Basta acrescentar uma varivel contadora para indicar quantos elementos esto armazenados na fila. Esta varivel deve ser inicialmente zerada. Quando um elemento for inserido, ela ser incrementada; quando for removido, ela ser decrementada. Desta forma, o impasse pode ser resolvido simplesmente consultando essa varivel. Para eliminar o desperdcio de memria, o ideal seria que cada posio liberada por um elemento removido se tornasse prontamente disponvel para receber um novo elemento inserido. Para isso teramos de dispor de uma rea seqencial de memria tal que a posio 1 estivesse imediatamente aps a ltima posio. Assim, a fila somente estaria cheia, quando realmente tivesse um elemento para cada posio.

27

1 a ... b c Final e 5 d 4 2

Comeo

Figura 4.2: Representao de uma fila circular.

4.3 Implementao Circular para Filas


A partir da representao grfica percebemos que possvel implementar uma fila circular tendo quatro recursos bsicos: espao de memria para armazenas os elementos (um vetor); uma referncia ao primeiro elemento da coleo (uma varivel); uma referncia primeira posio livre, aps o ltimo elemento da fila (uma varivel); um indicador da quantidade de elementos da fila. 4.3.1 Declarando e inicializando uma fila Para declarar uma fila, precisamos ento de quatro variveis: um vetor com o tamanho necessrio para armazenar as informaes; uma varivel que indica o comeo da fila; uma varivel que indica o final da fila e uma varivel que indica a quantidade de elementos na fila, como no exemplo abaixo, que implementa uma fila de tamanho mximo igual a 10 e inicializa seus respectivos valores, : Pascal:
Program FilaCircular; const MAX = 10; Var V: Array[1..MAX] of integer; qtd, comeco, final : integer; Begin comeco := 1; final := 1; qtd := 0; ...

C:
#include <stdio.h> #include <stdlib.h> #include <conio.h> const MAX=10; void main() { int inicio=0, fim=0, qtd=0, fila[MAX]; int op=0, nro=0;

28

char x[5];

4.3.3 Verificando limites da fila Uma insero s pode ser feita na fila se a fila no estiver cheia, (se a quantidade de elementos for menor que a quantidade mxima). Da mesma forma, uma retirada da fila s pode ser feita se existem elementos na fila (se a quantidade de elementos for maior que zero). Para verificar se uma fila est cheia, basta testar se a varivel contadora igual ao seu tamanho mximo. O teste para verificar se pode ser feita uma insero numa fila que tem tamanho mximo igual a 10 mostrado no exemplo abaixo: Pascal:
if qtd = MAX then begin writeln('Fila cheia!'); end;

C:
if (qtd == MAX) printf("Overflow - fila cheia !!!") ;

Para verificar se uma fila est vazia, basta testar se o comeo igual ao final. O teste para verificar se pode ser feita uma retirada mostrado no exemplo abaixo: Pascal:
if qtd = 0 then begin writeln('Fila vazia!'); end;

C:
if (qtd == 0) printf("Underflow - fila vazia !!!");

4.3.4 Inserindo elementos na fila Para inserir um elemento na fila, precisamos saber qual o elemento a ser inserido e testar se a fila est cheia. Se a mesma no est cheia, o elemento inserido na posio apontada pela varivel final, que deve ser incrementada depois da insero. O exemplo abaixo ilustra a insero de um elemento na fila: Pascal:
Program FilaCircular; const MAX = 10; Var V: Array[1..MAX] of integer; qtd, comeco, final : integer; x: integer; Begin comeco := 1; final := 1; qtd := 0; write('Valor a inserir: '); readln (x); if qtd = MAX then begin writeln('Fila cheia!');

29

else

end begin V[final] := x; qtd := qtd + 1; if final = MAX then begin final := 1 ; end else begin final := final + 1; end; end;

... end.

C:
#include <stdio.h> #include <stdlib.h> #include <conio.h> const MAX=10; void main() { int inicio=0, fim=0, qtd=0, fila[MAX];; int op=0, nro=0; char x[5]; do { clrscr(); printf("1 - Inserir\n"); printf("2 - Retirar\n"); printf("3 - Mostrar fila\n"); printf("9 - Sair\n"); printf("Escolha uma opcao -> "); scanf("%d", &op); if ( op == 1) { if (qtd == MAX) printf("Overflow - fila cheia !!!") ; else { printf("Digite um numero inteiro e positivo: "); scanf("%d",&nro); fila[fim]=nro; qtd++; if (fim == MAX-1) fim=0; else fim++; printf("O valor %d foi inserido.",nro); } fflush(stdin); gets(x);

4.3.5 Removendo elementos da fila Para remover um elemento da fila, precisamos saber se a fila no est vazia. Se a mesma no est vazia, mostra-se o valor que est no vetor na posio apontada pelo comeco e incrementa-se o comeo. O exemplo abaixo ilustra a remoo de um elemento da fila:e 30

Pascal:
Program FilaCircular; const MAX = 10; Var V: Array[1..10] of integer; qtd, comeco, final : integer; x: integer; Begin comeco := 1; final := 1; qtd := 0; write(?Valor a inserir: ?); readln (x); if qtd = MAX then begin writeln('Fila cheia!'); end else begin V[final] := x; qtd := qtd + 1; if final = MAX then begin final := 1 ; end else begin final := final + 1 end; end; if qtd = 0 then begin writeln('Fila vazia!'); end else begin writeln('O valor retirado : ', V[comeco]); qtd := qtd - 1; if comeco = MAX then begin comeco := 1 ; end else begin comeco := comeco + 1; end; end; end.

C:
#include <stdio.h> #include <stdlib.h> #include <conio.h> const MAX=10; void main() { int inicio=0, fim=0, qtd=0, fila[MAX];; int op=0, nro=0; char x[5]; do { clrscr(); printf("1 - Inserir\n");

31

printf("2 - Retirar\n"); printf("3 - Mostrar fila\n"); printf("9 - Sair\n"); printf("Escolha uma opcao -> "); scanf("%d", &op); if ( op == 1) { if (qtd == MAX) printf("Overflow - fila cheia !!!") ; else { printf("Digite um numero inteiro e positivo: "); scanf("%d",&nro); fila[fim]=nro; qtd++; if (fim == MAX-1) fim=0; else fim++; printf("O valor %d foi inserido.",nro); } fflush(stdin); gets(x);

if ( op == 2 ) { if (qtd == 0) printf("Underflow - fila vazia !!!"); else { fila[inicio]=0; qtd--; if (inicio == MAX-1) inicio = 0; else inicio++; printf("Valor retirado com sucesso.") ; } fflush(stdin); gets(x);

Exerccios Utilizando procedimentos e/ou funes, fazer um programa em Pascal ou C que manipule uma fila de nomes de alunos. O programa deve ter um menu principal com as seguintes opes: 1 - Inserir alunos na fila 2 - Retirar alunos da fila 3 - Apresentar todos os alunos da fila 4 - Sair

32

5 Classificao por Insero


A classificao por insero caracterizada pelo princpio no qual os n dados a serem ordenados so divididos em dois segmentos: um j ordenado e outro a ser ordenado. Num momento inicial, o primeiro segmento formado por apenas um elemento, que, portanto, pode ser considerado como j ordenado. O segundo segmento formado pelos restantes n-1 elementos. A partir da, o processo se desenvolve em n-1 iteraes, sendo que em cada uma delas um elemento do segmento no ordenado transferido para o primeiro segmento, sendo inserido em sua posio correta em relao queles que para l j foram transferidos. Os mtodos que pertencem a esta famlia diferem um dos outros apenas pela forma como localizam a posio relativa em que cada elemento deve ser inserido no segmento j ordenado.

5.1 Mtodo da Insero Direta


Neste mtodo, considera-se o segmento j ordenado como sendo formado pelo primeiro elemento do vetor de chaves. Os demais elementos, ou seja do 2 ao ltimo, pertencem ao segmento no ordenado. A partir desta situao inicial, toma-se um a um dos elementos no ordenados, a partir do primeiro e, por busca seqencial realizada da direita para a esquerda no segmento j ordenado, localiza-se a sua posio relativa correta. cada comparao realizada entre o elemento a ser inserido e os que l j se encontram, podemos obter um dos seguintes resultados: a)O elemento a ser inserido menor do que aquele com que se est comparando. Nesse caso, este movido uma posio para a direita, deixando, assim, vaga a posio que anteriormente ocupava. b)O elemento a ser inserido maior ou igual quele com que se est comparando. Nesse caso, fazemos a insero do elemento na posio vaga, a qual corresponde sua posio relativa correta no segmento j ordenado. Caso o elemento a ser inserido seja maior do que todos, a insero corresponde a deix-lo na posio que j ocupava. Aps a insero, a fronteira entre os dois segmentos deslocada uma posio para a direita, indicando, com isto, que o segmento ordenado ganhou um elemento e o no ordenado perdeu um. O processo prossegue at que todos os elementos do segundo segmento tenham sido inseridos no primeiro. 5.1.1 Exemplo de Insero Direta vetor original diviso inicial primeira iterao segunda iterao terceira iterao quarta iterao quinta iterao sexta iterao (vetor ordenado)
Tabela 5.1 Exemplo do mtodo de insero direta.

18 18 15 7 7 7 7 7

15 15 Ordenado 18 15 9 9 9 9

7 7 7 18 15 15 15 14

9 9 9 9 18 18 16 15

23 16 23 16 no ordenado 23 16 23 16 23 16 23 16 18 23 16 18

14 14 14 14 14 14 14 23

33

void insercao_direta(void) { int aux,i,j; for ( j=1; j<MAX; j++) { aux=vetor[j]; i=j-1; while ((i>=0) && (vetor[i]>aux)) { vetor[i+1]=vetor[i]; i--; } vetor[i+1]=aux; } }

5.1.2 Implementao A implementao acima demonstra estritamente a descrio formulada: o segmento j ordenado percorrido da direita para a esquerda, at que seja encontrada uma chave menor ou igual quela que est sendo inserida, ou at que o segmento termine . Enquanto nenhuma dessas condies ocorrer, as chaves comparadas vo sendo deslocadas uma posio para a direita. Na hiptese da chave a ser inserida ser maior do que todas as do segmento ordenado, ela permanece no seu local original, caso contrrio, inserida na posio deixada vaga pelos deslocamentos , avanando-se, a seguir, a fronteira entre os dois segmentos . O processo todo completado em n-1 iteraes. 5.1.3 Anlise do Desempenho O desempenho do mtodo da insero direta fortemente influenciado pela ordem inicial das chaves a serem ordenadas. Isto se deve principalmente ao fato de que as chaves que j esto no vetor ordenado, e que so maiores do que a que est sendo inserida, devem ser transpostas uma posio para a direita, para dar lugar quela que vai ser inserida. Observe que, mesmo se usssemos um mtodo de busca mais eficiente do que a linear, no evitaramos a operao de transposio. A situao mais desfavorvel para o mtodo aquela em que o vetor a ser ordenado se apresenta na ordem contrria desejada. Isto significa que cada elemento a ser inserido sempre ser menor do que todos os que j esto no segmento ordenado. Isto acarreta um deslocamento de todos eles uma posio para a direita. A tabela a seguir mostra a quantidade de comparaes e transposies necessrias para inserir cada uma das chaves:
Tabela 5.2 Quantidade de comparaes e transposies para cada chave.

1 chave : 1 2 chave : 2 3 chave : 3 ... chave : ... (n-1) chave : n-1 O total corresponder soma dos nmeros de operaes efetuadas em cada iterao: 1+2+3+...+(n1), que igual soma dos termos de uma progresso aritmtica cujo primeiro termo 1 e o ltimo (n -1): S = ( 1 + (n+1) ) / 2 * (n - 1) = (n - n) / 2. 34

Por outro lado, o melhor caso para o mtodo aquele no qual as chaves j se apresentam previamente ordenadas. Nesta situao, cada chave a ser inserida sempre ser maior do que aquelas que j o foram. Isto significa que nenhuma transposio necessria. De qualquer maneira, necessria pelo menos uma comparao para localizar a posio da chave. Logo, nesta situao, o mtodo efetuar um total de n-1 iteraes para dar o vetor como ordenado. Podemos supor que em situaes normais o vetor no se apresente j ordenado, nem tampouco com a ordenao inversa quela desejada. E razovel imaginarmos uma situao intermediria entre os casos extremos, supondo que esta corresponda mdia aritmtica entre os dois casos. Assim, o desempenho mdio do mtodo dado por: ( (n - 1) + ( (n - n) / 2 ) ) / 2 = (n + n - 2) / 4 = (n) Podemos tambm usar uma outra abordagem para estimarmos a complexidade do mtodo. Examinaremos o seu desempenho para os casos de haver nenhuma, uma, duas, trs, etc... chaves fora do seu local definitivo. Depois, generalizaremos para um certo valor mdio de chaves fora do lugar. Para cada caso, consideramos dois tipos de comparaes: as necessrias para descobrir a posio correta das chaves que esto fora do seu lugar, e as necessrias para descobrir que as demais chaves j esto no seu local correto. Iremos supor que as chaves fora do local esto a uma distncia mdia de n/2 posies, j que a distncia mnima 1 e a mxima n-1. A quantidade de chaves fora do local poder variar de nenhuma at n-1, j que, mesmo que o vetor se apresente na ordem inversa quela desejada, podemos considerar que pelo menos uma chave se encontra na sua posio correta, enquanto as n-1 demais se encontram deslocadas. A Tabela 5.3 mostra a quantidade de comparaes necessrias em cada caso. A coluna A indica a quantidade de chaves fora do seu local. A coluna B mostra o nmero de comparaes efetuadas para descobrir o local correto de cada chave que est fora do seu lugar. J a coluna C exibe o nmero de comparaes para descobrir que as demais chaves esto no seu local correto.
Tabela 5.3 - Quantidade de comparaes efetuadas em cada caso.

A 0 1 2 3 ... k=n1

B 0 n/2 2*n/2 3*n/2 ... k*n/2

C n1 n2 n3 n4 ... n (k 1)

O total de comparaes para um certo 0 <= i <= k , pois, a soma das comparaes indicadas nas colunas B e C, ou seja: i * n/2 + n (i+1). De fato, para i = 0 e para i = k, temos, respectivamente, n - 1 e (n - n) / 2 comparaes, as quais correspondem ao melhor e ao pior caso do mtodo, tambm respectivamente, na anlise efetuada anteriormente. 35

Considerando, como antes, que o caso mdio corresponde mdia aritmtica entre o melhor e o pior caso, temos: nmero mdio de chaves fora do local = ( 0 + (n - 1)) / 2 = (n1) / 2 nmero mdio de comparaes = (n-1)/2 * (n/2) + n (n-1)/2-1 = (n + n-2)/4 o que corresponde exatamente ao resultado encontrado na anlise anterior. 5.2 Insero Direta com Busca Binria Como mencionamos no incio desta seo, o uso de um mtodo mais eficiente do que o da busca seqencial pode ser usado para localizar a posio na qual uma certa chave deve ser inserida no segmento ordenado. Isto no nos livra, no entanto, da necessidade de efetuarmos os deslocamentos das chaves que so maiores do que aquela que est sendo inserida, para lhe dar lugar. Eventualmente poderamos pensar em organizar o segmento ordenado sob a forma de uma lista encadeada, de tal forma a evitar a necessidade de deslocamentos. A insero seria feita apenas com ajustes de ponteiros. Mas, por outro lado isto nos obrigaria a uma pesquisa seqencial na lista para localizar o ponto de insero. Isto nos deixa num impasse. Assim, mesmo melhorando o tempo de localizao do ponto de insero, ficaremos ainda com o nus dos elevados tempos de deslocamento. Supondo, no entanto, que mesmo assim optemos por esta soluo, vejamos ento qual o seu comportamento. Sabemos que o nmero de comparaes necessrias para localizar a posio que um elemento deve ocupar em uma tabela que contm k smbolos, por pesquisa binria, aproximadamente log2 k. No nosso caso, esta operao ser repetida para valores de k variando de 1 at n-1. Restam as operaes de deslocamento que, como j sabemos, so realizadas em quantidades proporcionais ao quadrado do nmero de chaves. Sugerimos, como exerccio, que demonstre que o nmero mdio de deslocamentos necessrios para inserir as n-1 chaves (n - n)/4. Vemos, assim, que a alternativa de usar a pesquisa binria para rapidamente encontrar a posio de insero no produz grandes ganhos no desempenho do mtodo, pois aps a localizao, ainda necessrio promover os deslocamentos das chaves direita da posio de insero, que uma operao de desempenho (n). Caso, no entanto, possamos dispor de uma arquitetura que execute deslocamentos em blocos, poderemos obter vantagens no tempo despendido com a operao. Deixamos ainda a sugesto de implementar o mtodo de classificao por insero com busca binria, comparando-o com o de busca seqencial. 5.3 Mtodo dos Incrementos Decrescentes - Shellsort Este mtodo, proposto em 1959 por Donald L. Shell, explora o fato de que o mtodo da insero direta apresenta desempenho aceitvel quando o nmero de chaves pequeno e/ou estas j possuem uma ordenao parcial. De fato, como o desempenho do mtodo quadrtico, se dividirmos o vetor em h segmentos, de tal forma que cada um possua aproximadamente n/h chaves e classificarmos cada segmento separadamente, teremos, para cada um deles, um desempenho em torno de (n/h)2. Para classificarmos todos os h segmentos, o desempenho ser proporcional a h * (n/h)2 = n2/h. Alm disso, teremos rearranjado as chaves no vetor, de modo que elas se aproximem mais do arranjo final, isto , teremos feito uma ordenao parcial do vetor, o que contribui para 36

a diminuio da complexidade do mtodo. Para implementar o mtodo, o vetor C[1..n] dividido em segmentos, assim formados:
Tabela 5.4 Diviso em segmentos no mtodo dos Incrementos Decrescentes.

segmento 1: C[1], C[h+1], C[2h+1], ... segmento 2: C[2], C[h+2], C[2h+2], ... segmento 3: C[3], C[h+3], C[2h+3], ... ..... segmento h: C[h], C[h+h], C[2h+h], ... onde h um incremento. Inicialmente consideraremos incrementos iguais a potncias inteiras de 2. Num primeiro passo, para um certo h inicial, os segmentos assim formados so ento classificados por insero direta. Num segundo passo, o incremento h diminudo (a metade do valor anterior, se este era uma potncia inteira de 2), dando origem a novos segmentos, os quais tambm sero classificados por insero direta. O processo se repete at que h=1. Quando for feita uma classificao com h=1, o vetor estar classificado. Observe que quando h=1, o mtodo se confunde com o da insero direta. A vantagem reside no fato de que o mtodo shell, em cada passo, faz classificaes parciais do vetor, o que favorece o desempenho dos passos seguintes, uma vez que a insero direta acelerada quando o vetor j se encontra parcialmente ordenado (veja mais, em relao a desempenho, em Mtodo de Insero Direta). 5.3.1 Exemplo Incrementos Decrescentes - Shellsort Conveno: os nmeros acima de cada clula indicam os ndices das chaves no vetor. Os nmeros abaixo de cada clula indicam o nmero do segmento ao qual cada clula pertence. Primeiro passo: h = 4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 25 49 12 18 23 45 38 53 42 27 13 11 28 10 14 1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 Aps classificar cada um dos quatro segmentos, o vetor ter o aspecto a seguir. Segundo passo: h = 2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 11 23 10 12 17 25 27 13 18 28 45 14 53 42 49 38 Aps classificar cada um dos dois segmentos, o vetor ter o aspecto seguinte: Terceiro (e ltimo) passo: h = 1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 10 12 11 13 17 14 18 23 27 25 45 28 49 38 53 42 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 Aps a execuo do ltimo passo, o vetor resultar classificado:P 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 10 11 13 13 14 17 18 23 25 27 28 38 42 45 49 53

37

5.3.2Implementao A implementao do mtodo dos incrementos decrescentes corresponde a uma adaptao da implementao do mtodo de insero direta, de tal forma que, ao invs de classificar um vetor cujos elementos se encontram contguos uns aos outros, considera apenas os elementos do vetor que pertencem a um certo segmento, ou seja, os elementos que iniciam numa dada clula f e esto espaados entre si a uma distncia h. A seguir so mostradas as adaptaes que devem ser efetuadas no procedimento insero direta. As setas indicam as linhas que foram alteradas. O resultado o procedimento insero direta shell.
void shell_sort(void) { int i, j, k, salto, aux; int a[5] = {9,5,3,2,1}; // intervalos for(k=0; k<5; k++) { salto = a[k]; for(i=salto; i<MAX; i++) { aux = vetor[i]; for(j=i-salto; aux<vetor[j] && j>=0; j=j-salto) vetor[j+salto] = vetor[j]; vetor[j+salto] = aux; } } }

5.3.3Anlise do Desempenho Segundo Knuth e Wirth, a anlise do desempenho do mtodo muito complexa, pois identifica alguns problemas matemticos bastante difceis, alguns deles ainda no resolvidos. Um dos problemas determinar o efeito que a ordenao dos segmentos em um passo produz nos passos subseqentes. Tambm no se conhece exatamente a melhor seqncia de incrementos que produz o melhor resultado. A este respeito, observa-se que os valores dos incrementos no podem ser mltiplos uns dos outros. Isto evitar que se combine, em uma iterao, dois segmentos entre os quais no havia nenhuma iterao prvia. Esta iterao desejvel e deve ser a maior possvel, de acordo com o teorema a seguir: Se a uma seqncia, previamente ordenada, de distncia k, for em seguida aplicada uma ordenao de distncia i, ento essa seqncia permanece ordenada de distncia k. D.Knuth O autor do teorema mostra, por meio de simulaes, que os melhores resultados obtidos foram com as seguintes seqncias de incrementos:
Tabela 5.5: Resultados para incrementos no shellsort.

1,09 n 1,22 n1,26 1,12 n1,28 1,66 n1,25


1,27

para a srie para a srie para a srie para a srie

2k +1,...,9,5,3,1; = ...,2,1 2k - l,...,15,7,3,l; k= ...,2,1 (2k - (-1) k) / 3,...,11,5,3,1; k= ...,3,2 (3k - 1) / 2, ...,40,13,4,1; k= ...,2,1

Observao: na primeira srie o valor 1 foi inserido. Quanto ao valor inicial do incremento, recomendado que seja aquele valor que divida o vetor de chaves no maior nmero possvel de segmentos de mais de um elemento.

38

6 Classificao por Trocas


Os mtodos de classificao por trocas caracterizam-se por efetuarem a classificao por comparao entre pares de chaves, trocando-as de posio caso estejam fora de ordem no par.

6.1 Mtodo da Bolha - Bubblesort


Neste mtodo, o princpio geral aplicado a todos os pares consecutivos de chaves. Este processo executado enquanto houver pares consecutivos de chaves no ordenados. Quando no restarem mais pares no ordenados, o vetor estar classificado. 6.1.1 Exemplo Suponha que se deseje classificar em ordem crescente o seguinte vetor de chaves: 28 26 30 24 25 Comparamos todos os pares de chaves consecutivas, a partir do par mais esquerda. Caso as chaves de um certo par se encontrem fora da ordem desejada, efetuamos a troca das mesmas. O processo de comparao dos n-1 pares de chaves denominado de varredura. O mtodo efetuar tantas varreduras no vetor quantas forem necessrias para que todos os pares consecutivos de chaves se apresentem na ordem desejada. Primeira varredura: 26 30 24 28 30 24 28 30 24 25 24 30 28 24 25

28 26 26 26 26

25 25 25 25 30

compara par (28,26): troca. compara par (28,30): no troca. compara par (30,24): troca. compara par (30,25): troca. fim da primeira varredura.

Como ainda existem pares no ordenados, reiniciamos o processo de comparaes de pares de chaves, executando mais uma varredura. Observe, no entanto, que a primeira varredura sempre ir posicionar a chave de maior valor na sua posio definitiva correta (no final do vetor). Isto significa que na segunda varredura podemos desconsiderar a ltima posio do vetor, que portanto fica reduzido de um elemento. Esta situao se repetir ao final de cada varredura efetuada. Segunda varredura: 28 24 25 28 24 25 24 28 25 24 25 28 Terceira varredura: 24 25 28 26 25 28 25 26 28

26 26 26 26 26 24 24

30 30 30 30 30 30 30

compara par (26,28): no troca. compara par (28,24): troca. compara par (28,25): troca. fim da segunda varredura. compara par (26,24): troca. compara par (26,25): troca. fim da terceira varredura.

Como no restam mais pares no ordenados, o processo de classificao dado por 39

concludo. Examinando-se o comportamento do mtodo, vemos que eventualmente, dependendo da disposio das chaves, uma certa varredura pode colocar mais do que uma chave na sua posio definitiva. Isto ocorrer sempre que houver blocos de chaves j previamente ordenadas, como no exemplo abaixo. Vetor de chaves: 13 11 25 10 18 21 23 A primeira varredura produzir o seguinte resultado: 11 13 10 18 21 23 25 Observe que, neste caso, as quatro ltimas chaves j se encontram na sua posio definitiva. Isto significa que a prxima varredura no precisa ignorar apenas a ltima chave. As quatro ltimas podem ser ignoradas. A quantidade de chaves, a partir da ltima, que pode ser ignorada de uma varredura para outra conhecida pela posio na qual ocorreu a ltima troca. A partir daquele ponto o vetor j se encontra ordenado. A denominao deste mtodo resulta da associao das chaves com bolhas dentro de um fluido. Cada bolha teria um dimetro proporcional ao valor de uma chave. Assim, as bolhas maiores subiriam com velocidades maiores, o que faria com que, aps um certo tempo, elas se arranjassem em ordem de tamanho. 6.1.2 Implementao A implementao do mtodo bubblesort bastante simples, no exigindo nenhum grande artifcio. O programa controlado por um comando while, que executado enquanto ocorrerem trocas durante uma varredura. Esta condio indicada pela varivel booleana troca. Seu valor ser verdadeiro, caso haja trocas durante uma varredura, e falso, caso contrrio. Assim, o comando while ser executado enquanto esta varivel for verdadeira, ou seja, enquanto houver trocas. A varivel k indica a posio onde ocorreu a ltima troca. Assim, a varredura seguinte, se necessria, ser feita somente at aquela posio.
void bubble_sort(void) { int i, j, aux; for(i = MAX - 1; i > 0; i--) for(j = 0; j < i; j++) if(vetor[j] > vetor[j+1]) { aux = vetor[j]; vetor[j] = vetor[j+1]; vetor[j+1] = aux; } }

6.1.3 Anlise do Desempenho. Consideremos duas situaes extremas para o mtodo: aquela que mais lhe favorece, e a que lhe mais prejudicial, em termos de quantidade de operaes efetuadas para ordenar um vetor de chaves. Pelo exame de como o mtodo procede, percebe-se que o caso mais favorvel aquele no qual as chaves j se encontram na ordem desejada. De fato, nesses casos, ao final da primeira varredura, o mtodo j ter descoberto que nenhuma troca foi efetuada, e que 40

portanto, o vetor j se encontra ordenado. Esta primeira e nica varredura demandar, pois, n1 comparaes. J o caso mais desfavorvel ser aquele no qual as chaves se encontram na ordem inversa quela desejada. Nesses casos, a cada varredura, apenas uma chave ser depositada no seu local definitivo, enquanto as demais apenas se deslocaro uma posio para a esquerda (supondo ordenao crescente). Desse modo, as quantidades de comparaes que sero efetuadas a cada varredura so as seguintes:
Tabela 6.1: Nmero de comparaes efetuadas por varredura (bubblesort)

N da varredura 1 2 3 ... n1

Comparaes efetuadas n1 n2 n3 ... 1

O total de comparaes ser, pois, a soma da progresso aritmtica cujo primeiro termo n - 1 e o ltimo 1, com n - 1 termos, ou seja, ((n-1+1)/2 ) x (n-1) = (n2 - n)/2. Podemos supor que, na prtica, os casos que ocorram correspondam a situaes intermedirias entre os extremos acima. Assim, se tomarmos a mdia aritmtica dos valores encontrados, provavelmente estaremos fazendo uma boa estimativa do nmero mdio de comparaes que sero efetuadas. Cmdio = (Cpior + Cmelhor) /2 = ((n - n)/2)+ (n-1))/2 = (n + n-2)/4 Na verdade, o nmero mdio de comparaes provavelmente ser pouco menor do que isso, pois no estamos considerando, nesta anlise, os possveis blocos de chaves j ordenadas. De qualquer maneira, sendo n2 a parcela dominante, o mtodo de complexidade quadrtica.

6.2 Mtodo da Agitao - Shakesort


Ao efetuarmos a anlise do desempenho do mtodo bubblesort, verificamos que a cada varredura efetuada, a maior das chaves consideradas levada at a sua posio definitiva, enquanto que as demais se aproximam da sua posio correta de apenas uma casa. Isto sugere um aperfeioamento no mtodo, no qual, ao final de uma varredura da esquerda para a direita, seja efetuada outra, da direita para a esquerda, de tal modo que, ao final desta, a menor chave tambm se desloque para a sua posio definitiva. Estas varreduras em direes opostas se alternariam sucessivamente, at a ordenao completa do vetor. O mtodo resultante desta alterao do bubblesort recebeu a denominao de shakesort, j que o seu funcionamento lembra os movimentos de vaivm de uma coqueteleira. A implementao deste mtodo um bom exerccio. Quanto ao seu desempenho, a melhoria obtida reside apenas na quantidade de comparaes efetuadas, uma vez que o mtodo evita muitas das comparaes suprfluas que o bubblesort efetua. J o nmero de trocas necessrias no se altera em relao ao bubblesort. Como esta uma operao mais onerosa do que aquela, o ganho, em tempo, no ser significativo. 41

6.3 Mtodo do Pente - Combsort


Um ganho significativo no mtodo bubblesort pode ser obtido usando a estratgia de promover as chaves em direo s suas posies definitivas por saltos maiores do que apenas uma casa de cada vez. Essa alternativa, proposta por Lacey e Box, consiste em comparar no os pares consecutivos de chaves, mas pares formados por chaves que distam umas das outras uma certa distncia h. Na primeira varredura essa distncia dada pelo valor h = n div 1,3. Nas varreduras subseqentes, esta distncia (tambm denominada salto) progressivamente diminuda do fator 1,3, at que seja igual a unidade. Neste momento, o mtodo se confunde com o bubblesort tradicional. Cada varredura rapidamente conduz as chaves para locais prximos do definitivo por meio de grandes saltos. A medida que os saltos vo diminuindo de comprimento, aproximandose da unidade, as chaves estaro j bem prximas de suas posies corretas, quando ento o bubblesort se tornar eficiente. O fator 1,3 de reduo dos saltos foi obtido pelos autores do algoritmo por simulao. Foram testados fatores que variavam de 1,1 a 1,45, sendo que o de valor 1,3 foi o que apresentou melhores resultados, em mdia. O estudo da seqncia de saltos, com fator de reduo 1,3 revela que as possveis seqncias de valores de h s podem terminar de uma das seguintes formas: 9 10 11 6 7 8 4 5 6 3 3 4 2 2 3 1 1 2

O resultado das simulaes revelou, tambm, que o terceiro caso aquele que produz os melhores resultados. Assim, quando h>11 e o clculo do valor do salto seguinte resultarem 9 ou 10, ele forado a tomar o valor 11, para que a srie conclua na sua forma mais favorvel ao mtodo. A reduo do tempo de classificao desse mtodo em relao ao bubblesort tradicional (sem qualquer tipo de otimizao) foi da ordem de 27 vezes. As sucessivas redues dos saltos so anlogas ao ato de pentear cabelos longos e embaraados, inicialmente apenas com os dedos e depois usando pentes com espaos entre os dentes cada vez menores. Da a denominao do mtodo dada pelos autores. 6.3.1 Exemplo Suponhamos o mesmo vetor de chaves usado para exemplificar o mtodo bubblesort. Como o vetor possui 5 chaves, o salto inicial igual a 3. Como vemos, a classificao do vetor, embora tambm tenha consumido 9 iteraes, demandou apenas trs trocas, enquanto o bubblesort, para ordenar o mesmo vetor, efetuou 8 trocas. justamente neste ponto que reside a vantagem do combsort em relao ao bubblesort, uma vez que a operao de troca, por envolver vrios acessos memria, a que vai determinar a velocidade de classificao.

42

Tabela 6.2: Nmero de comparaes efetuadas por varredura (combsort).

Varredura Iterao 1 1 28 2 24 24 2 3 4 24 5 24 3 6 24 7 24 8 24 9 24 6.3.2 Implementao

Vetor de chave 26 30 24 30 28 26 25 28 30 30 25 28 25 28 30 26 28 25 28 25 26 25 26 28 25 26 28

25 25 26 26 26 30 30 30 30

Salto 3 3 2 2 2 1 1 1 1

Par comparado 28,24 26,25 24,30 25,28 30,26 24,25 25,26 26,28 28,30

Ao troca troca no troca no troca troca no troca no troca no troca no troca

int calcula_salto (int nsalto) { nsalto = nsalto / 1.3; if ( (nsalto == 9) || (nsalto == 10) ) nsalto=11; return (nsalto); } void comb_sort (void) { int i, aux, salto,troca; salto = MAX; do { salto = calcula_salto(salto); for ( i = 0 ; i < (MAX-salto) ; i++ ) { if ( (i+salto) < MAX ) { if ( vetor[i] > vetor[i+salto] ) { aux = vetor[i]; vetor[i] = vetor[i+salto]; vetor[i+salto] = aux ; } } else break; } } while ( salto >= 1 ); }

Observe que quando h=1, o mtodo se confunde com o bubblesort.

6.4 Mtodo de Partio e Troca - Quicksort


O mtodo apresentado a seguir foi proposto por Hoare. Seu desempenho to espetacular, que seu inventor denominou-o quicksort, ou seja, ordenao rpida. De fato, comparado com os demais mtodos, o que apresenta, em mdia, o menor tempo de classificao. Isto porque, embora tenha um desempenho logartmico como muitos outros, o que apresenta menor nmero de operaes elementares por iterao. Isto significa que, mesmo que tenha que efetuar uma quantidade de iteraes proporcional a n log2 n, cada uma delas ser mais rpida.

43

6.4.1 Descrio do Mtodo Seja o vetor de chaves C[1..n] a ser ordenado. Numa etapa inicial, esse vetor particionado em trs segmentos S1, S2, S3, da seguinte forma: S2 ter comprimento 1 e conter uma chave denominada particionadora; S1 ter comprimento >=0 e conter todas as chaves cujos valores forem menores ou iguais ao da chave particionadora. Esse segmento posicionado esquerda de S2; S3 tambm ter comprimento >=0 e conter todas as chaves cujos valores forem maiores do que o da particionadora. Esse segmento posicionado direita de S2. Esquematicamente esse particionamento mostrado nas figuras a seguir. vetor inicial: 1 vetor particionado: 1 S1 n

k-1

k S2

k+1

S3

Observe que, pelo fato de C[i] <= C[k] para i=1,...,k-1, e C[i] > C[k] para i = k+1,...,n, a chave particionadora C[kl ocupa a sua posio definitiva correta na ordenao. O processo de particionamento reaplicado aos segmentos S1 e S3 e a todos os segmentos correspondentes da resultantes, que tiverem comprimento >=1. Quando no restarem segmentos a serem particionados, o vetor estar ordenado. Como vemos, o mtodo consiste na aplicao sucessiva da operao de particionamento de segmentos, que a operao bsica do mtodo. Assim sendo, esta operao deve ser implementada de forma simples e eficiente. Um dos pontos principais da operao o da escolha da chave particionadora, j que o seu valor determinar o tamanho dos segmentos S1 e S3. A chave particionadora ideal aquela que produz segmentos S1 e de igual tamanho (ou aproximado). Isto pode ser obtido se o seu valor for igual ao da chave de valor mediano. Entretanto, para encontrarmos tal chave, seria necessrio percorrer todo o vetor a ser particionado. Este procedimento tornaria a operao lenta, pois deve ser aplicado a todos os segmentos de comprimento maior ou igual a um e que se formam aps cada particionamento. Para evitar esse problema, devemos usar um critrio de escolha simples e rpido, de preferncia que no envolva uma pesquisa entre as chaves. Ora, se no tivermos nenhum conhecimento prvio sobre a distribuio dos valores das chaves, podemos supor que, em princpio, qualquer uma delas pode ser a particionadora. Tendo isto em vista, e com a finalidade de simplificar o algoritmo de particionamento, arbitraremos que a chave que se encontra na posio inicial do vetor a ser particionado ser a particionadora. Caso, no entanto, tenhamos um conhecimento prvio sobre a distribuio dos valores das chaves, podemos usar um outro critrio de escolha. Por exemplo, se soubermos que o vetor j se encontra parcialmente ordenado, podemos eleger como particionadora aquela chave que se encontra na posio central do vetor. O exemplo a seguir ilustra o processo de particionamento, usando a chave inicial do vetor como particionadora. O algoritmo executa o particionamento em n passos (n = nmero de chaves). Nos n-1 primeiros, as chaves (excluda a particionadora) so deslocadas para o lado esquerdo do vetor, se menores ou iguais particionadora, ou para o lado direito, se maiores. No ltimo passo a 44

chave particionadora inserida na sua posio definitiva correta. Suponhamos o vetor abaixo: 9 25 10 18 5 7 15 3 Escolhemos a chave 9 como particionadora e a guardamos em uma varivel temporria (cp). Com isto, a posio ocupada por ela se torna disponvel para ser ocupada por outra chave. Esta situao indicada pelo smbolo x : 25 10 18 5 7 15 3 cp=9

Marcamos tambm o incio e o fim do vetor por dois apontadores: i (de incio) e f (de fim). A expresso esquerda escrita ao lado do vetor indica que a posio apontada pelo ponteiro i (o da esquerda) est disponvel e pode ser ocupada por outra chave. Nos passos seguintes, a expresso direita indica o contrrio. i f 18 5 7 15 3 esquerda 1. 25 10 A seguir, comparamos a chave que est apontada por f com a particionadora. Como aquela menor do que esta, a deslocamos para o lado esquerdo do vetor (que o lado onde ficam as chaves menores ou iguais particionadora), ao mesmo tempo em que avanamos o ponteiro i para indicar que a chave recm-movida j se encontra no segmento correto. A nova posio vaga passa a ser a apontada por f: i f 2. 3 25 10 18 5 7 15 direita Agora comparamos a chave 25 (pois a nova posio vaga a da direita) com a particionadora. Como aquela maior, a deslocamos para a posio vaga, ao mesmo tempo em que recuamos o ponteiro fuma posio para a esquerda, indicando assim que a chave 25 j se encontra no seu segmento correto. Agora a posio vaga a da esquerda: i f 3. 3 10 18 5 7 15 25 esquerda O processo prossegue comparando a chave 15. Neste caso, por ela ser maior do que a particionadora, no deve ser trocada de posio, pois j se encontra no segmento correto. Apenas o ponteiro f deslocado para a esquerda. A posio vaga permanece sendo a da esquerda: i f 4. 3 10 18 5 7 15 25 esquerda No passo seguinte a chave 7 colocada na posio vaga, e o ponteiro i ajustado: i f 5. 3 10 18 5 15 25 direita 7 Os passos seguintes so feitos de forma similar e so mostrados a seguir: i f 6. 3 18 5 10 15 25 esquerda 7 45

i f 7. 3 5 18 10 15 25 direita 7 O resultado do 7 passo (correspondente ao n-1) produz a situao abaixo, na qual os dois ponteiros i e j se encontram. Quando isto ocorre, os segmentos S1 e S3 j esto formados. A posio vaga que fica entre eles corresponde posio do segmento S2, ou seja, aquele que contm a chave particionadora. i,f 3 5 18 10 15 25 7 Assim, resta copiar o valor da chave particionadora na posio apontada por i e j, que o processo de particionamento estar completado: 3 7 5 9 18 10 15 25 Observe que, embora os segmentos Si e S3 no estejam ainda ordenados, a chave particionadora j se encontra na sua posio definitiva correta. O processo de classificao prossegue com o particionamento dos segmentos S1 e S3 e todos os demais segmentos correspondentes de tamanho maior ou igual a um que forem se formando. A seqncia a seguir exibe apenas o estado final de cada processo de particionamento. As chaves particionadoras so mostradas em tipo negrito, enquanto os segmentos de apenas um elemento que se formarem so mostrados em tipo itlico. 7 5 15 10 25 3 9 18 5 10 3 7 9 15 18 25 O que encerra a ordenao do vetor. 6.4.2 Implementao A implementao do mtodo exigir, portanto, a definio de dois procedimentos: um para executar os particionamentos, e outro para efetuar todos os particionamentos necessrios. O primeiro procedimento, denominado partio a seguir apresentado.
int particao (int inicio, int fim) { int esquerda=1, cp = vetor[inicio]; while (inicio < fim) { if (esquerda) { if (cp >= vetor[fim]) // troca para S1 (esquerda) { vetor[inicio] = vetor[fim]; vetor[fim] = 0; inicio++; esquerda = 0 ; } else fim--; } else {

8.

46

} vetor[inicio] = cp; return(inicio); } // fim funcao particao

if (cp < vetor [inicio]) // troca para S3 (direita) { vetor[fim] = vetor[inicio]; vetor[inicio] = 0; fim--; esquerda = 1; } else inicio++; }

Uma alternativa de implementao da operao de particionamento, adequada quando a chave particionadora escolhida ocupar uma posio intermediria do vetor a ser particionado e possuir um valor mediano, foi proposta por N. Wirth, e funciona da seguinte forma: uma vez escolhida a chave particionadora (denominada cp), percorre-se o vetor a ser particionado da esquerda para a direita, at que seja encontrada uma chave c[i] > cp. A seguir percorre-se o vetor da direita para a esquerda, at que seja encontrada uma chave c[j] <= cp. Nesta ocasio as duas chaves c[i] e c[j] so permutadas de posio. As varreduras prosseguem a partir destes pontos, at que os dois deslocamentos se encontrem em uma posio intermediria do segmento. A esquerda ficaro as chaves menores ou iguais particionadora. A direita as maiores. Observe que este processo divide o segmento em dois outros, j que a particionadora usada apenas como referncia e ir estar sempre contida no segmento da esquerda. Independentemente da implementao usada para efetuar os particionamentos, devemos implementar o procedimento que ir efetuar todos os particionamentos necessrios para ordenar o vetor. Essa operao (particionamento sucessivo) claramente recursiva, uma vez que, obtido o primeiro particionamento, a operao reaplicada sobre os segmentos resultantes, at que todas as parties fiquem reduzidas a um nico elemento. Assim, o procedimento que implementa a operao refletir esta natureza recursiva, sendo recursivo tambm:
void quick_sort (int comeco, int final) { int divisor; if ( final > comeco ) { divisor = particao (comeco, final); quick_sort(comeco, divisor-1); quick_sort(divisor+1,final); } } // fim funcao quick_sort

// particiona // ordena segmento a direita // ordena segmento a esquerda

Em linguagens de programao que no suportam a recursividade, a implementao do procedimento quicksort deve manipular explicitamente a pilha de pedidos pendentes de particionamento. Deixaremos a implementao dessa alternativa como exerccio. 6.4.3 Anlise do Desempenho A anlise que ser feita a seguir baseada na implementao da operao de particionamento descrita inicialmente. Esta implementao, ao contrrio da segunda alternativa proposta, no efetua trocas (que implicam uma operao triangular). Para cada chave encontrada fora do seu segmento, feita apenas uma transposio da mesma para a posio vaga existente. Essa operao 47

(transposio de segmento) no ser considerada na anlise. Apenas consideraremos a quantidade de comparaes efetuadas para decidir em que segmento deve se localizar cada uma das chaves. Como comparamos a chave particionadora com todas as demais, obviamente sempre sero requeridas n-1 comparaes para particionar n chaves. Supondo, agora, a situao ideal em que a chave particionadora seja tal que o particionamento produza dois segmentos S1 e S3 de igual tamanho, teremos dividido o vetor de n elementos em segmentos com os seguintes tamanhos: S1 com (n1) /2 chaves; S2 com 1 chave; S3 com (n1)/2 chaves. Esquematicamente:

(n-1)/2

(n-1)/2

Persistindo a situao ideal ao longo dos demais particionamentos, os segmentos que sero sucessivamente gerados tero os seguintes comprimentos:

(n-1)/2

(n-1)/2

(n-3)/4

(n-3)/4

(n-3)/4

(n-3)/4

(n-7)/8

(n-7)/8

(n-7)/8

(n-7)/8

(n-7)/8

(n-7)/8

(n-7)/8

(n-7)/8

E assim por diante, at que (n-k)/(k+1) <=1. Cada um desses segmentos vai requerer, para ser particionado, tantas comparaes quantos so seus elementos menos uma unidade. Assim, temos: 1. Para o primeiro segmento: n-1 comparaes; 2. Para os 2 seguintes: (((n-1)/2)-1) x 2 = n-3 comparaes; 3. Para os 4 seguintes: (((n-3)/4)-1) x 4 = n-7 comparaes; 4. Para os 8 seguintes: (((n-7)/8)-1) x 8 = n-15 comparaes; e assim sucessivamente, enquanto restarem segmentos de mais de um elemento. Esta 48

condio deixar de ocorrer aps executados log2 n passos de particionamento (a= maior inteiro <= a). Logo, o total das comparaes a serem efetuadas igual soma do nmero de comparaes efetuadas em cada passo, ou seja:

Este seria, pois, o melhor caso para o mtodo. Naturalmente, no devemos esperar que isto ocorra em situaes normais. Supondo que as chaves sejam nicas, a probabilidade de escolhermos ao acaso exatamente a particionadora mediana 1/n. No entanto, segundo Wirth, quando escolhemos a chave particionadora aleatoriamente, o desempenho mdio do quicksort se torna inferior ao do caso timo de um fator da ordem de apenas 2 ln 2. H, ainda, a considerar o pior caso. Este se configura quando a chave particionadora escolhida a menor (ou a maior) de todas. Nesse caso, e persistindo a situao em todos os demais particionamentos, o mtodo se torna extremamente lento, com desempenho quadrtico. De fato, se a chave escolhida for sempre a menor de todas, os particionamentos produziro os segmentos S1 vazios e os S3 com n-1 elementos, conforme o esquema mostrado adiante. Sendo necessrios, pois, n-1 passos, cada um deles exigindo um nmero de comparaes igual ao nmero de elementos menos um, ou seja: (n-1) + (n-2) + (n-3) + ... + 1 = ((n-1) + 1)/2 x (n-1) = (n2 - n)/2 Esse caso ocorrer, por exemplo, se o vetor j estiver ordenado e sempre escolhermos a primeira chave como particionadora. Assim, quando o vetor estiver parcialmente ordenado, conveniente usar uma chave que ocupe uma posio mais central do mesmo.

49

7 Classificao por Seleo


Os mtodos que formam a famlia de classificao por seleo caracterizam-se por procurarem, a cada iterao, a chave de menor (ou maior) valor do vetor e coloc-la na sua posio definitiva correta, qual seja, no incio (ou no final) do vetor, por permutao com a que ocupa aquela posio. O vetor a ser classificado fica, desta maneira, reduzido de um elemento. O mesmo processo repetido para a parte restante do vetor, at que este fique reduzido a um nico elemento, quando ento a classificao estar concluda. Os mtodos de classificao por seleo diferenciam-se uns dos outros, pelo critrio utilizado para selecionar, a cada iterao, a chave de menor (ou maior) valor.

7.1 Mtodo de Seleo Direta


Neste mtodo, a seleo da menor chave feita por pesquisa seqencial. Uma vez encontrada a menor chave, esta permutada com a que ocupa a posio inicial do vetor, que fica, assim, reduzido de um elemento. O processo de seleo repetido para a parte restante do vetor, sucessivamente, at que todas as chaves tenham sido selecionadas e colocadas em suas posies definitivas. 7.1.1 Exemplo Suponha que se deseja ordenar o vetor: 9 25 10 18 5 7 15 3 Segundo o princpio formulado, a ordenao desse vetor de 8 chaves demandar 7 iteraes, conforme mostrado na tabela a seguir.
Tabela 7.1 Demonstrao do Mtodo de Seleo Direta

Itera o 1 2 3 4 5 6 7

Vetor 9 3 3 3 3 3 3 3 25 25 5 5 5 5 5 5 10 10 10 7 7 7 7 7 18 18 18 18 9 9 9 9 5 5 25 25 25 10 10 10 7 7 7 10 10 25 15 15 15 15 15 15 15 15 25 18 3 9 9 9 18 18 18 25

Chave Permutao Vetor ordenado at a posio 3 93 5 25 5 1 7 10 7 2 9 18 9 2 10 10 25 4 15 15 25 5 18 25 18 6 8

Observe que a ltima iterao (7) encerra a classificao, pois se as n-1 menores chaves do vetor esto em suas posies definitivas corretas, ento a maior (e ltima) automaticamente tambm estar. 7.1.2 Implementao A implementao do mtodo direta. O procedimento a seguir toma a primeira chave da parte ainda no ordenada do vetor como sendo, em princpio, a menor de todas, comparando-a seqencialmente com todas as demais. Sempre que encontrar uma de valor inferior, passa a considerar esta como a menor. Ao final de cada iterao, a menor chave encontrada inserida na primeira posio da parte no classificada, que assim fica reduzida de um elemento. 50

void selecao_direta (void) { int i, j, ind_x, menor; for (i=0; i<MAX-1; i++) { menor = vetor[i]; ind_x = i; for (j=i+1; j<MAX; j++) { if (vetor[j] < menor) { menor = vetor[j]; ind_x = j; } } vetor[ind_x] = vetor[i]; vetor[i] = menor; } } // fim selecao_direta

7.1.3Anlise do Desempenho Na anlise do desempenho do mtodo de seleo direta podemos considerar dois aspectos: a quantidade de comparaes efetuadas e a quantidade de trocas de mnimos efetuadas. Quanto ao nmero de comparaes efetuadas, temos: 1 iterao: compara o 1 elemento com os n-1 demais: n-1 comparaes. 2 iterao: compara o 2 elemento com os n-2 demais: n-2 comparaes. 3 iterao: compara o 3 elemento com os n-3 demais: n-3 comparaes. ... (n-1) iterao: compara o (n-1) elemento com o ltimo: 1 comparao. Total de comparaes: (n-1) + (n-2) + (n-3) + ... + 1 = (n-n)/2 = O(n). Observe que, neste mtodo, a quantidade de comparaes efetuadas constante para um dado n, ou seja, no depende do arranjo prvio das chaves. J o nmero de trocas de mnimos durante a busca da menor chave em cada iterao no constante, pois depende da ordem em que as chaves se apresentam. Este fato vai fazer com que os tempos de classificao de dois vetores com as mesmas chaves, porm arranjadas de formas diversas, possam ser diferentes. Supondo que a distribuio dos valores das chaves seja uniforme, ento o nmero mdio de trocas de mnimos efetuadas ao longo de uma iterao pode ser estimado da seguinte forma: a probabilidade de que a segunda chave seja menor do que a primeira (tomada como mnimo inicial) 1/2. A probabilidade de que a terceira chave seja menor do que as duas primeiras 1/3. A probabilidade da quarta ser menor do que as trs primeiras 1/4 e, assim por diante, at a ltima chave, cuja probabilidade de ser menor do que todas as n-1 que a antecedem 1/n. Estes valores correspondem s freqncias relativas com que cada troca vai ocorrer. Portanto, a quantidade mdia de trocas de mnimos durante uma iterao ser: (1/2 )+ (1/3) +(1/4)+ ... + (1/n), sendo n o nmero de chaves envolvidas na iterao. A srie de valores apresentada, acrescida da unidade, conhecida na literatura como srie harmnica, e sua soma denominada nmero harmnico, denotado por Hn e cujo valor, para os fins aqui propostos, pode ser aproximado por: 51

O smbolo y representa a constante de Euler, cujo valor 0,5772157... 7.2 Mtodo da Seleo em rvore - Heapsort A concluso a que chegamos sobre o mtodo da seleo direta torna o seu uso muito restrito, pois infelizmente sempre sero necessrias n-1 comparaes para decidir qual a menor dentre n chaves. Por outro lado, essa condio s absolutamente necessria na primeira varredura. Ao final desta, podemos reter outras informaes alm da identificao da menor chave, que permitam acelerar a busca das outras menores que sucedem a primeira. Por exemplo, se dividirmos o vetor de chaves em dois segmentos de igual tamanho, poderemos, na primeira varredura, identificar a menor chave de cada segmento. A seguir, selecionaramos a menor dentre estas duas, colocando-a na sua posio definitiva correta. Na segunda varredura ser necessrio operar apenas com o segmento que forneceu a menor chave, ou seja, trabalharemos com n/2 chaves ao invs de n-1, o que j representa um ganho significativo. Uma vez encontrada a segunda menor chave deste segmento, a comparamos com a do outro para selecionar a menor chave desta segunda iterao. Este princpio pode ser estendido de modo a subdividir o vetor de chaves em tantos segmentos quantos forem possveis, de tal forma a minimizar a quantidade de comparaes a ser efetuada em cada iterao para encontrar a chave de menor valor. O quadro a seguir exemplifica a aplicao deste princpio, mostrando um vetor de chaves sucessivamente dividido ao meio, sendo a menor chave de cada segmento identificada em negrito.
Tabela 7.2: Exemplo de aplicao Mtodo Heapsort.

15 15 15 15

09 09 09 09

18 18 18 18

25 25 25 25

14 14 14 14

10 10 10 10

22 22 22 22

12 12 12 12

A identificao da menor chave em cada um dos sucessivos segmentos nos quais o vetor foi subdividido pode ser representada pela seguinte estrutura de rvore:

52

09

09

10

09

18

10

12

15 09

18 25

14 10

22 12

Figura 7.1 rvore das menores chaves de cada segmento.

Agora podemos selecionar a menor de todas as chaves e remov-la do sistema, deixando vagas as posies que ela ocupava. Essas vagas sero tomadas, por sua vez, pelas chaves sucessoras (na ordem crescente), de cada subrvore, permanecendo livre apenas a ltima posio anteriormente ocupada pela chave removida. Na Figura 7.2a vemos a rvore com a chave 09 removida e, na Figura 7.2b, vemos as vagas por ela deixadas ocupadas pelas chaves sucessoras.
O

10

18

10

12

15

18 25

14 10

22 12

Figura 7.2a Remoo da menor chave. 10

15

10

15

18

10

12

15 O

18 25

14 10

22 12

Figura 7.2b Promoo da sucessora.

53

Desse modo, a segunda menor chave aparecer na raiz da rvore e poder ser removida, seguindo-se a promoo da terceira menor chave, mostrada na Figura 7.3. Esse processo se repete at que todas as n chaves tenham sido removidas.
12

15

12

15

18

14

12

15 O

18 25

14 O

22 12

Figura 7.3 Promoo da terceira menor chave.

Observe que, a cada nvel da rvore que se desce, o problema fica reduzido pela metade, pois apenas uma subrvore afetada pela remoo da chave. Com isso, a cada iterao, evitamos trabalhar com a totalidade das chaves restantes, o que ir acelerar bastante a seleo da menor chave em cada iterao. Este , pois, o princpio bsico do mtodo de seleo em rvore. 7.2.1 Implementao A implementao, proposta por J. Williams, permite simplificar ainda mais a estrutura de rvore que mantm a hierarquia das chaves. A simplificao elimina a redundncia de ocorrncias de chaves na rvore, fazendo com que ela possua exatamente n nodos, ao invs dos 2n-1 at agora usados, o que simplifica a manipulao da estrutura. Dado ento um vetor de chaves C1, C2, ... , Cn consideramos este vetor como sendo a representao de uma rvore binria, usando a seguinte interpretao dos ndices das chaves: C1 raiz da rvore e C2i = subrvore da esquerda de Ci, C2i+1 = subrvore da direita de Ci para i=1, n div 2. Exemplificando: dado o vetor C1..7, e utilizando a interpretao anterior, podemos v-lo como sendo a representao da rvore mostrada na Figura 7.4. C1

C2

C3

C4

C5

C6

C7

Figura 7.4 rvore representada pelo vetor C1..7.

54

O passo seguinte consiste em trocar as chaves de posio dentro do vetor, de tal forma que estas passem a formar uma hierarquia, na qual todas as razes das subrvores sejam maiores ou iguais a qualquer uma das suas sucessoras, ou seja, cada raiz deve satisfazer as seguintes condies: Ci >= C2i e Ci >= C2i+1para i= 1, n div 2. Quando todas as razes das subrvores satisfazem essas condies, dizemos que a rvore forma um heap. Da a denominao do mtodo. Essas condies, na verdade, estabelecem uma relao de ordem entre as subrvores, de tal modo que, para qualquer heap, podemos afirmar que C1 >= Ci, i=2, n, ou seja, podemos afirmar que a maior chave dentre as n que formam o vetor aquela que est na raiz da rvore, isto , a chave C1. O problema agora , ento, efetuar as trocas de posies das chaves no vetor, de tal forma que a rvore representada passe a ser um heap. Esse processo pode ser feito testandose cada uma das subrvores para verificar se elas satisfazem, separadamente, a condio de heap. Naturalmente, apenas as subrvores que possuem pelo menos um sucessor devem ser testadas. A maneira mais simples de sistematizar esse teste inici-lo pela ltima subrvore, qual seja, aquela cuja raiz est na posio n div 2 do vetor de chaves, prosseguindo-se, a partir da, para as subrvores que antecedem esta, at testar a raiz da rvore. Desse modo, a primeira subrvore da Figura 7.4a ser testada aquela cuja raiz C3, seguindo-se o teste pela subrvore de raiz C2 e finalizando com a de raiz C1. Sempre que for encontrada uma subrvore que no forme um heap, seus componentes devem ser rearranjados de modo a formar o heap. Por exemplo, se for encontrada uma subrvore como a da Figura 7.5 (a), seus componentes devem ser rearranjados na forma da Figura 7.5 (b).
10 12

5 a)

12

5 b)

10

Figura 7.5 Transformao de uma subrvore em heap.

Pode ocorrer tambm que, ao rearranjarmos uma subrvore, isto venha a afetar outra, do nvel imediatamente inferior, fazendo com que esta deixe de formar um heap. Essa possibilidade obriga a verificar, sempre que for rearranjada uma subrvore, se a sucessora do nvel abaixo no teve a sua condio de heap desfeita. Para exemplificar todo o processo, suponhamos o seguinte vetor de chaves: 1 12 2 09 3 13 4 25 5 18 6 10 7 22

Cuja interpretao sob a forma de rvore :

55

12

09

13

25

18

10

22

A transformao dessa rvore em heap inicia pela subrvore cuja raiz 13, j que seu ndice, no vetor, 7 div 2 = 3. O rearranjo dos componentes desta subrvore produz a configurao da rvore e do vetor correspondente mostrada na Figura 8.6.

Figura 7.6: Transformao da subrvore de raiz 13 em heap.

A prxima subrvore a ser rearranjada a de raiz 09, mostrada na Figura 7.7.

Figura 7.7: Transformao da subrvore de raiz 09 em heap.

E finalmente a de raiz 12, na figura abaixo.

56

Figura 7.8: Transformao da subrvore de raiz 12 em heap.

Nesse caso ocorreu aquela possibilidade anteriormente mencionada, na qual a transformao de uma subrvore poderia afetar outra de um nvel abaixo. A subrvore afetada deve ser rearranjada, conforme mostra a figura abaixo.

Figura 7.9: Rearranjo de uma subrvore do nvel inferior.

Uma vez que todas as subrvores formam heaps, a rvore toda tambm um heap. At aqui se conseguiu apenas selecionar a maior chave que aparece na raiz da rvore. Entretanto, a partir deste ponto, em vista da configurao tomada pela rvore, a seleo das chaves seguintes ser bastante simplificada. Se a chave que est na raiz a maior de todas, ento sua posio definitiva correta na ordem crescente na ltima posio do vetor, onde ela colocada, por troca com a chave que ocupa aquela posio. Com a maior chave j ocupando a sua posio definitiva podemos, a partir de agora, considerar o vetor como tendo um elemento a menos, o qual ser mostrado no vetor (rea sombreada), mas no aparecer na rvore correspondente, conforme ilustrado na figura a seguir.

57

Figura 7.10: Colocao da maior chave em sua posio definitiva.

Para selecionar a prxima chave, deve-se fazer com que a rvore volte a ser um heap. Como a nica subrvore que sofreu alteraes foi a da raiz, basta fazer com que esta subrvore volte a formar um heap, rearranjando-se os seus componentes, e verificando-se, tambm, se este rearranjo no interferiu em uma das subrvores do nvel imediatamente abaixo.

Figura 7.11: Seleo da segunda maior chave.

Novamente a maior chave dentre as restantes aparece na raiz (perceba a pequena quantidade de comparaes necessrias para efetuar a seleo desta chave). A Figura 7.12 mostra a segunda maior chave sendo colocada em sua posio definitiva correta, de forma idntica ao do caso anterior.

Figura 7.12: Colocao da segunda maior chave em sua posio.

58

Na figura seguir a rvore novamente rearranjada para formar um heap, o que permitir selecionar a prxima chave.

Figura 7.13: Seleo da terceira maior chave.

Os demais passos seguem o mesmo princpio e so mostrados na figura, a seguir. Observe que, da mesma forma que no caso da seleo direta, a classificao estar encerrada aps a iterao n-1, pois teremos selecionado as n-1 maiores chaves, restando, assim, apenas a menor de todas, que automaticamente estar ocupando a posio inicial do vetor, o que encerra a classificao.

Figura 7.14: Final do processo de classificao.

O algoritmo que implementa esse mtodo segue exatamente a descrio anterior. Para 59

simplificar a manipulao do vetor, foi criada uma funo auxiliar keyval, que retorna o valor da chave que est na posio i do vetor, caso i <= n. Em caso contrrio, o valor da funo ser igual a minkey, que corresponde ao menor valor de chave possvel de ser representado. Por exemplo, se as chaves forem do tipo inteiro de 16 bits, ento minkey ser igual a 215 = -32768. O uso dessa funo nos permitir no fazer distino entre nodos razes e folhas, assegurando o trmino natural do processamento. /* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 59 */
int keyval (c, n, i) { if (i > n ) keyval(minkey) else keyval(c[i]); }

Com auxlio de keyval, escrevemos o procedimento heap, cuja finalidade transformar em heap a subrvore cuja raiz apontada por r. Para cada subrvore transformada, verificado se a subrvore do nvel inferior que esteve envolvida na transformao no foi afetada. Em caso afirmativo, a verificao tambm se estende a ela. O processo se encerra quando no houver mais trocas, ou quando for atingido o nvel das folhas da rvore. /* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 60 */

void heap (c, n, r) { i = r; troca = 1; while (troca) { // procura maior subrvore if ( keyval(c, n, 2*i) > keyval(c, n, 2*i+1) ) h = 2*i; // subrvore da esquerda else h = 2*i+1; // subrvore da direita if ( c[i] < keyval(c, n, h) ) // compara raiz com o maior sucessor { aux = c[i]; // troca c[i] = c[h]; c[h] = aux; i = h ; // desce p/raiz da subrvore afetada pela troca } else troca = 0; // se no houve troca } }

Resta agora escrever o procedimento final, que denominaremos heapsort. Este ser constitudo de duas partes. A primeira far a formao do heap inicial, ou seja, preparar a rvore para a seleo da maior chave. A segunda parte, que denominaremos manuteno do heap, rearranjar a rvore aps cada seleo da maior chave, para que ela retome a forma de heap, permitindo assim a seleo da prxima chave. Essa segunda parte demanda n-1 iteraes, e corresponde fase de classificao propriamente dita.

60

void heapsort(c, n) { i = n div 2; // raiz da ltima for (r = i; r > 1 ; r--) heap (c, n, r); for ( n1= n-1 ; n>1 ; n1) { aux = c[1]; c[1] = c[n1+1]; c[n1+1] = aux ; heap (c, n1, 1); } }

/* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 61 */


rvore // formao do heap inicial // manuteno do heap // coloca a maior chave em sua posio

// refaz heap a partir da raiz da rvore

7.2.2 Anlise do Desempenho Na anlise do desempenho do heapsort, devemos considerar separadamente as duas fases do mtodo: a formao do heap inicial e a sua manuteno. Para a fase de formao, a situao mais favorvel ao mtodo aquela na qual as chaves j satisfazem as condies Ci >= C2i e Ci >= C2i+1, para i=1, n div 2. Uma permutao que satisfaz essas condies a do vetor na ordem decrescente, ou seja, na ordem contrria desejada. Nesses casos, a rvore derivada j forma um heap, no sendo necessria nenhuma troca de posio entre as chaves. Apenas sero efetuadas 2 x (n div 2) comparaes (duas para cada subrvore testada), para se certificar de que a rvore um heap. Com vistas simplificao da anlise, consideraremos a diviso exata n/2 ao invs da diviso inteira n div 2. Isto afeta em meia unidade apenas os casos de n mpar. Assim, o nmero de comparaes no melhor caso ser considerado como sendo igual a n e no a 2 (n div 2). Ao contrrio, o pior caso para a formao do heap aquele no qual o vetor j se encontra na ordem desejada. Nesta situao, alm de haver trocas em cada uma das n/2 subrvores examinadas, as trocas sempre afetaro as demais subrvores dos nveis inferiores, at o ltimo nvel, pois as chaves rebaixadas em cada subrvore sero sempre as menores, as quais devero se localizar nas folhas. Assim, teremos as n comparaes seguidas de n/2 trocas, acrescidas daquelas comparaes e trocas referentes ao escorregamento das chaves menores at o nvel das folhas. Para calcularmos esta quantidade extra de comparaes e trocas, podemos usar a tcnica da induo, calculando quantas sero efetuadas a partir de cada nvel da rvore, iniciando pela raiz (nvel 1), obtendo, a seguir, a expresso que generaliza para qualquer nvel. No caso da subrvore da raiz, teremos uma troca para que ela, isoladamente, forme um heap, seguida de log2 (n)-2 trocas correspondentes ao escorregamento da chave rebaixada at o nvel das folhas. No nvel 2, alm das duas trocas correspondentes s duas subrvores deste nvel, teremos 2x(log2(n)-3) trocas devidas ao escorregamento at o nvel das folhas, e assim sucessivamente at o penltimo nvel, no qual somente haver trocas referentes transformao das subrvores em heaps, j que, por formar o ltimo nvel de subrvores, no h propagao.

7.3 Mtodo de Seleo em rvore Amarrada - Threadedheapsort


A fase de manuteno do heap pode ser implementada de forma alternativa. Ao invs de trocar o elemento da raiz com aquele que ocupa a ltima posio do vetor, provocando a necessidade de rearranjar o heap, podemos coloc-lo em um vetor auxiliar, em sua posio correta e, depois, simplesmente promover as chaves sucessoras de cada subrvore a partir da 61

raiz. Tudo se passa como se as chaves sucessoras de cada raiz de subrvore estivessem amarradas umas s outras e, ao removermos a raiz da rvore, a sua sucessora seria puxada pela amarra, ocupando assim o lugar vago da raiz, trazendo, ao mesmo tempo, um nvel acima, as demais sucessoras ligadas pelas amarras. A ltima chave dessa cadeia deixaria um vazio no seu lugar. Quando da implementao, esse vazio pode ser representado por um valor singular de chave, tal como minkey, j utilizado na funo keyval. A seqncia de quadros da figura 7.15a e 7.15b ilustram esse processo, partindo do heap j formado. O valor minkey est representado pelo smbolo (*). Os arcos representam as amarras. A seta apontando para cima sobre a raiz indica que esta ser removida. Abaixo de cada rvore mostrado o vetor de chaves (acima) e o vetor auxiliar (abaixo).

Figura 7.15a: Manuteno do heap por arrasto das chaves sucessoras (1a parte).

Figura 7.15b: Manuteno do heap das chaves sucessoras (2a parte).

O algoritmo que implementa esta alternativa bastante semelhante ao j apresentado, 62

e praticamente auto-explicativo. O procedimento pull up faz o papel das amarras, promovendo a ascenso de cada uma das chaves sucessoras um nvel acima do qual se encontram. J o procedimento threadedheapsort trata de construir o heap inicial (de forma idntica ao heapsort) e, na fase de manuteno, a cada iterao, coloca o elemento da raiz na sua posio correta no vetor auxiliar c a executa a operao pull up. /* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 72 */
void pull_up (c, n) { i = 1; do { if ( keyval(c,n,2*i) > keyval(c, n, 2*i+1) h = 2*i; // amarra sucessor da esquerda else h = 2*i+1; // amarra sucessor da direita chave = keyval(c, n, h); // obtem valor do sucessor c[i] = chave ; // promove um nvel i = h; // desce } while ( chave = minkey); } void threadedheapsort (c, c', n) { i = n div 2; for ( k = i; k > 1 ; k --) // heap (c, n, k); c'[n] = c[1]; for (k = n-1 ; k > 1 ; k --) // { pull_up(c, n); // c'[k] = c[1]; // } }

formao do heap manuteno do heap promove chaves sucessoras coloca a maior chave em sua posio

A anlise da fase de manuteno neste caso mais simples, uma vez que todas as chaves, com exceo daquela que j se encontra na raiz, devero subir desde o nvel onde esto at o nvel 1. Dessa forma, teremos 2 chaves subindo 1 nvel, 4 chaves subindo 2, 8 subindo 3 e assim por diante. Esse valor corresponde quantidade total de nveis que as chaves subiram. Obviamente esse total superior s trocas efetuadas pelo mtodo heapsort em sua fase de manuteno, as quais correspondem a aproximadamente 3/4 do valor que obtivemos. Entretanto, no caso do mtodo threadedheapsort, no se trata de operaes de trocas, mas simples transposies (atribuies) de chaves de um vetor para outro. Devemos ento, para comparar as duas alternativas, computar as atribuies efetuadas em cada caso. Cada troca do heapsort corresponde a trs atribuies, enquanto cada operao de subida de nvel do threadedheapsort corresponde a somente uma atribuio. Assim, o total de atribuies do heapsort expresso por (9/4)n * log2(n/4)+3n enquanto as atribuies do threadedheapsort correspondem a n x log2 (n/4)+n. O que claramente corresponde a uma vantagem para esta alternativa. Alm disso, a quantidade de comparaes efetuadas tambm menor, pois enquanto o heapsort efetua duas comparaes para cada troca, o threadedheapsort efetua apenas uma para cada promoo de nvel. O Quadro 5.6 mostra as quantidades de operaes efetuadas em cada um dos dois mtodos na fase de manuteno do heap.

63

Tabela 7.3: Trocas para manter o heap.

h 1 2 3 4 5 ... m

Total de folhas 1 2 4 8 16 ... 2m-1

Trocas de folhas 0 1 2 3 4 ... m-1

Total de trocas 1*0=0 2*1=2 4*2=8 8 * 3 = 24 16 * 4 = 64 ... m-1 2 * (m-1)

Tabela 7.4: Resumo das comparaes e atribuies efetuadas.

Atribuies Comparaes

Heapsort (9/4)n * log2(n/4)+3n (3/2)n * log2(n/4)

Manuteno do heap

Threadedheapsort n * log2(n/4)+n n * log2(n/4)

A desvantagem do uso dessa alternativa a necessidade de uma rea extra para representar o vetor ordenado.

64

8 Classificao por Distribuio de Chaves


Este mtodo de classificao antecede aos computadores digitais. Ele uma adaptao do processo de classificao mecnica de cartes perfurados, utilizados desde a primeira metade do sculo XX como dispositivo para armazenamento de dados. O modelo mais comum de carto perfurado era aquele no qual podiam ser representados at 80 caracteres, dispostos em colunas, formando, cada um, um registro de um arquivo. Caracteres contguos podiam representar um dado e recebiam a denominao de campo. Desta forma, o carto era formado de um ou mais campos de dados, conforme a aplicao. Antes do advento do computador, o carto perfurado era usado em processos mecanizados, que envolviam basicamente as operaes de tabulao, contagem e totalizao de dados. Como surgimento do computador digital, o carto passou a ser usado tambm como meio de entrada de dados. Em ambos os casos, freqentemente surgia a necessidade de ordenar fisicamente os cartes segundo os valores de um determinado campo de dados, para que pudessem ser processados naquela ordem. A operao de classificao dos cartes era feita por um equipamento que lia a informao perfurada, em uma dada coluna de cada vez, e remetia o carto para um escaninho correspondente ao valor lido. Dessa forma, todos os cartes que possuam o dgito 0 na coluna selecionada eram empilhados no escaninho 0. Os que possuam o dgito 1, no escaninho 1, e assim por diante. Para obter os cartes ordenados pelo dgito da coluna selecionada, bastava recolh-los dos escaninhos na ordem crescente, ou seja, primeiro os do escaninho 0, depois os do escaninho 1 e assim at o 9. Quando o dado a ser classificado ocupava um campo com mais de uma coluna (possua mais de um dgito), era necessrio efetuar tantos passos de ordenaes quantos fossem os dgitos, iniciando-se pelo de menor ordem (o das unidades), prosseguindo-se da em diante at o de mais alta ordem. Aps classificar cada coluna, as pilhas formadas em cada escaninho eram colocadas umas sobre as outras, na ordem crescente, sendo repetido o processo para as colunas seguintes, sucessivamente, at a ltima. Por exemplo, suponhamos que um arquivo formado por 20 cartes tivesse, em um certo campo, os seguintes valores perfurados: 523, 018, 125, 937, 628, 431, 243, 705, 891, 362, 429, 005, 540, 353, 115, 427, 910, 580, 174, 456 A classificao pela coluna que contm o dgito da unidade produziria a seguinte distribuio pelos escaninhos: 115 005 705 125 5

580 910 540 0

353

891 431 1

362 2

243 523 3

174 4

456 6

427 937 7

628 018 8

426 9

Recolhendo os cartes na ordem descrita, obteremos: 540, 910, 580, 431, 891, 362, 523, 243, 353, 174, 125, 705, 005, 115, 456, 937, 427, 018, 628, 429 Classificando agora pela coluna das dezenas obtemos : 65

005 705 0

910

018 115 1

429 628 427 125 523 2

937 431 3

243 540 4

456 353 5

362 6

174 7

580 8

891 9

Recolhendo-os em ordem: 705, 005, 910, 115, 018, 523, 125, 427, 628, 429, 431, 937, 540, 243, 353, 456, 362, 174, 580, 891 E finalmente classificando pela coluna da centena e depois retirando-os em ordem, obtemos os cartes na ordem desejada:

018 005 0

115

174 125 1

243 2

362 352 3

456 431 429 427 4

580 540 523 5

628 6

705 7

891 8

937 910 9

005, 018, 115, 125, 174, 243, 353, 362, 427, 429, 431, 456, 523,540,580,628, 705, 891,910,937 O mesmo processo podia ser aplicado em campos com dados alfanumricos, exigindo no entanto que cada passo fosse desdobrado em duas etapas, j que os caracteres alfabticos eram representados por meio de duplas perfuraes em cada coluna do carto.

8.1 Mtodo de Indexao Direta - Radixsort


A utilizao deste princpio para classificar dados usando um computador aparentemente desvantajosa, pois o processo requer tantos passos de classificao quantos so os dgitos dos dados. Entretanto, como em cada passo somente um dgito do dado considerado, podemos utiliz-lo como um ndice que vai remeter o dado todo para o escaninho correspondente. Ou seja, no ser necessrio efetuar comparaes entre os dados para estabelecer a sua ordem relativa. Resta-nos, assim, construir um mecanismo que simule de maneira eficiente os escaninhos da classificadora de cartes, considerando que no sabemos, a priori, qual a capacidade de empilhamento que deve ter cada um deles. Para isto, temos duas alternativas. A primeira construir uma lista encadeada onde os dados so inseridos medida que so distribudos segundo os valores de seus dgitos. Para simplificar a administrao da lista como um todo, pode-se manter uma lista (pilha) separada para representar cada escaninho, juntamente com um vetor de cabeas que apontam para o topo de cada uma. O valor inicial dos ponteiros null, indicando escaninhos vazios. Para que possamos manter a informao que indica a base de cada pilha, podemos organizar as listas sob forma circular, de modo que o nodo do topo aponte para o nodo da base. A manuteno 66

dessa informao necessria para que possamos, ao final de cada passo, unir as listas formadas, e evita, ao mesmo tempo, a necessidade de manter outro vetor de ponteiros que apontem para as bases das pilhas. medida que os dados vo sendo distribudos, os ponteiros correspondentes vo sendo atualizados, indicando o topo de cada pilha. Aps a distribuio do ltimo dado, as pilhas devem ser concatenadas, formando a lista ordenada pelo dgito em questo. Deve haver um apontador cabea para indicar o incio da lista. Dessa forma, a distribuio das chaves pelos escaninhos pode ser feita utilizando-se o dgito selecionado para enderear o vetor de ponteiros, inserindo-se a chave na posio apontada pelo ponteiro correspondente, o que corresponde a distribuir a chave no topo do seu escaninho. Visando minimizar as operaes de alocao e desalocao dinmica de nodos da lista, os mesmos podem ser reutilizados de um passo para outro, bastando efetuar o ajuste adequado dos ponteiros envolvidos, o que evita o uso de uma rea extra de memria. Para tornar os passos homogneos, conveniente dispor inicialmente as chaves sob a forma de lista encadeada. A segunda alternativa para simular os escaninhos da mquina classificadora contabilizar previamente a quantidade de dados que conter cada um deles. Com esta informao, podemos utilizar uma estrutura de vetor (no encadeado) para representar os escaninhos, j que a posio inicial de cada um deles pode ser calculada. Os ponteiros foram obtidos calculando-se as freqncias acumuladas dos dgitos, e apontam para a primeira posio livre (topo) de cada escaninho. medida que as chaves vo sendo distribudas pelos escaninhos, os ponteiros vo sendo incrementados, passando a indicar as prximas posies livres. Para executar o primeiro passo do exemplo, obtemos as seguintes freqncias de ocorrncia de cada dgito, as quais nos permitem calcular as freqncias acumuladas, que por sua vez indicam a posio inicial de cada escaninho:
Tabela 8.1: Freqncias acumuladas por escaninho.

Dgito freqncia freq. acumulada

0 3 0

1 2 3

2 1 5

3 3 6

4 1 9

5 4 10

6 1 14

7 2 15

8 2 17

9 1 19

Esta segunda alternativa possui a vantagem de dispensar o uso de ponteiros. Por outro lado, exige uma varredura prvia das chaves para contabilizar as freqncias de ocorrncia dos dgitos. Independentemente da alternativa de implementao utilizada, o mtodo possui a vantagem de podermos adaptar a mquina classificadora para as chaves que vo ser ordenadas, caso estas possuam alguma caracterstica especial que possa ser explorada para obter alguma vantagem no processo como um todo. Considere, por exemplo, a classificao de dados que representam os anos de nascimento de um grupo de pessoas. Se considerarmos as chaves como um nmero comum de 4 dgitos cada, teremos de fazer a classificao em 4 passos. Entretanto, podemos observar que apenas no 1 e 2 passo podem ocorrer todos os dgitos de 0 a 9. No 3 passo os dgitos possveis so 8, 9 e 0, enquanto que no 4 apenas o 1 e o 2 so possveis. De posse desta informao, podemos dividir as chaves de forma diferente. Agrupando os dgitos da centena e do milhar, efetuaremos a classificao em trs passos ao invs de 67

quatro, sendo o 1 e 2 normalmente processados, enquanto o 3 considerar os dgitos 18, 19 e 20. Isto possvel, j que podemos construir a mquina classificadora com a quantidade de escaninhos que quisermos, cada um deles com o endereo que tambm quisermos. Nesse caso, para o terceiro passo teramos apenas trs escaninhos para receber as chaves que tiverem os valores 18, 19 e 20 na parte considerada. Certamente isto contribui para melhorar a eficincia do mtodo. Naturalmente, esse mtodo no recomendado para chaves muito longas, como por exemplo nomes de pessoas, pois alm de exigir muitos passos para decidir a ordem, cada parte da chave possui muitas alternativas (as letras do alfabeto, incluindo acentos, maisculas e minsculas). Nesses casos, recomenda-se o uso de mtodos baseados em comparaes entre as chaves, pois muitas vezes apenas a 1 letra do prenome suficiente para decidir a ordem relativa de dois nomes. O procedimento distribuio, apresentado adiante, implementa a alternativa que usa listas encadeadas para representar os escaninhos, e efetua apenas um dos passos do processo de classificao, fazendo a distribuio da chaves segundo a isima parte de cada uma delas. Supe-se que os dados j se encontram sob a forma de uma lista encadeada, cujo primeiro elemento apontado pela varivel incio. Cada nodo da lista um registro de dois componentes: a chave e o ponteiro para o nodo sucessor. Esses componentes so acessados pelas funes key (p) e next (p), respectivamente. O vetor top, de b elementos, indexados de 0 a b-1, contm os ponteiros para os topos das pilhas que representam os escaninhos. A funo parte (i, k) extrai a parte i da chave k. A operao setnext (p, q) atribui o valor q ao componente next do nodo apontado por p. A execuo do procedimento redistribui os nodos da lista de modo que estes se apresentem ordenados segundo a parte i das chaves. A varivel incio aponta para o incio da nova lista. A operao de insero de um nodo em um escaninho vazio est esquematizada na Figura 8.1, e a de um nodo em um escaninho no vazio na Figura 8.2.

Figura 8.1: Insero de um nodo em um escaninho vazio.

Figura 8.2: Insero de um nodo em um escaninho no vazio.

68

void distribuicao ( inicio, i, b) { // inicializacao p = inicio ; if ( p != null ) { for ( j = 0 ; j < b-1 ; j++) top [j] = null; while ( p != null) // distribuicao das chaves { d = parte ( i, key(p) ); // extrai parte i da chave inicio = next(p); if top[d] = null setnext( p, p ); else { // escaninho 'd' no vazio setnext( p, next (top[d]) ); setnext( top[d], p); } top[d] = p; // topo do escaninho 'd' } // concatena os escaninhos d = 0; while ( top[d] = null ) { d = d+1 ; // procura 1o escaninho vazio inicio = next( top[d] ); // inicio de nova lista ant = top[d]; // final provisrio da lista j = d + 1 ; // proximo escaninho do { if ( top[j] != null ) { // concatena setnext( ant, next(top[j]) ); ant = top[j]; setnext(ant, null); // marca fim lista provisria } j = j +1 ; // prximo escaninho } while ( j = b ); } }

/* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 82 */

A implementao da alternativa que utiliza um vetor no lugar da lista encadeada mais simples, por no necessitar lidar com os ponteiros. Para este caso, ser necessrio, no entanto, utilizar um vetor de chaves auxiliar, uma vez que no podemos deslocar as posies dos seus elementos como fizemos no caso da lista encadeada. A implementao, que deixamos a cargo do aluno, est esquematizada a seguir:
func proc distribuicao; { inicializao; clculo das freqncias; clculo das freqncias acumuladas; distribuio das chaves }

Da mesma forma que na alternativa anterior, esse procedimento dever ser repetido para cada parte na qual as chaves foram divididas, atendo-se ao fato de que o resultado de um passo de classificao sempre aparecer no vetor auxiliar.

69

8.1.1 Desempenho Este mtodo apresenta a caracterstica de possuir um desempenho determinstico, ou seja, sempre ser executada a mesma quantidade de operaes para uma dada quantidade n de chaves, independentemente da ordem com que estas se apresentam no incio. Como cada passo demanda exatamente n operaes de distribuio, ento a quantidade total de operaes ser n x np, sendo np o nmero de partes pelas quais as chaves esto divididas. Trata-se, pois, de um mtodo de desempenho linear, ou seja, O (n). Infelizmente, como j mencionado anteriormente, o mtodo no recomendado para chaves longas (np grande). Outra deficincia do mtodo que ele no pode ser aplicado, na forma como foi apresentado, em chaves numricas que possuam valores negativos e positivos misturados. Deixamos para o leitor a discusso deste caso e a proposta de solues.

70

9 Classificao por Intercalao


A classificao por intercalao se baseia em um princpio de funcionamento bastante simples. Consiste em dividir o vetor de chaves a ser classificado em dois ou mais, orden-los separadamente e, depois, intercal-los dois a dois, formando, cada par intercalado, novos segmentos ordenados, os quais sero intercalados entre si, reiterando-se o processo at que resulte apenas um nico segmento ordenado. A Figura 9.1 ilustra esse processo, supondo uma diviso inicial em quatro segmentos.

Figura 9.1 Esquema do processo de intercalao.

Para obter a ordenao dos segmentos iniciais, pode ser usado qualquer mtodo de classificao.

9.1 Mtodo da Intercalao Simples - Mergesort


Outra alternativa iniciar com segmentos de comprimento 1,os quais, por conterem apenas um elemento cada, j esto ordenados. Dessa forma, consideramos, ento, o vetor de n elementos como sendo formado de n segmentos de 1 elemento cada, e aplicamos o processo reiterado de intercalao a partir da, sem a necessidade de lanar mo de outros mtodos de classificao para dar partida ao processo. Estudaremos essa alternativa. A primeira alternativa ser discutida na seo intitulada Intercalao de Arquivos Classificados. A Figura 9.2 mostra um exemplo deste princpio sendo aplicado a um vetor de 8 chaves. Cada linha, a partir da segunda, corresponde ao resultado da intercalao de todos os pares de segmentos consecutivos da linha anterior. 23 17 8 7 17 23 15 8 8 8 17 9 15 15 23 12 9 9 7 15 12 12 9 17 19 7 12 19 7 19 19 23

Figura 9.2: Classificao por intercalao de um vetor de 8 chaves.

Quando o tamanho do vetor no for uma potncia inteira de 2, sempre ocorrer, em pelo menos uma iterao, que um segmento resulte sem um par para ser intercalado. Quando 71

isto ocorrer, esse segmento, que obviamente sempre ser o ltimo do vetor, simplesmente transcrito para a iterao subseqente, como no caso ilustrado na Figura 9.3. 23 17 8 7 7 17 23 15 8 8 8 8 17 9 9 15 15 23 12 10 9 9 7 15 12
12

Figura 9.3 Intercalao com n <> 2i, i inteiro.

12 9 17 14

19 7 12 19 15

7 19 19 23 17

14 10 10 10 19

10 14 14 14 23

9.1.1 Implementao A implementao deste mtodo ser feita em trs nveis. No primeiro, denominado simplemerge, definida a operao de intercalao de um par de segmentos ordenados, resultando um terceiro segmento tambm ordenado. O segundo nvel, mergepass, far a intercalao de todos os pares de segmentos ordenados nos quais o vetor est dividido. O terceiro e ltimo nvel, mergesort, efetuar toda a seqncia de intercalaes necessria para que resulte apenas um segmento intercalado. Esquematicamente, a implementao da operao simplemerge est representada na Figura 9.4. Os dois segmentos a serem intercalados ocupam posies contguas no vetor C. O primeiro segmento inicia na posio isl e termina na fsl. O segundo ocupa as posies de is2 at fs2. O resultado da intercalao aparece no vetor C, a partir da posio r.

Figura 9.4 Esquema da operao simplemerge.

A operao mergepass, por sua vez, efetuar a intercalao de todos os pares consecutivos de segmentos de comprimento L do vetor C.

Figura 9.5: Esquema da operao mergepass.

72

void simplemerge (c, is1, fs1, is2, fs2, c', r) { is1' = is1 ; is2' = is2; k = r; while ( (is1' <= fs1) an (is2' <= fs2) ) // enquanto no atingir o final de { // nenhum dos dois segmentos, compara if c[is1'] < c[is2'] // primeiros elementos de cada segmento { c'[k] = c[is1']; // copia 1o elemento is1' = is1' +1 ; } else { c'[k] = c[is2']; // copia 2o elemento is2' = is2' + 1; } k = k + 1; } if ( is1' > fs1 ) // verifica saldo dos segmentos for ( i = is2' ; i < fs2 ; i ++) // copia resto do 2o segmento { c' [k] = c[i] ; k = k + 1 ; } else for ( i = is1' ; i < fs1 ; i ++ ) // copia resto do 1o segmento { c' [k] = c[i] ; k = k + 1 ; } } void mergepass (c, c', n, L) { // L comprimento dos segmentos p = 1; // p = inicio do 1o segmento q = p + L; // q = inicio do 2o segmento r = 1 ; // r = inicio do segmento resultante while ( q <= n) // enquanto houver pares de segmentos { simplemerge(c, p, q-1, min(q+L-1,n), c', n) //intercala um par segmentos r = r + 2 * L; // proximo segmento resultante p = q + L; // proximo 1o segmento q = p + L; // proximo 2o segmento } if ( p <= n ) // verifica se ltimo segmento possui par for ( i = p ; i < n ; i ++) // se no, transcreve para c'[p..n] c'[i] = c[i] }

/* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 88 */

Observe que o 5 parmetro de chamada da operao simplemerge, que indica a posio final do 2 segmento, prev a possibilidade de que o ltimo deles tenha um comprimento menor do que L. O nvel final de implementao, visvel ao usurio, dever efetuar todas as seqncias de intercalaes, desde as dos segmentos de comprimento 1, at obter um segmento intercalado de comprimento n, dobrando, aps cada execuo da operao mergepass, o comprimento dos segmentos. Devido ao fato da intercalao sempre ser feita em um vetor auxiliar, a operao mergepass dever alternar sucessivamente os vetores de origem e destino. Dessa forma, as primeiras intercalaes sero feitas do vetor C para o C. As segundas de C para C e assim at o final. Ao concluir a ltima seqncia de intercalaes, deve ser verificado em qual dos dois vetores, C ou C, est o resultado final. Isto pode ser feito examinando-se a quantidade de 73

iteraes executadas. Se estiver em C, este copiado para C, uma vez que C no visvel ao usurio.

void mergesort ( c, n) { L = 1 ; while ( L < n ) { if log2 L par mergepass(c, c', n, L); // c -> c' else mergepass(c', c, n, L) // c' -> c L = 2 * L; } if log2 n impar // se ltima intercalao resultou c' for (i = 1 ; i < n ; i++) c[i] = c'[i] }

/* pseudo-cdigo baseado em algoritmo de [Aze 96], Pg 89 */

9.1.2 Anlise do Desempenho Para fazermos a anlise do desempenho deste mtodo, necessrio primeiramente estimarmos o nmero mdio de comparaes efetuadas para intercalar dois segmentos de n chaves cada um. Consideremos, por exemplo, o caso de dois segmentos de 4 chaves cada. O nmero mnimo de comparaes ser 4 quando todos os elementos de um segmento forem menores (ou maiores) do que os do outro. Assim, teremos dois casos que exigiro 4 comparaes. Por outro lado, o mximo ser 7, quando o processo de intercalao tiver de se estender at o ltimo elemento de cada segmento. Este exemplo nos permite verificar que a quantidade de comparaes necessrias para intercalar dois segmentos de comprimento n varia de n at 2n-1. Ocorre que no podemos simplesmente considerar o nmero mdio de comparaes como sendo a mdia aritmtica entre estes dois extremos (o que daria 5,5 para n=4), pois as freqncias de cada caso no so as mesmas. No exemplo que estamos considerando, o caso de 4 comparaes ocorre apenas duas vezes, enquanto o de 7 comparaes ocorre 40 vezes, conforme mostra a Tabela 9.1. Portanto, devemos considerar, isto sim, a mdia das quantidades de comparaes efetuadas, ponderadas pelas freqncias com que cada uma delas ocorre.
Tabela 9.1: Freqncia das comparaes efetuadas para n = 4. Comparaes (C) Freqncia (F) Freqncia ponderada (FxC) 4 2 8 5 8 40 6 20 120 7 40 280 Total 70 448

Temos, pois, uma mdia ponderada de comparaes igual a 448 / 70 = 6,4. Observando as freqncias das quantidades de comparaes efetuadas, para diversos valores de n, verificamos que estas se distribuem segundo o dobro das combinaes (i-1/i-1), para i = n, 2n-1. As freqncias ocorrem no dobro deste valor porque, para cada par de segmento que corresponde a uma permutao possvel de ocorrer, existe o par reciproco, o qual exigir a mesma quantidade de comparaes para ser intercalado. Dessa forma, para qualquer valor de n, podemos tabular as freqncias, de acordo com a Tabela 9.2. 74

Tabela 9.2 Tabulao das freqncias.

O nmero esperado de comparaes necessrias para intercalar dois segmentos de n elementos cada dado, ento, pela relao entre os dois totais da Tabela 7.2. Para expressar essa relao sob a forma de uma funo recorreremos definio de combinaes de ''r'' elementos tomados ''k'' de cada vez, e a trs propriedades das combinaes.

75

10 Listas Ordenadas
Esta seo apresenta uma estrutura de dados auto-ajustvel, no sentido de sempre se manter ordenada aps cada insero ou remoo. Devido a este comportamento altamente dinmico, esta estrutura perfeita para mostrar como a alocao dinmica encadeada pode ser utilizada na implementao de um tipo de dados abstrato. Uma lista ordenada L:[a 1, a 2, ..., a n ] uma lista linear tal que, sendo n > 1, temos: a) a 1 <= a k para qualquer 1 < k <= n; b) a k <= a n para qualquer 1 <= k < n; c) a k 1 <= a k <= a k+1 , para qualquer 1 < k < n. Se L uma lista ordenada, podemos garantir que nenhum elemento em L inferior a 'a1' ou superior a 'an'. Alm disso, tomando um elemento qualquer no meio da lista, nenhum elemento sua esquerda o supera e nenhum elemento sua direita inferior a ele. Entre as diversas operaes que podem ser realizadas com uma lista ordenada, podemos destacar: Ins (Inserir): insere um novo elemento na lista ordenada; Rem (Remover): remove um elemento da lista ordenada; Find (Encontrar): procura um elemento na lista ordenada. Sendo L uma lista ordenada e 'x' um elemento qualquer, a operao Ins ( L, x ) aumenta o tamanho da lista L , acrescentando o elemento 'x' na sua respectiva posio. A operao Rem ( L ) faz com que a lista diminua, se o elemento estiver na mesma, removendo o elemento e retornando verdadeiro. A operao Find ( L, x ) retornar a posio do elemento 'x' na lista. Exerccio 1: Dadas as operaes sobre uma lista ordenada L, preencha o quadro a seguir com estado da lista e o resultado de cada operao: Operao Ins (L, 10) Ins (L, 15) Ins (L, 8) Ins (L, 12) Ins (L, 5) Ins (L, 20) Rem (L, 8) Rem (L, 10) Rem (L, 9) Find (L, 12) Ins (L, 13) Find (L, 15) Find (L, 7) Rem (L, 10) Estado da Lista Ordenada L: [ ] L: [ 10 ] Resultado

10.1 Implementao Encadeada para Listas Ordenadas


E bvia a necessidade de inserir e remover elementos no meio de uma lista ordenada. Para evitar o custo de movimentao de dados que teramos numa implementao seqencial, 76

utilizaremos alocao dinmica encadeada ao implementar as listas ordenadas. Uma lista ordenada L:[a1, a2, a3, ..., an,] pode ser graficamente representada como uma lista encadeada cujos nodos armazenam os elementos a1, a2, a3,.., an .
Figura 10.1: Lista ordenada na representao encadeada.

Como nenhum espao de memria ser alocado at que um elemento seja inserido na lista ordenada, para defini-la, precisamos apenas especificar o formato dos nodos e o tipo de ponteiro que ser usado para criar a varivel L, que aponta o primeiro dos nodos encadeados:
/* define tipo 'elem' baseado em char */ typedef char elem; /* define tipo 'LstOrd' baseado em ponteiro para estrutura nodo */ typedef struct nodo *LstOrd; /* define estrutura nodo: - obj : caracter a ser armazenado - prox: ponteiro para proximo nodo */ struct nodo { elem obj; LstOrd prox; };

[NULL].

Obviamente, uma lista vazia representada por um ponteiro cujo valor nulo: [\] ou

A inicializao e o teste de lista vazia so triviais nesta implementao, uma vez definido que um ponteiro com valor nulo representar uma lista vazia. Para criar uma lista ordenada vazia, vamos usar a operao Criar () e para verificar se uma lista est vazia, usaremos a funo Anular ():
int Criar () { L = NULL ; return(1); } int Anular() { if ( L == NULL ) return(1); else return(0); }

10.1.1 A Operao de Insero Ao inserir um elemento X numa lista L:[a1, a2, a3, ..., an], aps ter sido alocado um nodo apontado por N para armazen-lo, temos quatro casos considerar: a) A lista ordenada L est vazia. Neste caso, o nodo apontado por N passa a ser o primeiro da lista L. Para que isto ocorra, devemos anular o campo de ligao do nodo apontado pois N, de modo a indicar que nenhum elemento alm de X existe na lista. Em seguida, o endereo do nodo apontado por N deve ser copiado para a varivel L, tal que ambos os ponteiros apontem o mesmo nodo. 77

N->prox = NULL ; L = N;

b) O elemento X menor ou igual ao primeiro elemento da lista. O nodo N passa a ser o primeiro da lista, seguido imediatamente pelos nodos que j estavam armazenados em L, isto , indicamos que o nodo apontado por L passa a ser o sucessor do nodo apontado por N e, finalmente, fazemos L apontar para o mesmo nodo apontado por N.

N->prox = L; L= N;

c) O elemento X maior que o primeiro e existe outro que o supera. Neste caso, temos que encontrar na lista L o local correto para realizar a insero. Como no possvel o acesso direto aos nodos da lista encadeada, devemos partir do primeiro nodo e ir seguindo os campos de ligao, um a um, at encontrar o nodo que armazena um elemento ak, que supera X. Claramente, o nodo N dever ser inserido antes de tal elemento.

P = L; while ( x > P->prox->obj ) P = p->prox; N->prox = P->prox; P->prox = N;

d) O elemento X maior que todos os elementos de L. O nodo N ser inserido no final da lista ordenada L. Novamente, partimos do primeiro nodo e seguimos os campos de ligao at encontrar o ltimo, aps o qual o nodo N ser inserido.

P = L; while (P->prox != NULL) P = P->prox; N->prox = NULL; P->prox = N;

78

Analisando os casos descritos acima, observamos uma grande semelhana entre os algoritmos apresentados para os dois primeiros casos. Na verdade, eles so idnticos pois, no primeiro algoritmo, podemos trocar NULL por L. Tambm os dois ltimos casos podem ser descritos por um nico algoritmo. Logo, o algoritmo completo para insero pode ser codificado como a seguir:
int Inserir (elem x) { LstOrd N, P; N = (LstOrd *) malloc ( sizeof (LstOrd) ) ; N->obj = x; if ( ( Anular() ) || (x < L->obj) ) { N->prox = L; L = N; return(1); } /* 1o e 2o casos */

else

{ /* 3o e 4o casos*/ P = L; while ( (P->prox != NULL) && (x > P->prox->obj) ) P = P->prox; N->prox = P->prox; P->prox = N; return(1); }

10.1.2 A operao de Remoo Diferentemente da insero, que teoricamente sempre possvel, a remoo nem sempre pode ser realizada com sucesso. Por exemplo, qual seria o resultado da operao Remover([a,b,c], d), se o elemento d no faz parte da lista? Para sinalizar sucesso ou falha, a operao de remoo ser implementada como uma funo lgica, que retorna verdadeiro ou falso de acordo com o sucesso ou no da operao. Assim como na insero, ao remover um elemento X de uma lista ordenada L, devemos considerar alguns casos importantes: a) A lista ordenada L est vazia. Como o elemento X no pode estar contido numa lista vazia e, conseqentemente, no pode ser removido, a operao resulta em um valor lgico falso. b) O elemento X menor que o primeiro elemento da lista ordenada L. Como a lista L ordenada, se X menor que o primeiro elemento em L, ento concluise que X no est contido na lista e, portanto, no pode ser removido, e a operao resulta em falso. c) O elemento X igual ao primeiro elemento da lista ordenada L. Neste caso, o primeiro nodo da cadeia dever ser liberado e o ponteiro L atualizado de modo a apontar o prximo nodo. Para poder acessar o nodo removido a fim de devolv-lo ao espao de memria livre, antes de deslig-lo da cadeia, precisamos fazer uma cpia do seu endereo. 79

d) O elemento X maior que o primeiro elemento de L. Precisamos encontrar a posio ocupada por X. Partimos do primeiro nodo e, seguindo os campos de ligao, vamos caminhando pela lista at encontrar um elemento ak cujo valor maior ou igual a X. Se tivermos ak=X, ento podemos remov-lo. Caso contrrio, X no se encontra na lista e a operao falha. Tambm pode ocorrer de X ser maior que qualquer valor contido na lista ordenada, e neste caso, constataremos a falha da operao quando atingirmos o ltimo nodo da cadeia e ainda no tivermos encontrado o elemento X. Os dois primeiros casos so triviais, nada precisa ser feito alm de retornar falso como resultado da operao! O terceiro caso ( X=a1) tambm bastante simples de ser resolvido. Na verdade, ns teremos um pouco mais de dificuldade apenas no ltimo caso, mesmo assim, j vimos que procurar um certo elemento numa lista ordenada bastantes simples. O algoritmo completo para remoo em lista ordenada est codificado a seguir. Note que o comando free executado sempre que um elemento removido. Isto faz com que as reas de memria que no so mais necessrias sejam colocadas novamente disposio para futura alocao pelo comando malloc. Desta forma, garantimos que enquanto houver memria no utilizada, novos elementos podero ser inseridos na lista ordenada.
int Remover (elem x) { LstOrd P, Q; if ( else ( Anular() ) || ( x < L->obj) ) return(0); /* 1o e 2o casos */

{ if ( x == L->obj ) /* 3o caso */ { P = L ; L = L->prox; free(P); return(1); } else { P = L; /* 4 caso */ while ( ( P->prox != NULL ) && ( x > P->prox->obj ) ) P = P->prox; if ( ( P->prox != NULL ) && ( x == P->prox->obj ) ) { Q = P->prox; P->prox = Q->prox; free(Q); return(2); } else return(0); } /* fim else x == L->obj */ } /* fim else Anular */ } /* fim funcao */

10.1.3 A Operao de Pesquisa Dada uma lista ordenada L e um elemento X, a operao Procurar(X) verifica em L a existncia de X. Caso o elemento X seja encontrado, a operao resulta num valor atravs do qual possvel acess-lo, seno, um valor especial retornado para sinalizar o fracasso da busca. Assim, quando o elemento for encontrado, verdadeiro ser o valor de retorno da operao, caso contrrio, a funo retornar falso para indicar que a pesquisa falhou:

80

int Procurar (elem x) { LstOrd P; P = L ; while ( ( P != NULL ) && ( x > P->obj ) ) P = P->prox; if ( ( P != NULL ) && ( x == P->obj ) ) return(1); else return(0); }

10.1.4 Imprimindo uma Lista Ordenada Para observar os elementos contidos numa lista ordenada, seria interessante dispor de uma rotina que, dada uma lista L, a imprimisse no vdeo do computador. Atravs desta rotina poderamos ento ter acesso ao conjunto completo de valores armazenados na lista. O formato de impresso a ser escolhido, naturalmente depende do tipo das informaes manipuladas e da aplicao propriamente dita. Por exemplo, se a lista ordenada armazena os nomes dos convidados de uma festa, poderamos imprimir um elemento por linha. Aqui, entretanto, seremos consistentes com a notao j utilizada.
void Mostrar( ) { LstOrd P; P = L ; printf("\nLista: [ "); while ( P != NULL) { printf("%c , ", P->obj); P = P->prox; } printf(" ]"); }

10.1.5 Destruindo uma Lista Ordenada Considere uma rotina dentro da qual utilizada uma lista encadeada cujos nodos so alocados dinamicamente. Para se poder acessar o primeiro nodo da cadeia, certamente ser necessrio usar uma varivel ponteiro local rotina, conforme exemplificado a seguir:
# include <stdio.h> #include <stdlib.h> void main () { LstOrd P; Criar(P); Inserir(b); Inserir(d); Inserir(c); Inserir(a); Mostrar(); }

81

Durante a execuo da rotina, seria criada urna lista encadeada contendo quatro nodos, sendo o endereo do nodo inicial aquele armazenado na varivel P: Como P uma varivel local, ela ser automaticamente destruda ao trmino da execuo da rotina main(). Entretanto, os nodos pertencentes cadeia, sendo variveis dinmicas, somente sero liberados atravs de um comando explcito (free). Desta forma, aps a execuo da rotina, teremos quatro nodos perdidos na memria. Para resolver o problema, vamos desenvolver uma rotina que, dada uma lista encadeada L, libera todos os seus nodos. Devemos lembrar que no possvel liberar diretamente o primeiro nodo, pois perderamos o acesso ao resto da cadeia. Assim, vamos precisar de uma varivel auxiliar P para guardar o endereo de um nodo at que ele possa ser liberado.
int Destruir(void) { LstOrd P; while ( L != NULL) { P = L ; L = L->prox; free(P); } return(1); }

Com esta nova operao, podemos agora liberar todos os nodos de uma lista encadeada, assim que ela no for mais necessria em uma certa aplicao. Vale lembrar que vetores so automaticamente liberados quando saem do escopo em que foram definidos, isto torna desnecessrio definir operaes de destruio para estruturas implementadas com alocao esttica. Entretanto, toda implementao baseada em alocao dinmica deve oferecer uma operao de destruio, que ser chamada explicitamente toda vez que uma varivel criada no for mais necessria na aplicao. De qualquer forma, aps a execuo total de um programa, o sistema operacional recupera automaticamente qualquer espao de memria que tenha sido alocado por ele e que no foi liberado explicitamente. O problema dos nodos perdidos s ocorre durante a execuo do programa, que pode ser interrompida devido insuficincia de memria.

10.2 Tcnicas de Encadeamento


Ao utilizar alocao encadeada, precisamos lanar mo de alguns artifcios que venham facilitar a manipulao das estruturas desenvolvidas. dependendo das operaes a serem efetuadas sobre a lista encadeada, ter simplesmente um ponteiro para o nodo inicial pode no ser o suficiente se esperamos maior eficincia. Este captulo apresenta algumas tcnicas que podero ser muito teis. 10.2.1 Nodos Cabea e Sentinela Um nodo cabea um nodo extra mantido sempre na primeira posio de uma lista encadeada. Ele no usado para armazenar um elemento da lista, tendo como nico objetivo simplificar os procedimentos de insero e remoo. Numa implementao que use nodo cabea, toda lista encadeada tem sempre pelo menos um nodo, desta forma, uma lista L vazia passa a ter outra representao:

82

Figura 10.2: Lista vazia com nodo cabea

Vejamos como esta pequena alterao poder tornar mais simples os procedimentos de insero e remoo em lista ordenada. Comecemos pela operao de inicializao. Nesta nova representao, ela no se resume apenas em anular o ponteiro inicial da cadeia:
int Criar () { L = (LstOrd *) malloc ( sizeof (LstOrd) ) ; L->prox = NULL ; return(1); }

Como toda lista ter sempre um nodo cabea, que nunca pode ser removido, todas as operaes de insero e remoo sero realizadas em nodos a partir da segunda posio.
int Inserir (elem x) { LstOrd N; N = (LstOrd *) malloc ( sizeof (LstOrd) ) ; N->obj = x; while ( (L->prox != NULL) && (x > L->prox->obj) ) L = L->prox; N->prox = L->prox; L->prox = N; return(1); }

Observe que, como os novos elementos no podem entrar antes do nodo cabea, a insero fica reduzida a um nico caso: o elemento ser inserido no meio ou no fim da lista. Assim, basta procurar a posio correta para insero (com o lao while) e atualizar as ligaes. Tambm a remoo ser reduzida a procurar o elemento no resto da lista e atualizar as ligaes relevantes.
int Remover (elem x) { LstOrd P; while ( (L->prox != NULL) && (x > L->prox->obj) ) L = L->prox; if ( (L->prox != NULL) && (x == L->prox->obj) ) { P = L->prox; L->prox = P->prox; free(P); return(1); } else return(0); } /* fim funcao */

Dependendo da implementao, o nodo cabea pode ser usado para guardar 83

informaes gerais a respeito da lista encadeada. Um caso bastante comum o uso do nodo cabea para armazenar o nmero de elementos contidos na lista. Naturalmente, cada vez que um nodo inserido ou removido, o campo de dados do nodo cabea deve ser atualizado.

Figura 10.3: Lista L:[a, b, c, d] com nodo cabea contador.

Um nodo sentinela tambm um nodo extra, porm este mantido na ltima posio da lista e armazena um valor especial chamado high value (HV), que deve ser o mximo entre todos aqueles que podem fazer parte da lista ordenada em questo. Por exemplo, supondo uma lista cujos elementos so caracteres, o valor HV seria o cdigo do ltimo caractere da tabela ASCII: unsigned char HV = 255; Nodos cabea e sentinela, em conjunto, tornam os algoritmos ainda mais simples:

Figura 10.4: Lista vazia com nodos cabea e sentinela.

Para inicializar a lista, precisamos alocar e encadear os dois nodos iniciais da estrutura:
int Criar () { L = (LstOrd *) malloc ( sizeof (LstOrd) ) ; L->prox = (LstOrd *) malloc ( sizeof (LstOrd) ) ; L->prox->obj = HV; L->prox = NULL ; return(1); }

A rotina a seguir, implementa a operao de insero em listas ordenadas com nodos cabea e sentinela. Observe que a introduo do nodo sentinela permite eliminar o teste que verifica se o final da lista j foi atingido. Como HV mximo, certamente o teste X>L->prox>obj tornar-se- falso antes que seja atingido o fim da lista encadeada!
int Inserir (elem x) { LstOrd N; N = (LstOrd *) malloc ( sizeof (LstOrd) ) ; N->obj = x; while ( x > L->prox->obj ) L = L->prox; N->prox = L->prox; L->prox = N; return(1); }

Nada impede que um elemento inserido na lista tenha o mesmo valor de HV. Entretanto, ao remover um nodo da cadeia, devemos nos certificar de que ele no seja o nodo sentinela. Para isto que serve o teste L->prox->prox != NULL; qualquer nodo pode armazenar o valor de HV, mas somente o nodo sentinela tem um ponteiro nulo!

84

int Remover (elem x) { LstOrd P; while ( x > L->prox->obj ) L = L->prox; if ( (L->prox->prox != NULL) && (x == L->prox->obj) ) { P = L->prox; L->prox = P->prox; free(P); return(1); } else return(0); } /* fim funcao */

Tambm, durante a operao de pesquisa, precisamos ter certeza de que o nodo encontrado no o nodo sentinela, j que ele no faz parte dos elementos da lista.

10.3 Encadeamento circular


Numa lista com encadeamento circular, ao invs do campo de ligao do ltimo nodo armazenar um endereo nulo, ele armazena o endereo do primeiro nodo:

Figura 10.5: Lista encadeada circular

Como o endereo do primeiro nodo pode ser facilmente obtido atravs do campo de ligao do ltimo nodo, a varivel ponteiro para uma lista circularmente encadeada guarda no o endereo do primeiro, mas sim do ltimo nodo da cadeia. A grande vantagem em se usar encadeamento circular que podemos acessar rapidamente tanto o primeiro quanto o ltimo nodo da lista. Na implementao no circular, acessar o ltimo elemento requer a passagem por todos os elementos da cadeia, um a um, at atingir o ltimo. Por exemplo, se resolvemos implementar filas com listas encadeadas, a operao de remoo seria rpida (primeiro elemento), entretanto a operao de insero seria bastante lenta (ltimo elemento). Veja a seguir, a implementao de filas usando listas circularmente encadeadas. Primeiro vamos definir a organizao dos dados:
typedef char elem; typedef struct nodo *Fila; struct nodo { elem obj; Fila prox; };

A inicializao e teste de fila vazia so triviais na implementao com listas circulares. Note que no h necessidade de teste de fila cheia, j que estamos usando alocao dinmica de memria: 85

void IniciaFila ( Fila F) { F = NULL; } int FilaVazia (Fila F) { if (F == NULL) return(1); else return(0);

Ao ser inserido na fila, um novo nodo dever sempre armazenar o endereo do primeiro elemento na lista encadeada. Se a fila estava vazia antes da insero, ento o novo nodo ter que apontar para si mesmo.
int Inserir (Fila F, elem x) { Fila N; N = (LstOrd *) malloc ( sizeof (LstOrd) ) ; N->obj = x; if ( FilaVazia(F) { F = N; N->prox } else { N->prox F->prox F = N; } return(1); ) = N;

= F->prox; = N;

Ao remover um elemento, devemos estar atentos para o caso de a fila ter somente o elemento que vai ser removido, pois, neste caso, a fila tornar-se- vazia.
elem Retirar (Fila F) { Fila P; if ( ! FilaVazia(F) ) { if ( F == F->prox ) { Retirar = F->obj; free(F); F = NULL; } else { P = F->prox; F->prox = P->prox Retirar = P->obj; free(P); } } else

86

printf("Underflow !");

Analise o cdigo apresentado e veja como o encadeamento circular tornou a implementao muito mais eficiente do que ela seria se tivssemos usado uma lista encadeada comum. 10.4 Encadeamento Duplo Numa lista com encadeamento duplo, cada nodo tem dois campos de ligao sendo que um deles armazena o endereo do nodo predecessor e o outro armazena o endereo do sucessor:

Figura 10.6: Lista duplamente encadeada.

Sendo cada nodo da forma (esq, obj, dir), dado um ponteiro P que armazena o endereo de um nodo qualquer no meio da lista, vale a seguinte igualdade: P = P->esq->dir = P->dir->esq . Por exemplo, na figura 10.6, se partimos do ponteiro P e vamos para a esquerda e depois para a direita, retornamos ao mesmo nodo de partida. Assim, esta igualdade caracteriza a principal propriedade de uma lista duplamente encadeada, que a facilidade de retroceder ou avanar na cadeia, a partir de um determinado nodo, com a mesma eficincia. Entretanto, a igualdade no se verifica se P estiver apontando um nodo extremo da lista. Por exemplo, se P aponta o primeiro nodo da lista, que no tem predecessor, ento no possvel ir para a esquerda e depois voltar direita. Por este motivo, listas duplamente encadeadas normalmente so implementadas de forma circular; pois, neste caso, an precede a1 e a1 sucede an. A existncia de um nodo sentinela garante a validade da igualdade mesmo estando a lista vazia e, portanto, torna a estrutura ainda mais uniforme.

Figura 10.7: Encadeamento duplo circular com sentinela.

Em termos prticos, uma lista duplamente encadeada permite o acesso aos seus nodos tanto da esquerda para a direita quanto da direita para a esquerda. Supondo uma lista ordenada, com a estrutura apresentada na figura 10.7, ento podemos acessar os seus elementos tanto em ordem crescente quanto decrescente, sempre com a mesma eficincia. Se o encadeamento duplo tem a desvantagem de gastar mais memria, por outro lado, tem a vantagem de ser mais flexvel e de tornar as operaes bsicas mais elegantes. Sendo P um ponteiro para um nodo qualquer no meio de uma lista duplamente encadeada, e N um ponteiro para um novo nodo a ser inserido antes de P, podemos usar as seguintes instrues: N->esq = P->esq; N->dir = P; P->esq->dir = N; P->esq = N;

87

Para remover o nodo apontado por P, fazemos: P->esq->dir = P->dir ; p->dir->esq = P->esq; free (P);

88

Bibliografia
[Aze 96] Azeredo, Paulo A. . Mtodo de Classificao de dados e anlise de suas complexidades. Editora Campus. Rio de Janeiro. 1996. [Per 96] Pereira, Silvio Lago. Estrutura de dados Fundamentais: conceitos e aplicaes. Editora rica. So Paulo. 1996 [Szw 94] Szwarcfiter, Jaime Luiz, Markenzon, Lilian. Estruturas de dados e seus algoritmos. Editora Livros Tecnicos e Cientficos. Rio de Janeiro. 1994. [Ten 95] Tenenbaum, Aaron M.. Estruturas de dados usando C. Editora Makron Books, So Paulo.1995. [Vel 83] Veloso, Paulo, Santos, Clesio dos, Azeredo, Paulo, Furtado, Antonio. Estrutura de Dados. Editora Campus, Rio de Janeiro. 1998.

89

Vous aimerez peut-être aussi