Buscar

Introdução à Programação: Conceitos e Ordenação

Prévia do material em texto

Ementa
Noções de hardware e software.
Conceitos Fundamentais.
Vetores, cadeia de caracteres e registros.
Definição e manipulação de arquivos externos.
Subprogramação.
Alocação dinâmica de memória. 
Estruturas de dados básicas: listas lineares, pilha e fila. Árvores e grafos.
Métodos de ordenação.
Aplicações práticas utilizando uma linguagem estruturada de alto nível	(Linguagem C).
Noções de Programação Orientada a Objetos com a Linguagem C++.
Ordenação
Ordenar corresponde ao processo de rearranjar um conjunto
de objetos em uma ordem ascendente ou descendente. 
O objetivo principal da ordenação é facilitar a recuperação
posterior de itens do conjunto ordenado.
Os algoritmos de ordenação trabalham sobre os registros de
um arquivo e somente uma parte, ou campo, do registro, 
chamada chave, é utilizada para controlar a ordenação. Além
da chave podem existir outros componentes em um registro,
os quais não têm influência no processo de ordenar, a não ser
pelo fato de que permanecem com a mesma chave.
Estrutura de um item do arquivo
A estrutura de dados registro é a indicada para representar os itens
componentes de um conjunto, ou arquivo, a ser ordenado:
struct TipoItem {
 int Chave;
 // outros componentes do registro
};
A escolha do tipo para a chave é arbitrária. Qualquer tipo sobre o qual
exista uma regra de ordenação bem definida pode ser utilizado. 
As ordens numérica (int) e alfabética (string) são as usuais.
Tipos de Ordenação
Os métodos de ordenação são classificados em dois grandes grupos. 
Se o arquivo a ser ordenado cabe todo na memória principal, então o 
método de ordenação é chamado de ordenação interna. Neste caso o
número de registros a ser ordenado é pequeno o bastante para caber em
um array do C. 
Se o arquivo a ser ordenado não cabe na memória principal e, por isso,
tem que ser armazenado em fita ou disco, então o método de ordenação
é chamado de ordenação externa. 
A principal diferença entre os dois métodos é que, em um método de
ordenação interna, qualquer registro pode ser imediatamente acessado,
enquanto, em um método de ordenação externa, os registros são 
acessados sequencialmente ou em grandes blocos.
Ordenação Interna
Os algoritmos de ordenação constituem bons exemplos de como
resolver problemas utilizando computadores. As técnicas de ordenação
permitem apresentar um conjunto amplo de algoritmos distintos para
resolver uma mesma tarefa. Dependendo da aplicação, cada algoritmo
considerado possui uma vantagem particular sobre os outros. 
O aspecto predominante na escolha de um algoritmo de ordenação é
o tempo gasto para ordenar um arquivo. 
Para algoritmos de ordenação interna as medidas de complexidade
relevantes contam o número de comparações entre chaves e o número
de movimentações (ou trocas) de itens do arquivo. A quantidade extra
de memória auxiliar utilizada pelo algoritmo é também um aspecto 
importante.
Tipos utilizados
Na implementação dos algoritmos de ordenação interna, serão 
utilizados:
1. a constante "n" que representa a quantidade de itens do arquivo que
 serão ordenados.
2. o tipo de dado "TipoVetor", representa o conjunto de elementos do
 TipoItem, apresentado anteriormente, a ser ordenado. Observe que
 o índice do vetor é um subintervalo de 0 até n, para poder armazenar
 chaves especiais na posição 0 chamadas de sentinelas.
3. a variável "A", indica o conjunto a ser ordenado propriamente dito.
	#define n 10 // tamanho do vetor
typedef struct TipoItem TipoVetor[n+1];
struct TipoVetor A;
Procedimento Auxiliar
Os algoritmos de ordenação que serão descritos a seguir, na sua grande
maioria, fazem uso do procedimento "Swap" para trocar dois elementos
de posição.
 void Swap(TipoItem *x, TipoItem *y) {
 TipoItem aux;
 aux = *x;
 *x = *y;
 *y = aux;
}
Troca o valor das variáveis “x” e “y” entre si, usando uma posição de
memória auxiliar (variável “aux”).
1. Ordenação por Seleção (Selection Sort)
Um dos algoritmos mais simples de ordenação. Neste método a
classificação é efetivada por seleção sucessiva do menor valor
de chave contido no vetor "A". A cada passo é feita uma
varredura do segmento do vetor com os elementos ainda não
selecionados e determinado aquele elemento de menor valor, o 
qual é colocado, por troca, em sua posição definitiva no vetor
classificado e o processo repetido para o segmento que contém
os elementos ainda não selecionados.
Na Prática
1º passo: 	procura a posição do menor elemento do vetor e em 
		seguida realiza a troca deste elemento com aquele
		que está na primeira posição;
2º passo:	procura o menor elemento no subvetor que começa a
		partir da segunda posição e troca com o segundo
		elemento e assim por diante;
O método deverá repetir sucessivamente o processo de seleção
durante "n-1" passos.
// Ordenação por seleção 
// (Selection Sort)
void Selecao(TipoVetor A) {
 int i, j, menor;
 for (i=1; i<=(n-1); i++) {
 menor = i;
 for (j=(i+1); j<=n; j++)	
 if (A[j].Chave < A[menor].Chave)
	 menor = j;
 Swap(&A[i], &A[menor]);
 }
}
seleciona a posição do 
menor elemento
2. Ordenação por Troca ou Método da Bolha (Bubble Sort)
A filosofia básica deste método consiste em:
1. Varrer o vetor, comparando os elementos vizinhos
 entre si: if (A[j] > A[j+1]).
2. Caso estejam fora de ordem, os mesmos trocam de 
 posição entre si (Swap).
3. Procede-se assim até o final do vetor. Na primeira 
 varredura verifica-se que o último elemento do vetor
 já está no seu devido lugar (no caso de ordenação 
 crescente, ele é o maior de todos).A segunda 
 varredura é análoga à primeira e vai até o
 penúltimo elemento.
4. Este processo é repetido até que seja feito um número
 de varreduras igual ao número de elementos a serem 
 ordenados menos um. Ao final do processo o vetor está
 classificado segundo o critério escolhido.
// Ordenação por Troca ou 
// Método da Bolha (Bubble Sort)
void Bolha(TipoVetor A) {
 int i, j;
 for (i=1; i<=(n-1); i++)
 for (j=1; j<=(n-i); j++)
 if (A[j].Chave > A[j+1].Chave)
 Swap(&A[j], &A[j+1]);
}
1
2
3
4
3. Ordenação por Inserção (Insertion Sort)
Em cada passo, a partir de i=2, o i-ésimo item da sequência fonte é
apanhado e transferido para a sequência destino, sendo inserido no seu
lugar apropriado. A colocação do item no seu lugar apropriado na
sequência destino é realizada movendo-se itens com chaves maiores
para a direita (A[j+1]:=A[j];) e então inserindo o item na posição
deixada vazia (A[j+1]:=x;). Neste processo de alternar comparações
e movimentos de registros existem duas condições distintas que podem
causar a terminação do processo:
1. um item com chave menor ou igual que o item em consideração é
 encontrado;
2. o final da sequência destino é atingido à esquerda (sentinela).
A melhor solução para a situação de um anel com duas condições de
terminação é a utilização de um registro sentinela: na posição zero do
vetor colocamos o próprio registro em consideração. Para tal o índice
do vetor tem que ser estendido para n+1, ou seja, de 0 até n.
// Ordenação por inserção 
// (Insertion Sort)
void Insercao(TipoVetor A) {
 TipoItem x;
 int i, j;
 for (i=2; i<=n; i++) {
 x = A[i];
 j = i - 1;
 A[0] = x; // sentinela
 while (x.Chave < A[j].Chave) {
 A[j+1] = A[j];
 j = j - 1;
 }
 A[j+1] = x;
 }
}
insere o i-ésimo item (x) no seu lugar apropropriado na sequência destino
4. ShellSort
Este método de classificação, proposto por Ronald L. Shell em 1959,
pertence à família dos métodos de classificação por inserção. O método
da inserção troca itens adjacentes quando está procurando o ponto de
inserção na sequência destino. Se o menor item estiver na posição mais
a direita no vetor então o número de comparações e movimentações é
igual a n - 1 paraencontrar o seu ponto de inserção.
O método Shell contorna este problema permitindo trocas de registros
que estão distantes um do outro (A[j] = A[j - h];). Os itens que estão
separados h posições são rearranjados de tal forma que todo h-ésimo
item leva a uma sequência ordenada. Tal sequência é dita estar
h-ordenada. Várias sequências para h têm sido experimentadas. 
Knuth (1973, p.95) mostrou experimentalmente que a escolha do
incremento para h, mostrada abaixo, é difícil de ser batida por mais
de 20% em eficiência no tempo de execução:
			h = 3 * h + 1
A sequência para h corresponde a 4, 13, 40, 121, 364, 1093, 3280, ...
// classificação por inserção (ShellSort)
void ShellSort(TipoVetor A) {
 int i, j, h;
 TipoItem x;
 h = 1;
 do { 
 h = 3 * h + 1; 
 } while (h <= n);
 do {
 h = h / 3;
 for (i=(h+1); i<=n; i++) {
 x = A[i];
 j = i;
 while ((j > h) && (A[j-h].Chave > x.Chave)) {
 A[j] = A[j-h];
 j = j - h;
 }
 A[j] = x;
 }
 } while (h != 1);
}
A cada passo, todos os segmentos são classificados isoladamente por inserção direta. Uma vez terminado um passo, o processo é repetido para um novo incremento igual à um terço do anterior, até que seja executado um passo com h = 1, quando, então, o vetor resulta ordenado.
5. HeapSort (Seleção em Árvore Binária)
HeapSort é um método de ordenação cujo princípio de funcionamento é o
mesmo princípio utilizado para a ordenação por seleção, a saber: 
selecione o menor item do vetor e a seguir troque-o com o item que está na
primeira posição do vetor; repita estas duas 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 custa n - 1 comparações.
Este custo pode ser reduzido através da utilização de uma estrutura de
dados chamada fila de prioridades.
Filas de Prioridades
No estudo de listas lineares, foi visto que a operação de desempilhar um 
item de uma pilha retira o último item inserido (o mais novo), e a operação
de desenfileirar um item de uma fila retira o primeiro item inserido (o mais
velho). Em muitas situações é necessário uma estrutura de dados que
suporte as operações de inserir um novo item e retirar o item com a maior
chave. Tal estrutura de dados é chamada de fila de prioridades, porque a
chave de cada item reflete sua habilidade relativa de abandonar o conjunto
de itens rapidamente.
Heaps
São estruturas de dados eficiente para representar as filas de prioridades e
foram propostas por Willians (1964). Um heap é definido como uma
sequência de itens com chaves
		c[1], c[2], ..., c[n]
tal que
		c[i]  c[2i]		sucessor à esquerda, se 2i < n, 
					caso contrário não existe
		c[i]  c[2i + 1]		sucessor à direita, se 2i + 1 < n, 
					caso contrário não existe
para todo i = 1, 2, ..., n/2. Esta ordem pode ser facilmente visualizada se a
sequência de chaves for desenhada em uma árvore binária, onde as linhas
que saem de uma chave levam a duas chaves menores no nível inferior.
Uma árvore binária completa pode ser representada por um array e foi
apresentado por Floyd, 1964. Esta representação é extremamente compacta
e, além disso, permite caminhar pelos nodos da árvore facilmente: os filhos
de um nodo i estão nas posições 2i e 2i + 1 (caso existam), e o pai de um
nodo i está na posição i / 2.
Procedimento “HeapSort”
O primeiro passo é construir o heap, usando como chaves os caracteres
da palavra “ORDENAS”.
Os itens de A[4] a A[7] formam a parte inferior da árvore binária
associada, onde nenhuma relação de ordem é necessária para formarem um
heap. A seguir o heap é estendido para a esquerda (Esq = 3), englobando
o item A[3], pai dos itens A[6] e A[7]. Neste momento a condição do heap
é violada, e os itens D e S são trocados. A seguir, o heap é novamente
estendido para a esquerda (Esq = 2) incluindo o item R, passo que não
viola a condição do heap. Finalmente, o heap é estendido para a esquerda
(Esq = 1), incluindo o item O, e os itens O e S são trocados, encerrando o
processo de construção dos heaps.
			1	2	3	4	5	6	7
Chaves iniciais:	O	R	D	E	N	A	S
Esq = 3		O	R	S	E	N	A	D
Esq = 2		O	R	S	E	N	A	D
Esq = 1		S	R	O	E	N	A	D
S
1	2	3	4	5	6	7
S	R	O	E	N	A	D
Arvóre binária completa representada por um arranjo.
R
O
E
N
A
D
Arvóre binária completa representando o heap.
1
2
3
4
5
6
7
// reconstrói o heap
void Refaz(int Esq, int Dir, TipoVetor A) {
 int i, j;
 TipoItem x;
 i = Esq; 
 j = 2 * i;
 x = A[i];
 while (j <= Dir) {
 if ((j < Dir) && (A[j].Chave < A[j+1].Chave))
 j = j + 1; 
 if (x.Chave >= A[j].Chave) 
 break;
 A[i] = A[j];
 i = j; 
 j = 2 * i;
 }
 A[i] = x;
}
// HeapSort (seleção em árvore binária)
void HeapSort(TipoVetor A) {
 int Esq, Dir;
// Constrói o Heap
 Esq = (n / 2) + 1;
 Dir = n;
 while (Esq > 1) {
 Esq = Esq - 1;
 Refaz(Esq, Dir, A);
 }
// Ordena o Vetor
 while (Dir > 1) {
 Swap(&A[1], &A[Dir]);
 Dir = Dir - 1;
 Refaz(Esq, Dir, A);
 }
}
6. QuickSort
QuickSort é o algoritmo de ordenação interna mais rápido que se conhece
para uma ampla variedade de situações, sendo provavelmente mais 
utilizado do que qualquer outro algoritmo. O algoritmo foi inventado por
Hoare em 1960, quando visitava a Universidade de Moscou como 
estudante, e foi publicado mais tarde por Hoare (1962), após uma série de
refinamentos.
A idéia básica é a de partir o problema de ordenar um conjunto com n itens
em dois problemas menores. A seguir, os problemas menores são 
ordenados independentemente e depois os resultados são combinados para
produzir a solução do problema maior.
A parte mais delicada deste método é relativa ao procedimento partição, o
qual tem que rearranjar o vetor A[Esq..Dir] através da escolha arbitrária de
um item x do vetor chamado pivô, de tal forma que ao final o vetor A está
particionado em uma parte esquerda com chaves menores ou iguais a x e
uma parte direita com chaves maiores ou iguais a x.
Procedimento “Partição”:
1. escolha arbitrariamente um item do vetor e coloque-o em x;
2. percorra o vetor a partir da esquerda até que um item A[i]  x é
 encontrado; da mesma forma percorra o vetor a partir da direita até
 que um item A[j]  x é encontrado;
3. como os dois itens A[i] e A[j] estão fora de lugar no vetor final então
 troque-os de lugar;
4. continue este processo até que os apontadores i e j se cruzem em
 algum ponto do vetor.
Ao final o vetor A[Esq..Dir] está particionado de tal forma que:
a. os itens A[Esq], A[Esq + 1], ..., A[j] são menores ou iguais a x;
b. os itens A[i], A[i + 1], ..., A[Dir] são maiores ou iguais a x;
void Particao(TipoVetor A, int Esq, int Dir, int *i, int *j) {
 TipoItem x;
 *i = Esq;
 *j = Dir;
 x = A[(*i + *j) / 2]; // Obtém o Pivô
 do {
 while (A[*i].Chave < x.Chave)
 *i = *i + 1;
 while (x.Chave < A[*j].Chave)
 *j = *j - 1;
 if (*i <= *j) {
 Swap(&A[*i], &A[*j]);
 *i = *i + 1;
 *j = *j - 1;
 }
 } while (*i <= *j);
}
Nota: o anel interno do procedimento partição consiste apenas em incrementar 
um apontador e comparar um item do vetor contra um valor fixo em x. Este anel
é extremamente simples, razão pela qual o algoritmo Quicksort é tão rápido.
2
3
1
4
Particionando o Vetor
O método é ilustrado usando como chaves os caracteres da palavra 
"ORDENA”, ou seja, n = 6. 
O item x (pivô) é escolhido como sendo: A[(i + j) div 2]. Como
inicialmente i = 1 e j = 6, então x = A[3] = D, o qual aparece em negrito
na primeira linha. A varredura a partir da posição 1 pára no item O e a
varredura a partir da posição 6 pára no item A, sendo os dois itens
trocados, como mostrado na segunda linha. A seguir a varredura a partir
da posição 2 para no item R e a varredura a partir da posição 5 pára no
item D, e então os dois itens são trocados, como mostradona terceira
linha. Neste momento i e j se cruzam (i = 3 e j = 2), o que encerra o
processo de "partição".
 	1	2	3	4	5	6
	O	R	D	E	N	A
	A	R	D	E	N	O
	A	D	R	E	N	O
void Ordena(TipoVetor A, int Esq, int Dir) {
 int i, j;
 Particao(A, Esq, Dir, &i, &j);
 if (Esq < j)
 Ordena(A, Esq, j);
 if (i < Dir)
 Ordena(A, i, Dir);
}
void QuickSort(TipoVetor A) {
 Ordena(A, 1, n);
}
Notas: observar que o procedimento "Ordena" é recursivo.
Procedimento “QuickSort”
Após obter os dois pedaços do vetor através do procedimento partição,
cada pedaço é ordenado recursivamente. 
A Figura a seguir ilustra o que acontece com o vetor exemplo em cada
chamada recursiva do procedimento ordena. Cada linha mostra o
resultado do procedimento partição, onde o pivô é mostrado em negrito.
		 1	2	3	4	5	6
Chaves iniciais:	O	R	D	E	N	A
	1	A	D	R	E	N	O
	2	A	D
	3			E	R	N	O
	4				N	R	O
	5					O	R
		A	D	E	N	O	R
Referências
Projeto de Algoritmos: Com Implementações em Pascal e C.
Nivio Ziviani.
4ª ed.- São Paulo: Pioneira, 1999.
Estrutura de Dados.
Paulo Veloso, Clesio dos Santos, Paulo Azeredo e Antonio Furtado.
13ª ed.- Rio de Janeiro: Editora Campus, 1991.

Continue navegando