2. Sumário
1. Introdução
1.1 Tipo de dados Abstracto -TDA
1.2 Ponteiros
1.3 Funções
1.4 Estruturas e tipos de dados definidos pelo programador
2. Listas Lineares
2. 1 Listas Sequenciais
2.2 Listas ligadas
2.3 Listas Circulares
2.4 Listas duplamente ligadas
2.5 Pilhas
2.5.1Implementação sequencial da pilha
2.5.2Implementação dinâmica da pilha
2.6 Filas
2.6.1Implementação sequencial da fila
2.6.2Implementação dinâmica da fila
3. 3. Árvores
3.1 Introdução
3.2 Representação de árvores
3.2 Definição
3.3 Terminologia
3.4 Árvores binárias
3.5 Árvores balanceadas
3.6 Definição da estrutura de dados
4. Ordenação
4.1 Ordenação por Selecção
4.2 Ordenação por Inserção
4.3 Shellsort
4.4 Quicksort
4.5 Heapsort
5. Pesquisa
5.1 Pesquisa Sequencial
5.2 Pesquisa Binária
5.3 Árvores de Pesquisa
4. Capitulo I: Introdução
Sumário:
1.1Tipo de Dado Abstracto (TDA)
1.2Ponteiros
1.3Funções
1.4Estruturas
1.5 Tipos de dados definidos pelo
programador
5. 1.1 Tipo de Dados Abstracto -
TDA
Tipos de Dados
◦ Tipo de dados de uma variáveis
define o conjunto de valores que a variável pode
assumir.
Especifica a quantidade de bytes que deve ser
reservadas a ela.
◦ Tipos de dados podem ser vistos como métodos
para interpretar o conteúdo da memória do
computador
◦ Podemos ver o conceito de tipo de dados de uma
outra perspectiva: não em termos do que um
computador pode fazer (interpretar os bits …) mas
em termos do que o utilizador desejam fazer
(somar dois inteiros…)
6. 1.1 Tipo de Dado Abstracto - TDA
Tipo de Dado Abstracto
◦ Agrupa a estrutura de dados juntamente com as operações
que podem ser feitas sobre esses dados
◦ O TDA encapsula a estrutura de dados. Os utilizadores do
TDA só tem acesso a algumas operações disponibilizadas
sobre esses dados
◦ Utilizador do TDA x Programador do TDA
Utilizador só “enxerga” a interface, não a sua
implementação
Dessa forma, o utilizador pode abstrair da implementação
específica.
Qualquer modificação nessa implementação fica restrita
ao TDA
◦ A escolha de uma representação específica é fortemente
influenciada pelas operações a serem executadas
7. 1.1 Tipo de Dado Abstracto -
TDA
Estrutura de Dados - ED
◦ É um método particular de se implementar um
TDA
◦ Cada ED é construída dos tipos primitivos
(inteiro, real, char, …) ou dos tipos compostos
(array, registo, …) de uma linguagem de
programação.
◦ Algumas operações realizadas em TDA são:
criação, inclusão, busca e remoção de dados.
Exemplos de TDA
◦ Lineares: Não Lineares:
Listas ligadas . Árvores
Pilhas . Grafos
Filas
8. TDA-Exemplo
Existem vários métodos para especificar um TDA. O
método que usaremos é semi-formal e faz uso
intensivo da notação de C. Para ilustrar o conceito de
um TDA e método de especificação, examinemos o
TDA RACIONAL que corresponde ao conceito
matemático de um número racional.
Um número racional é o que pode ser expresso como o
quociente de dois inteiros.
As operações sobre números racionais que definimos
são:
◦ Criação de um número racional
◦ Adição
◦ Multiplicação
◦ Teste de igualdade.
9. TDA-Exemplo
//definição de valor
abstract typedef <integer, integer> RACIONAL;
condiction RACIONAL [1] <> 0;
//definição de operador
abstract RACIONAL criaracional(a, b)
int a, b;
precondition b <> 0;
postcondition criaracional[0]== a;
criaracional[1]== b;
abstract RACIONAL soma (a, b)
RACIONAL a, b;
postcondition soma[1]== a[1]*b[1];
soma[0]==a[0]*b[1]+b[0]*a[1];
10. abstract RACIONAL mult(a, b)
RACIONAL a, b;
postcondition mult[0]==a[0]*b[0];
mult[1]==a[1]*b[1];
abstract RACIONAL igual(a,b)
RACIONAL a, b;
postcondition igual==(a[0]*b[1]==a[1]*b[0]);
Exercício:
Crie o TODA para o conjunto de números complexos com todas
as suas operações básicas.
11. 1.2 Apontadores
Na realidade, C permite que o
programador referencie a posição de
objectos bem como os próprios
objectos (isto é, o conteúdo de suas
posições)
12. 1.2 Apontadores
• O objectivo:
– Armazenar o endereço de outra variável.
• Sintaxe
tipo * ptr
• ptr - é o nome da variável do tipo apontador
• tipo – o tipo da variável para qual apontará.
• * - indica que é uma variável do tipo apontador
• Exemplo 1:
char *pc;
int *pi;
float *pf
13. 1.2 Apontadores
Um ponteiro é como qualquer outro tipo de dado
em C.
O valor de um ponteiro é uma posição de
memória da mesma forma que o valor de um
inteiro é um número.
Os valores dos ponteiros podem ser atribuídos
como qualquer outro valor.
A conversão de pf do tipo “ponteiro para um
número de ponto flutuante” para o tipo “ponteiro
para um inteiro” pode ser feita escrevendo-se:
pi = (int *) pf;
Se pi é um ponteiro para um inteiro, então pi +1 é
o ponteiro para o inteiro imediatamente seguinte
ao inteiro *pi em memória, pi–1 é o ponteiro para
o inteiro imediatamente anterior a *pi.
14. Apontadores
Por exemplo, suponha que
determinada máquina usa
endereçamento de bytes, que um
inteiro exija 4 bytes e que valor de pi
seja 100 (isto é, pi aponta para inteiro
*pi na posição 100). Sendo assim, o
valor de pi–1 é 96, o valor de pi+1 é
104.
De modo semelhante, se o valor da
variável pc é 100 e um caractere tem
um byte, pc – 1 refere-se a posição 99
e pc+1 à posição 101.
15. Apontadores
Uma área na qual os ponteiros x =5
de C desempenham um printf(“%dn”, x) // imprime 5
notável papel é passagem de
parâmetro para funções. func(x); // imprime 6
Normalmente, os parâmetros printf(“%dn”, x) // imprime 5
são passados para uma …
função em C por valor, isto é, func(y)
os valores sendo passados
são copiados nos parâmetros int y
da função de chamada no {
momento em que a função for ++y;
chamada. Se o valor de um
printf(“%dn”, y);
parâmetro for alterado dentro
da função, o valor no }
programa chamada não será
modificado.
16. Se precisar usar func para modificar o valor de x, precisaremos passar
o endereço de x
x =5
printf(“%dn”, x) // imprime 5
func(&x); // imprime 6
printf(“%dn”, x) //imprime 6
…
func(py)
int *py
{
++(*py);
printf(“%dn”, *py);
}
17. 1.2.1 Inicialização automática de
apontadores
• Um bom hábito para evitar problemas
de programação é a inicialização
sempre dos apontadores
• A constante simbólica NULL, quando
colocada num apontador, indica que
este não aponta para nenhuma
variável.
int x = 5;
float pi = 3.1415;
int px = &x;
float * ppi = π
18. 1.3 Funções
Conceito:
◦ É uma unidade autónoma de código do
programa e é desenhada para cumprir
uma tarefa particular.
Sintaxe
tipo nome_da_função (parametros){
comandos;
}
19. 1.3 Funções
• Quando uma função não retorna um valor para função que a
chamou ela é declarada como void.
• Ex:
#include<stdio.h>
void linha (char ch, int size){
int i;
for (i=1; i size; i++)
putchar(ch);
}
main(){
char lin;
int tamanho;
printf(“Introduza o tipo de linha”);
scanf(“%c”, &lin);
printf(“Introduza o tamanho da linha”);
scanf(“%d”, &tamanho);
linha(lin, tamanho);
getchar();
}
20. 1.3 Funções
1.3.3 Função com retorno
O tipo de retorno tem que ser
declarada. main(){
int b, e;
Ex1 : printf(“Introduza a base e
int potencia (int base, int expoente”);
expoente){ scanf(“%d%d”, &b, &e);
int i, pot=1; printf(“valor=%dn”,
If (expoente<0) potencia(b,e));
getchar();
return;
}
for (i=1; i <= expoente;
i++)
pot=pot*base;
Return pot;
}
21. Estrutura de dados e tipo de dados definido pelo
utilizador
Estruturas são peças contíguas de armazenamento que contém
vários tipos simples agrupados numa entidade.
Estrutura são manifestadas através da palavra reservada struct
seguida pelo nome da estrutura e por uma área delimitada por
colchetes que contém campos.
struct pessoa {
char nome[30];
int idade;
char sexo;
};
Para criação de novos tipos de estrutura de dados utiliza-se a
palavra-chave: typedef
typedef struct pessoa {
char nome[30];
int idade;
char sexo;
} PESSOA;
22. Estrutura
Para obter directamente o valor de um campo, a
forma é <nome_da_estrutura>.<campo>.
Para obter o valor de um campo apontado por um
ponteiro a forma é <nome_da_estrutura> ->
<campo>
Ex
…
PESSOA func, *estud, &docente = func ;
func.nome=“Adriano”; //atribui o valor Adriano para o campo nome
func.idade=25; //atribui o valor 25 para o campo idade
func.sexo=“M”; //atribui o valor M para o campo sexo
estud=&func; //faz estud apontar para func
estud->nome=“Joana”; //muda o valor do campo nome para Joana
estud ->idade =22; //muda o valor do campo idade para 22
estud ->sexo =“F”; //muda o valor do campo sexo para F
docente.Nome=“Pedro”; //Referências usam a notação de ponto
…
23. Estruturas Recursivas
Estruturas podem ser recursivas, i.e.,
podem conter ponteiros para si
mesmas. Esta peculiaridade ajuda a
definir estruturas do tipo lista.
struct lista{
struct lista *prox;
int dado;
}
25. Listas
Listas são uma colecção de objectos,
onde os itens podem ser adicionados
em qualquer posição arbitrária.
26. Listas
Propriedades
• Uma lista pode ter zero ou mais
elementos
• Um novo item pode ser adicionado
a lista em qualquer ponto
• Qualquer item da lista pode ser
eliminado
• Qualquer item da lista pode ser
acessado
27. Listas - Formas de
Representação
Sequencial
◦ Explora a sequencialidade da memória do computador, de tal
forma que os nós de uma lista sejam armazenados em
endereço sequencias. Podem ser representado por um vector
na memória principal ou um arquivo sequencial em disco.
L Adão Adelin Basto Daniel Josefin …
0 1a 2 s 3 a
4 5 MAX-1
Ligada
◦ Esta estrutura é tida como uma sequencia de elementos
ligados por ponteiros, ou seja, cada elemento deve conter,
além do dado propriamente dito, uma referencia para o
próximo elemento da lista.
Adão Adelina Bastos Daniel Josefina
L
28. Listas sequenciais
Uma lista representada de forma sequencial é um
conjunto de registos onde estão estabelecidas
regras de precedência entre seus elementos.
Implementação de operações pode ser feita
utilizando array e registo, associando o elemento a[i]
com o índice i.
Características
◦ Os elementos na lista estão armazenados
fisicamente em posições consecutivas
◦ A inserção de um elemento na posição a[i] causa
o deslocamento a direita do elemento a[i] ao
último.
◦ A eliminação do elemento a[i] requer o
deslocamento à esquerda do a[i+1] ao último.
29. Listas Sequenciais
Vantagens
◦ Acesso directo a qualquer dos elementos da lista.
◦ Tempo constante para aceder o elemento i - (depende
somente de índice)
Desvantagens
◦ Para inserir ou remover elementos temos que deslocar
muitos outros elementos
◦ Tamanho máximo pré-estimado
Quando usar
◦ Listas pequenas
◦ Inserção e remoção no fim da lista
◦ Tamanho máximo bem definido
30. Listas sequenciais
Operações básicas
Definição
#define MAX 50 //tamanho máximo da lista
typedef char elem[20]; // tipo base dos elementos da lista
typedef struct tlista{
elem v[MAX];//vector que contém a lista
int n; //posição do último elemento da lista
};
Tlista
Adão Alberto Ana Daniela Carmen …
n=5 v
0 1 2 3 4 5 6 MAX – 1
1.Criar uma lista vazia 2. Verificar se uma lista está vazia
void criar(tlista *L) { int vazia(tlista L) {
L -> n = 0; return (L .n == 0);
} }
31. Listas sequenciais
Operações básicas
3. Verificar se uma lista está cheia 4. Obter o tamanho de uma lista
int cheia( tlista L) { int tamanho ( tlista L){
return (L .n == MAX); return (L.n);
} }
5. Obter o i-ésimo elemento de uma 6. Pesquisar um dado elemento,
lista retornando a sua posição.
int elemento( tlista L, int pos, elem *dado ) int posicao( tlista L, elem dado )
{ {
if ((pos > L.n) || (pos <=0)) int i;
return (0); for (i=1; i<L.n; i++)
*dado = L.v [pos - 1]; if(L.v[i-1] == dado)
return 1; return i;
} return 0;
}
32. Listas Sequenciais
Operações básicas
7. Inserção de um elemento em uma 8. Remoção do elemento de uma
determinada posição determinada posição
(requer o deslocamento à direita dos (requer o deslocamento à esquerda dos
elementos v(i+1) … v(n)) elementos v(p+1) … v(n))
// retorna 0 se a posição for inválida ou se a /* o parâmetro dado irá receber o elemento
lista encontrado.
// estiver cheia, caso contrário retorna 1 Retorna 0 se a posição for inválida , caso
contrário
retorna 1 */
int inserir( tlista *L, int pos, elem dado )
{
int i; int remover( tlista *L, int pos, elem *dado )
if (( L->n == MAX) || ( pos > L->n + 1 )) {
return (0); int i;
for (i = L->n; i >= pos; i--) if ( ( pos > L->n) || (pos <= 0) )
L->v[i] = L->v [i-1]; return (0);
L->v[pos-1] = dado; *dado = L-> v[pos-1];
(L->n)++; for (i = pos; i <= (L->n) - 1; i++)
return (1); L->v[i-1] = L->v [i];
} (L->n)--;
return (1);
}
33. Listas Ligadas
Uma lista ligada ou lista encadeada é uma estrutura de dados
linear e dinâmica. Ela é composta por células que apontam
para o próximo elemento da lista.
Para "ter" uma lista ligada/encadeada, basta guardar seu
primeiro elemento, e seu último elemento aponta para uma
célula nula.
Vantagens
◦ A inserção ou a retirada de um elemento na lista não implica na mudança de lugar de
outros elementos;
◦ Não é necessário saber, anteriormente, o número máximo de elementos que uma lista
pode ter, o que implica em não desperdiçar espaço na Memória Principal (Memória
RAM) do computador.
Desvantagens
◦ manipulação torna-se mais "perigosa" uma vez que, se o encadeamento (ligação) entre
elementos da lista for mal feito, toda a lista pode ser perdida;
◦ Para acessar o elemento na posição n da lista, deve-se percorrer os n - 1 anteriores.
34. Listas ligadas
As listas ligadas são conjunto de
elementos ligados, onde cada elemento
contem uma ligação com um ou mais
elementos da lista.
Uma lista ligada tem necessariamente uma
variável ponteiro apontando para o seu
primeiro elemento. Essa variável será
utilizada sempre, mesmo que esteja vazia,
e deverá apontar sempre para o início da
lista.
Cada elemento da lista ligada será
composta por duas/três partes principais:
uma parte conterá informações e a(s)
35. Listas Ligadas - Propriedades
Uma lista pode ter zero ou mais
elementos
Um novo item pode ser adicionado a
lista em qualquer ponto
Qualquer item da lista pode ser
eliminado
Qualquer item da lista pode ser
acessado.
36. Listas Ligadas
Uma lista ligada é composta por
elementos alocadas em memória.
Utiliza-se duas funções para alocar
(desalocar) memória dinamicamente:
◦ malloc()
◦ free()
Ex:
L = (lista) malloc(sizeof(lista));
free(L);
37. Lista Simplesmente Ligada
Cada elemento da lista possui uma única
conexão. Assim, cada elemento será
formado por um bloco de dados e um
ponteiro para próximo elemento.
Chamaremos cada elemento nó.
Um nó é um conjunto de informações de
tipos diferentes, portanto ela é uma estrutura
definida pelo programador:
struct no{
int dado;
struct no *prox; //ponteiro para o próximo
nó
}
38. Listas Simplesmente Ligadas
Sendo que uma memória alocada
dinamicamente não possui o nome
como no caso das variáveis
declaradas no programa, toda lista
deverá ter uma variável que apontará
para o seu primeiro elemento.
struct no *head; //cabeça da lista
head=null; //inicia a lista com nulo
39. Listas Simplesmente Ligadas
Podemos nos referenciar a uma lista
ligada através do seu primeiro nó.
Entretanto, toda vez que realizarmos
uma inserção no início da lista
teremos que garantir que todas as
partes do programa que referenciam a
lista tenha suas referencias
actualizadas. Para evitar este
problema, é comum usar um nó
especial chamado nó cabeça, e um
ponteiro para o primeiro elemento da
lista.
40. Listas Simplesmente Ligadas
A implementação em C de uma lista
simplesmente ligada com nó cabeça.
Precisamos de dois tipos de dados:
◦ Um tipo nó, representa cada elemento da
lista.
◦ e um tipo lista que contém um ponteiro
para nó cabeça.
41. Listas Simplesmente Ligadas
typedef struct no{
int dado; // o dado poderia ser de qualquer tipo
struct no *prox; // ponteiro para próximo
elemento
} NO
typedef struct lista{
NO *head;
}LISTA
42. Listas Simplesmente Ligadas
As operações básicas sobre a estrutura são: criação da lista e de nó.
1. Criação de lista
LISTA *criaLista(){
LISTA *L;
L=(LISTA *)malloc(sizeof(LISTA));
L->head = NULL;
return L;
}
2. Criação de nó
NO *criaNo(int dado){
NO *no;
no = (NO *)malloc(sizeof(NO));
no->dado = dado;
no->prox = NULL;
return no;
}
43. Listas Simplesmente Ligadas
Ao contrário dos vectores, onde acessamos qualquer
elemento usando um índice numérico, com uma lista
ligada temos apenas acesso sequencial, por exemplo,
para chegar ao decimo elemento temos que passar por
todos nove elementos anteriores.
Para percorrer uma lista, precisamos obter seu primeiro
elemento e então obtemos os elementos subsequentes
seguindo os ponteiros de próximo elemento em cada
nó.
NO *primeiro(LISTA *L){
return (L->head);
}
NO *proximo(NO *no){
return (no->prox);
}
44. Listas Simplesmente Ligadas
Andar para trás em uma lista simplesmente ligada não é tão
simples, requer que a lista seja percorrida desde o início verificado
se, para cada elemento x, proximo(x) é o elemento do qual
procuramos o elemento anterior.
NO *anterior(LISTA *L, NO *no){
NO *p;
if(no==L->head)
return NULL;
p = primeiro(L);
while(proximo(p)!=NULL){
if(proximo(p) == no)
return p;
p = proximo(p);
}
return NULL;
}
45. Listas Simplesmente Ligadas
Obter o último elemento também requer que
toda a lista seja percorrida.
NO *ultimo(LISTA *L){
NO *p;
p = primeiro(L);
if (p == NULL)
return NULL;
while(proximo(p)!=NULL)
p = proximo(p);
return p;
}
46. Listas Simplesmente Ligadas
Podemos querer 3 tipos de inserções em uma lista:
◦ no início,
◦ no fim,
◦ ou em uma posição arbitrária.
Na lista simplesmente ligada a inserção no início é trivial:
Fazemos o próximo do novo elemento apontar para o
primeiro elemento da lista e fazemos com que o novo
elemento seja o primeiro elemento (cabeça) da lista.
A inserção em posição arbitrária pode ser feito em tempo
constante se tivermos um ponteiro para o elemento que será
o elemento anterior ao novo elemento:
void insere_apos(NO *novo, NO *anterior){
novo->prox = anterior->prox;
antrior->prox = novo;
}
A inserção no final da lista exige que encontremos
seu último elemento.
47. Listas Simplesmente Ligadas
Remoção de um elemento arbitrário requer que
encontremos o elemento anterior ao removido.
void remove(LISTA *lista, NO *no){
NO *ant;
if(no == lista->head){
lista->head = no->prox;
} else{
ant = anterior(lista, no)
if(ant!=NULL)
ant->prox = no->prox;
}
free(no);
}
48. Listas duplamente ligadas
Listas duplamente ligadas são usadas
frequentemente nos problema que
exija uma movimentação eficiente
desde um nó quer para o seu
antecessor, quer para o seu sucessor.
frent
NULL
e valor valor valor
NULL Atrás
49. Listas duplamente ligadas
Uma das virtudes de listas
duplamente ligadas, é o facto de nos
podemos deslocar para o nó anterior
ou para nó seguinte, a um nó dado
com facilidade.
Uma lista duplamente ligada permite-
nos remover um nó qualquer da lista
em tempo constante, usando apenas
um ponteiro para essa célula.
50. Listas duplamente ligadas -
operações
anterior(): devolve o elemento anterior a
um determinado elemento.
cria(): devolve uma lista com um
elemento
insere(): insere um determinado
elemento na lista.
mostra(): mostra o conteúdo da lista.
procura(): devolve a posição de um
determinado elemento.
remove(): remove o elemento desejado
da lista.
51. Lista duplamente Ligada -
operações
#include<stdio.h>
#include<stdlib.h>
typedef struct listadl{
LDL *prox; //próximo elemento da lista
LDL *ant; //elemento anterior da lista
int dado;
}LDL;
52. Lista duplamente Ligada -
operações
Devolve uma lista com elemento elem.
LDL *cria(int elem){
LDL*tmp;
tmp =(LDL *)malloc(sizeof(tmp));
tmp->dado = elem;
tmp->prox = NULL;
tmp->ant = NULL;
return tmp;
}
53. Lista duplamente Ligada -
operações
Insere o elemento elem na lista L.
void *insere (int elem, LDL *L){
LDL*tmp;
tmp=L;
while((tmp->prox ==NULL)==0)
tmp = tmp->prox;
tmp->prox = cria(elem);
(tmp->prox)-> ant = tmp;
}
54. Lista duplamente Ligada -
operações
Imprime todos os elementos da if((no1 == NULL)==0)
lista.
printf(“O proximo:%d”,
void *mostra(LDL *L){ no1->dado);
LDL*tmp, *no1, *no2; else
tmp=L; printf(“Não possui
while((tmp->prox próximo.”);
==NULL)==0){ if((no2 == NULL)==0)
no1 = tmp->prox; printf(“O anterior:%d”,
no2 = tmp->ant; no2->dado);
printf(“O elemento: %d”, else printf(“Não possui
tmp->dado); anterior.”);
tmp= tmp->prox;
}
}
55. Lista duplamente Ligada -
operações
Remove o elemento elem da lista L.
else
LDL *remove(int elem, LDL *L){
if((proxno == NULL)==0){
LDL*tmp, *proxno, *noant;
proxno->ant =NULL;
tmp=L;
while((tmp->prox ==NULL)==0){
free(tmp);
proxno = tmp->prox;
L = proxno;
noant = tmp->ant; return L;
if((tmp->dado == elem) }
if((noant == NULL)==0) else {
if((proxno == NULL)==0){ L = NULL;
noant->prox =proxno; free(tmp);
proxno->ant =noant; return L;
free(tmp); }
return L; else{
} tmp = tmp -> prox;
else{ }
noant->prox=NULL;
printf(“O elemento %d não se
free(tmp);
return L;
encontra na listan”, elem);
} }
56. Lista duplamente Ligada -
operações
Devolve a posição do elemento elem.
int *procura(int elem, LDL *L){
LDL*tmp;
int pos = 0;
tmp=L;
while((tmp->prox ==NULL)==0)
if((tmp->dado) == elem){
printf(“A posição do elemento %d é %dn”, elem, pos);
return pos;
}
else{
pos++;
tmp = tmp->prox;
}
printf(“O elemento não se encontra na lista”);
}
57. Lista duplamente Ligada -
operações
Devolve o elemento anterior a elem.
void *anterior(int elem, LDL *L){
LDL*tmp, *noant;
tmp=L;
while((tmp==NULL)==0)
if((tmp->dado) == elem){
noant = tmp->ant;
printf(“O elemento anterior a %d é %dn”, elem, noant-
>dado);
else
printf(“Não existe anterior”);
exit();
}
else
tmp = tmp->prox;
printf(“O elemento não se encontra na lista”);
}
58. Lista Circular Ligada
As listas circulares são do tipo simples ou
duplamente ligadas, em que o nó final da lista a
ponta para o nó inicial. Este tipo de estrutura
adapta-se a situações do tipo FIFO (First In First Out).
Fig. 1 Lista circular ligada ( lista simplesmente ligada)
dado dado dado dado
Fig. 2 Lista circular ligada ( lista duplamente ligada)
dado dado dado dado
59. Lista circular Ligada -
operações
typedef struct no{ 3. Aloca e desaloca nó.
int dado;
int aloca(LCSL *lista, int valor){
struct no *prox;
}NO, LCSL;
LCSL *tmp;
NO cabeca; tmp= (LCSL)
malloc(sizeof(no));
1. Inicializa a lista circular if (tmp==NULL)
int inicializa(LCSL *cabeca) return 1; // Erro …
{ *lista = tmp;
*cabeca=NULL;
tmp->dado = valor;
return 0;
} tmp->prox = NULL;
2. Verifica se a lista está cheia return 0;
}
int vazia(LCSL lista){ void liberta(LCSL *lista){
return (lista==NULL)? 1:0; free(*lista);
}
*lista=NULL;
}
60. Lista circular Ligada -
operações
4. Inserção de um item 5. Adiciona um item ao fim da
int insere(LCSL *lista, int valor) lista.
{ int adiciona(LCSL *lista, int
LCSL tmp; valor){
if (aloca(&tmp, valor)== 1) LCSL tmp;
return 1; // Erro if (aloca(&tmp, valor)== 1)
if (vazia(*lista) == 1){ return 1; // Erro
//True=1 if (vazia(*lista) == 1) //True=1
tmp->prox = tmp;
tmp->prox = tmp;
*lista=tmp; else{
} else { tmp->prox = *lista->prox;
*lista->prox = tmp;
tmp->prox = *lista->prox; }
*lista->prox = tmp; *lista = tmp;
} return 0; //Ok
return 0;
} }
61. Lista circular Ligada -
6. Eliminação de um nóoperações
int apagaNo(LCSL *lista, LCSL no)
{
LCSL tmp;
if(vazia(*lista) == 1)
return 1;
if(no==no->prox)
*lista=NULL;
else {
for(tmp=*lista->prox; tmp!=*lista && tmp->prox!=no; tmp=tmp-
>prox) ;
if(tmp->prox !=no)
return 1;
tmp->prox = no->prox;
if(no== *lista)
*lista=tmp;
}
free(&no);
return 0;
}
62. Pilha e Fila
1. Pilha
• Implementação sequencial da pilha
2. Fila
I. Operações básicas
II. Implementação sequencial da fila
63. Pilhas
Pilhas são listas onde a inserção ou a
remoção de um item é feita no topo.
Definição:
dada a pilha P= (a[1], a[2], …, a[n]),
dizemos que a[1] é o elemento da base da
pilha; a[n] é o elemento topo da pilha; e
a[i+1] está acima de a[i].
Pilhas são conhecidas como listas LIFO
(last in first out)
64. Implementação de pilhas
Como lista sequencial ou ligada?
◦ No caso geral de listas ordenadas, a maior
vantagem da alocação dinâmica sobre a
sequencial, se a memória não for problema, é
a eliminação de deslocamento na inserção ou
eliminação dos elementos. No caso das pilhas,
essas operações de deslocamento não
ocorrem. Portanto, alocação sequencial é mais
vantajosa na maioria das vezes.
◦ Como o acesso a pilha é limitado ao último
elemento inserido, todos os elementos do meio
do vector devem estar preenchidos. Há
desvantagens quando a quantidade de dados
a ser armazenados não é conhecido
67. Implementação Sequencial
1. Inicializa a pilha vazia
int inicializa(PILHA *pp){
pp->topo = pp->base;
return 1;
}
2. Verifica se pilha está vazia
int vazia(PILHA *pp){
return (pp->topo == pp->base) ? 1 : 0;
}
3. Coloca dado na pilha. Apresenta erro se não haver espaço.
int push(PILHA *pp, int dado){
if(tpilha(pp) == MAXPILHA)
return 0;
*pp->topo=dado;
pp->topo++;
return 1;
}
68. Implementação Sequencial
4. Retira o valor do topo da pilha a atribui a dado.
int pop (PILHA *pp, int dado){
if(vazia(pp) == 1)
return 0;
pp->topo--;
*dado = *pp->topo
return 1;
}
5. Retorna o valor do topo da pilha se removê-lo.
int topo(PILHA *pp, int *dado){
if(pop(pp, dado) == 0)
return 0;
return push (pp, dado)
}
69. Fila
Uma Fila é uma estrutura de dados do
tipo FIFO (First In First Out), cujo
funcionamento é inspirado no de uma
fila “natural”, na qual o primeiro
elemento a ser inserido é sempre o
primeiro elemento a ser retirado.
70. Fila
Uma fila tem por norma as seguintes
funcionalidade:
Colocar e retirar dados da fila:
◦ insere – guardar um elemento na fila
◦ remove – retirar um elemento da fila
◦ topo – retornar o elemento do topo da fila.
Testar se a fila está vazia ou cheia:
◦ cheia – Verificar se a fila está cheia (não pode
guardar mais elementos).
◦ vazia – Verificar se a fila está vazia (não contém
elementos)
Inicializar ou limpar:
◦ inicializa – Colocar a fila num estado “pronto” a ser
utilizada
71. Fila – Outras Operações
Buscar por elementos que coincidam com certo
padrão (casamento de padrão)
Ordenar uma lista
Intercalar duas listas
Concatenar duas listas
Dividir uma lista em duas
Fazer cópia da lista
72. Fila
A implementação de uma fila pode ser
efectuada através da utilização de
diferentes estruturas de dados
(vectores, listas ligadas, árvores, etc.).
De seguida, apresenta-se duas
implementação de uma fila através da
utilização de vectores e listas ligadas.
73. Fila
Características das filas:
◦ Os dados são armazenados pela ordem de
entrada
Tipos de filas:
◦ Filas de espera (queues)
com duplo fim (deque “double-ended queue)
◦ Filas de espera com prioridades (priority
queues)
Implementação das filas:
◦ usando vectores/arrays (circulares ou não)
◦ utilizando um apontador para nós de
74. Fila
Implementação em C usando arrays
Vantagens:
◦ Facilidade de implementação.
Desvantagens:
◦ Vectores possuem um espaço limitado para
armazenamento de dados.
◦ Necessidade de definir um espaço grande o suficiente
para a fila
◦ Necessidade de um indicador para o inicio e para o fim da
fila
Método de Implementação
◦ A adição de elementos à fila resulta no incremento do
indicador do fim da fila
◦ A remoção de elementos da fila resulta no incremento do
indicador do inicio da fila
75. Implementação de Filas em C
Usamos o vector para armazenar os
elementos da fila e duas variáveis, inic e fim,
para armazenar as posições dentro do vector
do primeiro e último elementos da fila:
#define MAXFILA 100
struct fila{
int elem[MAXFILA];
int inic, fim;
}f;
É evidente que usar vector para armazenar
uma fila introduz a possibilidade de estouro,
caso a fila fique maior que o tamanho do
vector.
77. Sumário
3.1 Introdução
3.2 Tipos de árvores
3.3 Árvores binárias
3.3.1 Estrutura de uma árvore binária
3.3.2 Descrição
3.3.3 Altura
3.3.4 Representação em C
3.4 Árvores Genéricas
3.4.1 Estrutura de uma árvore genérica
3.4.2 Representação em C
3.4.3 Problemas com Representação
78. Introdução
As estruturas anteriores são chamadas
de unidimensionais (ou lineares)
◦ Exemplo são listas sequenciais e ligadas
Não podem ser usadas como
hierarquias.
◦ Exemplo: árvore de directórios
Árvore é uma estrutura adequada para
representar hierarquias
A forma mais natural para definirmos
uma estrutura de árvore é usando
79. Estrutura de Árvores
Uma árvore é composta por um conjunto de
nós.
Existe um nó r, denominado raiz, que contém
zero ou mais sub-árvores, cujas raízes são
ligadas a r.
Esses nós raízes das sub-árvores são ditos
filhos do nó pai, no caso r.
Nós com filhos são chamados nós internos e
nós que não têm filhos são chamados folhas,
ou nós externos.
É tradicional desenhar as árvores com a raiz
para cima e folhas para baixo, ao contrário do
que seria de se esperar.
80. Estrutura de Árvores
Por adoptarmos essa forma de representação gráfica,
não representamos explicitamente a direcção dos
ponteiros, subentendendo que eles apontam sempre do
pai para os filhos.
81. Tipos de Árvores
O número de filhos permitido por nó e as
informações armazenadas em cada nó
diferenciam os diversos tipos de árvores
existentes.
Existem dois tipos de árvores:
◦ árvores binárias, onde cada nó tem, no
máximo, dois filhos.
◦ árvores genéricas, onde o número de filhos é
indefinido.
Estruturas recursivas serão usadas como
base para o estudo e a implementação
das operações com árvores.
82. Árvore Binária
Um exemplo de utilização de árvores
binárias está na avaliação de
expressões.
Como trabalhamos com operadores
que esperam um ou dois operandos, os
nós da árvore para representar uma
expressão têm no máximo dois filhos.
Nessa árvore, os nós folhas
representam operandos e os nós
internos operadores.
84. Estrutura de uma AB
Numa árvore binária, cada nó tem
zero, um ou dois filhos.
De maneira recursiva, podemos
definir uma árvore binária como
sendo:
◦ uma árvore vazia; ou
◦ um nó raiz tendo duas sub-árvores,
identificadas como a sub-árvore da direita
(sad) e a sub-árvore da esquerda (sae).
85. Estrutura de uma AB
• Os nós a, b, c, d, e, f formam
uma árvore binária da seguinte
maneira:
• A árvore é composta do nó a,
da subárvore à esquerda
formada por b e d, e da sub-
árvore à direita formada por c,
e e f.
• O nó a representa a raiz da
árvore e os nós b e c as raízes
das sub-árvores.
• Finalmente, os nós d, e e f
são folhas da árvore.
86. Descrição de AB
Para descrever árvores binárias,
podemos usar a seguinte notação
textual:
◦ A árvore vazia é representada por <>,
◦ e árvores não vazias por <raiz sae sad>.
Com essa notação, a árvore da
Figura anterior é representada por:
◦ <a<b<><d<><>>> <c<e<><>><f<><>>>>.
87. Descrição de AB
Uma sub-árvore de uma árvore
binária é sempre especificada como
sendo a sae ou a sad de uma árvore
maior, e qualquer das duas sub-
árvores pode ser vazia.
As duas árvores da Figura abaixo são
distintas.
88. Altura de uma AB
Uma propriedade fundamental de todas
as árvores é que só existe um caminho
da raiz para qualquer nó.
Podemos definir a altura de uma árvore
como sendo o comprimento do caminho
mais longo da raiz até uma das folhas.
A altura de uma árvore com um único nó
raiz é zero e, por conseguinte, dizemos
que a altura de uma árvore vazia é
negativa e vale -1.
89. Representação em C
Podemos definir um tipo para representar
uma árvore binária.
Vamos considerar que a informação a ser
armazenada são valores de caracteres
simples.
Vamos inicialmente discutir como podemos
representar uma estrutura de árvore binária
em C.
Que estrutura podemos usar para
representar um nó da árvore?
Cada nó deve armazenar três informações:
a informação propriamente dita, no caso um
caractere, e dois ponteiros para as sub-
árvores, à esquerda e à direita.
90. Representação em C
struct arv {
char info;
struct arv* esq;
struct arv* dir;
};
typedef struct arv Arv;
Da mesma forma que uma lista encadeada é
representada por um ponteiro para o
primeiro nó, a estrutura da árvore como um
todo é representada por um ponteiro para o
nó raiz.
97. Exibe Conteúdo da Árvore
void imprime (Arv* a)
{
if (!vazia(a)){
printf("%c ", a->info); /* mostra raiz */
imprime(a->esq); /* mostra sae */
imprime(a->dir); /* mostra sad */
}
}
Como é chamada essa forma de exibição?
E para exibir na forma in-fixada? E na pós-
fixada?
98. Liberando Memória
Arv* libera (Arv* a){
if (!vazia(a)){
libera(a->esq); /* libera sae */
libera(a->dir); /* libera sad */
free(a); /* libera raiz */
}
return NULL;
}
99. Criação e Liberação
Vale a pena notar que a definição de
árvore, por ser recursiva, não faz
distinção entre árvores e sub-árvores.
Assim, cria pode ser usada para
acrescentar uma sub-árvore em um
ramo de uma árvore, e libera pode ser
usada para remover uma sub-árvore
qualquer de uma árvore dada.
100. Criação e Liberação
Considerando a criação da árvore feita
anteriormente,
podemos acrescentar alguns nós, com:
a->esq->esq = cria('x',
cria('y',inicializa(),inicializa()),
cria('z',inicializa(),inicializa())
);
E podemos liberar alguns outros, com:
a->dir->esq = libera(a->dir->esq);
Deixamos como exercício a verificação do
resultado
final dessas operações.
101. Buscando um Elemento
Essa função tem como retorno um
valor booleano (um ou zero)
indicando a ocorrência ou não do
carácter na árvore.
int busca (Arv* a, char c){
if (vazia(a))
return 0; /* não encontrou */
else
return a->info==c ||busca(a->esq,c)
||busca(a->dir,c);
}
102. Buscando um Elemento
Podemos dizer que a expressão:
return c==a->info || busca(a->esq,c) ||
busca(a->dir,c);
é equivalente a:
if (c==a->info)
return 1;
else if (busca(a->esq,c))
return 1;
else
return busca(a->dir,c);
103. Árvores Genéricas
Como vimos, numa árvore binária o
número de filhos dos nós é limitado em
no máximo dois.
No caso da árvore genérica, esta
restrição não existe.
Cada nó pode ter um número arbitrário
de filhos.
Essa estrutura deve ser usada, por
exemplo, para representar uma árvore
de directórios.
Supor que não há árvore vazia.
105. Representação em C
Devemos levar em consideração o
número de filhos que cada nó pode
apresentar.
Se soubermos que numa aplicação o
número máximo de filhos é 3, podemos
montar uma estrutura com 3 campos
apontadores, digamos, f1, f2 e f3.
Os campos não utilizados podem ser
preenchidos com o valor nulo NULL,
sendo sempre utilizados os campos em
ordem.
Assim, se o nó n tem 2 filhos, os
campos f1 e f2 seriam utilizados, nessa
ordem, ficando f3 vazio.
106. Representação em C
Prevendo um número máximo de filhos
igual a 3,
e considerando a implementação de
árvores para
armazenar valores de caracteres simples,
a declaração do tipo que representa o nó
da árvore poderia ser:
struct arv3 {
char val;
struct arv3 *f1, *f2, *f3;
};
108. Problemas com
Representação
Como se pode ver no exemplo, em cada um dos
nós que tem menos de três filhos, o espaço
correspondente aos filhos inexistentes é
desperdiçado.
Além disso, se não existe um limite superior no
número de filhos, esta técnica pode não ser
aplicável.
O mesmo acontece se existe um limite no
número de nós, mas esse limite será raramente
alcançado, pois estaríamos tendo um grande
desperdício de espaço de memória com os
campos não utilizados.
Há alguma solução para contornar tal
problema?
109. Solução
Uma solução que leva a um aproveitamento
melhor do
espaço utiliza uma “lista de filhos”: um nó aponta
apenas
para seu primeiro (prim) filho, e cada um de seus
filhos,
excepto o último, aponta para o próximo (prox)
irmão.
A declaração de um nó pode ser:
struct arvgen {
char info;
struct arvgen *prim;
struct arvgen *prox;
};
typedef struct arvgen ArvGen;
111. Exemplo da Solução
Uma das vantagens dessa representação
é que podemos percorrer os filhos de um
nó de forma sistemática, de maneira
análoga ao que fizemos para percorrer os
nós de uma lista simples.
Com o uso dessa representação, a
generalização da árvore é apenas
conceitual, pois, concretamente, a árvore
foi transformada em uma árvore binária,
com filhos esquerdos apontados por prim
e direitos apontados por prox.
112. Criação de uma ÁrvoreGen
ArvGen* cria (char c)
{
ArvGen *a =(ArvGen
*)malloc(sizeof(ArvGen));
a->info = c;
a->prim = NULL;
a->prox = NULL;
return a;
}
113. Inserção
Como não vamos atribuir nenhum significado
especial para a posição de um nó filho, a
operação de inserção pode inserir a sub-
árvore em qualquer posição.
Vamos optar por inserir sempre no início da
lista que, como já vimos, é a maneira mais
simples de inserir um novo elemento numa
lista ligada.
void insere (ArvGen* a, ArvGen* f)
{
f->prox = a->prim;
a->prim = f;
}
115. Exemplo Criação ÁrvoreGen
/* cria nós como folhas */
ArvGen* a = cria('a'); /* monta a hierarquia */
ArvGen* b = cria('b'); insere(c,d);
ArvGen* c = cria('c'); insere(b,e);
ArvGen* d = cria('d'); insere(b,c);
ArvGen* e = cria('e'); insere(i,j);
ArvGen* f = cria('f'); insere(g,i);
ArvGen* g = cria('g'); insere(g,h);
ArvGen* h = cria('h'); insere(a,g);
ArvGen* i = cria('i'); insere(a,f);
ArvGen* j = cria('j'); insere(a,b);
...
117. Busca de Elemento
int busca (ArvGen* a, char c)
{
ArvGen* p;
if (a->info==c)
return 1;
else {
for (p=a->prim; p!=NULL; p=p->prox) {
if (busca(p,c))
return 1;
}
}
return 0;
}
118. Liberação de Memória
O único cuidado que precisamos
tomar na programação dessa função
é a de liberar as subárvores antes
de liberar o espaço associado a um
nó (isto é, usar pós-ordem).
119. Liberação de Memória
void libera (ArvGen* a){
ArvGen* p = a->prim;
while (p!=NULL) {
ArvGen* t = p->prox;
libera(p);
p = t;
}
free(a);
}
120. Capitulo IV - Ordenação
Docente: Dikiefu Fabiano, Msc
121. Sumário
4. Ordenação Interna
4.1 Ordenação por Selecção
4.2 Ordenação por Inserção
4.3 Shellsort
4.4 Quicksort
4.5 Heapsort
121
122. Conceitos Básicos
Ordenar: processo de rearranjar um conjunto de
objectos em uma ordem ascendente ou descendente.
A ordenação visa facilitar a recuperação posterior de
itens do conjunto ordenado.
A maioria dos métodos de ordenação é baseada em
comparações das chaves.
Existem métodos de ordenação que utilizam o princípio
da distribuição.
.
122
123. Conceitos Básicos
Notação utilizada nos algoritmos:
• Os algoritmos trabalham sobre os
registos de um arquivo.
• Cada registo possui uma chave
utilizada para controlar a ordenação.
• Podem existir outros componentes
em um registo
124. Conceitos básicos
Um método de ordenação é estável se a ordem
relativa dos itens com chaves iguais não se altera
durante a ordenação.
• Alguns dos métodos de ordenação mais eficientes
não são estáveis.
• A estabilidade pode ser forçada quando o método é
não-estável.
• Sedgewick (1988) sugere agregar um pequeno índice
a cada chave antes de ordenar, ou então aumentar a
chave de alguma outra forma.
124
125. Classificação dos métodos de
ordenação
1. Ordenação interna: arquivo a ser ordenado cabe todo
na memória principal.
2. Ordenação externa: arquivo a ser ordenado não cabe
na memória principal.
Diferenças entre os métodos:
Em um método de ordenação interna, qualquer
registo pode ser imediatamente cessado.
Em um método de ordenação externa, os registos
são cessados sequencialmente ou em grandes blocos.
125
126. Classificação dos métodos de ordenação
interna
Métodos simples:
• Adequados para pequenos arquivos.
• Produzem programas pequenos.
Métodos eficientes:
• Adequados para arquivos maiores.
• Usam menos comparações.
• As comparações são mais complexas nos
detalhes.
Métodos simples são mais eficientes para pequenos
arquivos.
126
127. Ordenação por Selecção
Um dos algoritmos mais simples de ordenação.
Algoritmo:
Seleccione o menor item do vector.
Troque-o com o item da primeira posição do vector.
Repita essas duas operações com os n − 1 itens restantes, depois
com os n − 2 itens, até que reste apenas um elemento.
• As chaves em negrito sofreram uma troca entre si.
127
128. Ordenação por Selecção
Vantagens:
Custo linear no tamanho da entrada para o número
de movimentos de registos.
É o algoritmo a ser utilizado para arquivos com
registos muito grandes.
É muito interessante para arquivos pequenos.
Desvantagens:
O facto de o arquivo já estar ordenado não ajuda em
nada, pois o custo continua quadrático.
O algoritmo não é estável.
128
129. Ordenação por Selecção
void selectionSort(int vector[],int tam) {
int i, j, min, aux;
for(i=0; i<tam-1; i++) {
min = i;
aux = vector[i];
for(j=i+1; j<tam; j++) {
if (vector[j] < aux) {
min=j; aux=vector[j];
}
}
aux = vector[i];
vector[i] = vector[min];
vector[min] = aux;
}
}
129
131. Bubble Sort
O bubble sort, ou ordenação por
flutuação (literalmente "por bolha"), é
um algoritmo de ordenação dos mais
simples. A ideia é percorrer o vector
diversas vezes, a cada passagem
fazendo flutuar para o topo o maior
elemento da sequência. Essa
movimentação lembra a forma como
as bolhas em um tanque de água
procuram seu próprio nível, e disso
vem o nome do algoritmo
132. void bubble( int v[], int qtd ) {
int i; int j, aux, k = qtd - 1 ;
for(i = 0; i < qtd; i++) {
for(j = 0; j < k; j++) {
if(v[j] > v[j+1]) {
aux = v[j];
v[j] = v[j+1];
v[j+1]=aux;
}
}
}
}
133. void swapbubble( int v[], int i ) {
aux = v[i];
v[i] = v[i+1];
v[i+1]=aux;
}
void bubble( int v[], int qtd ) {
int i,j;
for( j = 0; j < qtd; j++ ) {
for( i = 0; i < qtd - 1; i++ ) {
if( v[i] > v[i+1] ) {
swapbubble( v, i );
}
}
}
}
134. Método de ordenação Bolha com ordenação de strings.
void bubble(int v[], int qtd){
int i, trocou;
char aux;
do {
qtd--;
trocou = 0;
for(i = 0; i < qtd; i++)
if(strcasecmp(v[i],v[i + 1])>0) {
strcpy(aux, v[i]);
strcpy(v[i], v[i + 1]);
strcpy(v[i + 1], aux);
trocou = 1;
}
}while(trocou==1);
}
135. Ordenação por Inserção
Método preferido dos jogadores de cartas.
Algoritmo:
• Em cada passo a partir de i =2 faça:
• Seleccione o i-ésimo item da sequência fonte.
• Coloque-o no lugar apropriado na sequência destino de
acordo com o critério de ordenação.
• As chaves em negrito representam a sequência destino.
135
136. Ordenação por Inserção
void insertionSort(int vector[], int tam) {
int i, j; int key;
for (j = 1; j < tam; ++j) {
key = vector[j];
i = j - 1;
while (vector[i] > key && i >= 0) {
vector[i+1] = vector[i];
--i;
}
vector[i+1] = key;
}
}
A colocação do item no seu lugar apropriado na
sequência destino é realizada movendo-se itens com
chaves maiores para a direita e então inserindo o item
na posição deixada vazia.
136
137. Ordenação por Inserção
Considerações sobre o algoritmo:
O processo de ordenação pode ser
terminado pelas condições:
• Um item com chave menor que o item
em consideração é encontrado.
• O final da sequência destino é atingido à
esquerda.
Solução:
• Utilizar um registo sentinela na posição
zero do vector.
137
138. Ordenação por Inserção
O número mínimo de comparações e movimentos
ocorre quando os itens estão originalmente em
ordem.
O número máximo ocorre quando os itens estão
originalmente na ordem reversa.
É o método a ser utilizado quando o arquivo está
“quase” ordenado.
É um bom método quando se deseja adicionar
uns poucos itens a um arquivo ordenado, pois o
custo é linear.
O algoritmo de ordenação por inserção é estável.
138
139. Shellsort
Proposto por Shell em 1959.
É uma extensão do algoritmo de ordenação por
inserção.
Problema com o algoritmo de ordenação por
inserção:
• Troca itens adjacentes para determinar o ponto
de inserção.
• São efectuadas n − 1 comparações e
movimentações quando o menor item está na
posição mais à direita no vector.
O método de Shell contorna este problema
permitindo trocas de registos distantes um do
outro.
139
140. Shellsort
Os itens separados de h posições são
rearranjados.
Todo h-ésimo item leva a uma sequência
ordenada.
Tal sequência é dita estar h-ordenada.
Exemplo de utilização:
• Quando h = 1 Shellsort corresponde ao algoritmo de 140
141. Shellsort
• Como escolher o valor de h:
Sequência para h:
h(s) = 3h(s − 1) + 1, para s > 1
h(s) = 1, para s = 1.
Knuth (1973, p. 95) mostrou experimentalmente
que esta sequência é difícil de ser batida por mais
de 20% em eficiência.
A sequência para h corresponde a 1, 4, 13, 40, 121,
364, 1.093, 3.280, . . .
141
142. Shellsort
void shellsort ( Int v [ ] , int n) {
int i,j,x,h = 1;
do h = h *3 + 1;
while (h < n) ;
do {
h /= 3;
for ( i = h + 1; i <= n; i ++) {
x = v[ i ] ; j = i ;
while (v[ j − h] > x > 0) {
v[ j ] = v[ j − h] ; j −= h;
i f ( j <= h) break;
}
v[ j ] = x;
}
} while (h != 1) ;
}
A implementação do Shellsort não utiliza registos
sentinelas.
Seriam necessários h registos sentinelas, uma para
cada h-ordenação. 142
143. Shellsort
Vantagens:
• Shellsort é uma óptima opção para
arquivos de tamanho moderado.
• Sua implementação é simples e requer uma
quantidade de código pequena.
Desvantagens:
• O tempo de execução do algoritmo é
sensível à ordem inicial do arquivo.
• O método não é estável,
143
144. Quicksort
Proposto por Hoare em 1960 e publicado em
1962.
É o algoritmo de ordenação interna mais rápido
que se conhece para uma ampla variedade de
situações.
Provavelmente é o mais utilizado.
A ideia básica é dividir o problema de ordenar um
conjunto com n itens em dois problemas menores.
Os problemas menores são ordenados
independentemente.
Os resultados são combinados para produzir a
solução final.
144
145. Quicksort
A parte mais delicada do método é relativa ao método
partição.
O vector v[esq..dir ] é rearranjado por meio da escolha
arbitrária de um pivô x.
O vector v é particionado em duas partes:
• A parte esquerda com chaves menores ou
iguais a x.
• A parte direita com chaves maiores ou iguais a
x.
145
146. Quicksort
Algoritmo para o particionamento:
1. Escolha arbitrariamente um pivô x.
2. Percorra o vector a partir da esquerda até que v[i]
x.
3. Percorra o vector a partir da direita até que v[j] x.
4. Troque v[i] com v[j].
5. Continue este processo até os apontadores i e j se
cruzarem.
• Ao final, o vector v[esq..dir ] está particionado de
tal forma que:
• Os itens em v[esq], v[esq + 1], . . . , v[j] são
menores ou iguais a x.
• Os itens em v[i], v[i + 1], . . . , v[dir ] são
maiores ou iguais a x.
146
147. Quicksort
• Ilustração do processo de partição:
O pivô x é escolhido como sendo v[(i+j) / 2].
Como inicialmente i = 1 e j = 6, então x = v[3] = D.
Ao final do processo de partição i e j se cruzam em i =
3e j = 2.
147
148. Quicksort
void swap(int* a, int* b) {
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
int partition(int vec[], int left, int right) {
int i, j;
i = left;
for (j = left + 1; j <= right; ++j) {
if (vec[j] < vec[left]) {
++i;
swap(&vec[i], &vec[j]);
}
}
swap(&vec[left], &vec[i]);
return i;
}
148
149. Quicksort
Método ordena e algoritmo Quicksort :
void quickSort(int vec[], int left, int right) {
int r;
if (right > left) {
r = partition(vec, left, right);
quickSort(vec, left, r - 1);
quickSort(vec, r + 1, right);
}
}
149
150. Implementação usando 'fat pivot':
void sort(int array[], int begin, int end) {
int pivot = array[begin];
int i = begin + 1, j = end, k = end, t;
while (i < j) {
if (array[i] < pivot) i++;
else if (array[i] > pivot) {
j--;
k--;
t = array[i];
array[i] = array[j];
array[j] = array[k];
array[k] = t;
151. } else {
j--;
swap(array[i], array[j]);
}
}
i--;
swap(array[begin], array[i]);
if (i - begin > 1)
sort(array, begin, i);
if (end - k > 1)
sort(array, k, end);
}
152. Lembrando que quando você for chamar
a função recursiva terá que chamar a
mesma da seguinte forma
ordenar_quicksort(0,n-1). O 0(zero)
serve para o início receber a posição
zero do vector e o fim será o tamanho
do vector -1.
153. void ordenar_quicksort(int ini, int fim) {
int i = ini, f = fim;
char pivo[50];
strcpy(pivo,vector[(ini+fim)/2]);
if (i<=f) {
while (strcmpi(vector[i],pivo)<0)
i++;
while (strcmpi(vetor[f],pivo)>0)
f--;
if (i<=f) {
strcpy (aux_char,vetor[i]);
strcpy (vetor[i],vetor[f]);
strcpy (vetor[f],aux_char);
i++; f--;
}
}
154. if (f>ini)
ordenar_quicksort_nome(ini,f);
if (i<fim)
ordenar_quicksort_nome(i,fim);
}
155. Quicksort
Exemplo do estado do vector em cada chamada
recursiva do procedimento Ordena:
• O pivô é mostrado em negrito.
155
156. Quicksort
Vantagens:
• É extremamente eficiente para ordenar
arquivos de dados.
• Necessita de apenas uma pequena pilha como
memória auxiliar.
Desvantagens:
• Sua implementação é muito delicada e difícil:
Um pequeno engano pode levar a efeitos
inesperados para algumas entradas de dados.
• O método não é estável.
156
157. Heapsort
Possui o mesmo princípio de funcionamento da
ordenação por selecção.
Algoritmo:
1. Seleccione o menor item do vector.
2. Troque-o com o item da primeira posição do
vector.
3. Repita estas operações com os n − 1 itens
restantes, depois com os n − 2 itens, e assim
sucessivamente.
O custo para encontrar o menor (ou o maior) item
entre n itens é n − 1 comparações.
Isso pode ser reduzido utilizando uma fila de
prioridades.
157
158. Heaps
É uma sequência de itens com chaves c[1], c[2], . . .
, c[n], tal que:
c[i] c[2i],
c[i] c[2i + 1], para todo i = 1, 2, . . . , n/2.
A definição pode ser facilmente visualizada em uma
árvore binária completa:
árvore binária completa:
• Os nós são numerados de 1 a n.
• O primeiro nó é chamado raiz.
• O nó k/2 é o pai do nó k, para 1 < k n.
• Os nós 2k e 2k + 1 são os filhos à esquerda e à
direita do nó k, para 1 k k/2
158
159. Heaps
As chaves na árvore satisfazem a condição do
heap.
As chaves em cada nó são maiores do que as
chaves em seus filhos.
A chave no nó raiz é a maior chave do conjunto.
Uma árvore binária completa pode ser
representada por um arranjo:
A representação é extremamente compacta.
Permite caminhar pelos nós da árvore facilmente.
Os filhos de um nó i estão nas posições 2i e 2i + 1.
O pai de um nó i está na posição i/2. 159
160. Heaps
Na representação do heap em um arranjo, a
maior chave está sempre na posição 1 do vector.
Os algoritmos para implementar as operações
sobre o heap operam ao longo de um dos
percursos da árvore.
Um algoritmo elegante para construir o heap foi
proposto por Floyd em 1964.
O algoritmo não necessita de nenhuma memória
auxiliar.
Dado um vector v[1], v[2], . . . , v[n].
Os itens v[n/2 + 1], v[n/2 + 2], . . . , v[n] formam um
heap:
• Neste intervalo não existem dois índices i e j
tais que j = 2i ou j = 2i + 1. 160
161. Heapsort
void heapsort(tipo a[], int n) {
int i = n/2, pai, filho;
tipo t;
for (;;) {
if (i > 0) {
i--;
t = a[i];
} else {
n--;
if (n == 0)
return;
t = a[n];
a[n] = a[0];
}
161
162. pai = i;
filho = i*2 + 1;
while (filho < n) {
if ((filho + 1 < n) && (a[filho + 1] >
a[filho]))
filho++;
if (a[filho] > t) {
a[pai] = a[filho];
pai = filho;
filho = pai*2 + 1;
} else break;
}
a[pai] = t;
}
} 162
163. Heapsort
Algoritmo:
Os itens de v[4] a v[7] formam um heap.
O heap é estendido para a esquerda (esq = 3), englobando o item
v[3], pai dos itens v[6] e v[7].
A condição de heap é violada:
• O heap é refeito trocando os itens D e S.
O item R é incluindo no heap (esq = 2), o que não viola a condição de
heap.
O item O é incluindo no heap (esq = 1).
A Condição de heap violada:
• O heap é refeito trocando os itens O e S, encerrando o
processo.
163
164. Heapsort
Exemplo da operação de aumentar o valor da chave do item na posição i:
O tempo de execução do procedimento AumentaChave em um item do
heap é O(log n).
164
165. Construção de Heap
Algoritmo:
1. Construir o heap.
2. Troque o item na posição 1 do vector (raiz do heap)
com o item da posição n.
3. Use o procedimento Refaz para reconstituir o heap
para os itens v[1], v[2], . . . , v[n − 1].
4. Repita os passos 2 e 3 com os n − 1 itens restantes,
depois com os n − 2, até que reste apenas um item.
166. Heapsort
Exemplo de aplicação do Heapsort :
1 2 3 4 5 6 7
S R O E N A D
R N O E D A S
O N A E D R
N E A D O
E D A N
D A E
A D
• O caminho seguido pelo procedimento Refaz para reconstituir a
condição do heap está em negrito.
• Por exemplo, após a troca dos itens S e D na segunda linha da
Figura, o item D volta para a posição 5, após passar pelas posições 1
e 2.
167. Referências
1. Levitin, Anany “Introduction to the design and analysis of algorithm”
Addison-Wesley 2003
2. Pedro Neto, João “Programação, algoritmo e estrutura de dados”
escola editora 2008
3. Damas, Luís “Programação em C” …
4. Ziviani, Nivio e Botelho, Fabiano Cupertino, “Projecto de Algoritmos
com implementação em Pascal e C” editora Thomson, 2007
5. http://www.icmc.usp.br/~sce182/arvore.html
6. http://w3.ualg.pt/~hshah/ped
7. http://www.liv.ic.unicamp.br/~bergo/mc202/