Comparação de Algoritmos em C++
Comparação de Algoritmos em C++
Alan
de
Freitas
Algoritmos
• Um algoritmo é um procedimento de
passos para cálculos.
• Este procedimento é composto de
instruções que definem uma função
• Até o momento, vimos apenas códigos em
C++ para descrever ideias
• Já um algoritmo pode descrever ideias
para pessoas que programem em outras
linguagens de programação
Exemplo
Código em C++ Algoritmo
int max(int[] a, int n){ Entrada: Conjunto de números inteiros A
int i, temp; Saída: Maior elemento deste conjunto
temp = a[0];
for (i = 1; i < n; i++){ m ← primeiro elemento de A
if (temp < a[i]){ Para todos os outros elementos Ai de A
temp = a[i];
}
se o elemento Ai for maior que m
} m ← Ai
return temp; retorne o valor de m
}
Definição
• Apesar de ser um conjunto de regras que define uma
sequência, não existe uma definição formal e geral de
algoritmo.
• Podem ser incluídos algoritmos que não fazem cálculos?
• O algoritmo precisa sempre parar?
• Um programa que não para é um algoritmo?
• Podem ser incluídos programas que só param com
intervenção do usuário?
• Programas que dependem do usuário são algoritmos?
• Algoritmos podem ter componentes aleatórios?
Expressando algoritmos
• Há também várias maneiras de se
expressar um algoritmo:
• Linguagem natural (Português)
• Pseudo-código (Listagem das operações
matemáticas)
• Diagramas (Quadrados e setas)
• As próprias linguagens de programação
Algoritmos
• Estamos mesmo assim interessados em
estudar algoritmos em vez de códigos em
linguagens de programação
• Isso porque queremos conclusões amplas
sobre a análises que fazemos dos
algoritmos
• Em programação, esta análise mais ampla
permite ajudar pessoas que utilizam
diferentes linguagens
Modelos de comparação
• Nem sempre 2 algoritmos que resolvem o
mesmo problema são igualmente
eficientes…
• Como comparar algoritmos?
• Alguns são mais rápidos…
• Outros gastam menos memória…
• Outros obtém soluções de melhor
qualidade…
Medida de custo real
• Usualmente inadequada
• Depende muito do sistema em geral
• Hardware, compilador, quantidade
disponível de memória
• Pode ser vantajosa quando dois
algoritmos têm comportamento similar
• Os custos reais de operação são então
considerados
Medida por Modelos Matemáticos
• Especificamos um conjunto de operações
e seus custos
• Usualmente, apenas as operações mais
importantes do algoritmo são
consideradas
• Comparações
• Atribuições
• Chamadas de função
Complexidade de Algoritmos
• Para se medir o custo de um algoritmo,
definimos uma função de custo ou função de
complexidade f
• f(n) pode medir o tempo necessário para
executar um algoritmo
• f(n) pode medir o tempo determinando quantas
vezes uma operação relevante será executada
• f(n) pode medir a memória gasta para executar
um algoritmo
Considere o algoritmo abaixo…
f(n) = n - 1
Note que a operação relevante deste algoritmo é o
número de comparações.
DCOMP/UFSJ AEDSII
· Intr. An. Complex. 35
Operações com notação O
Uma função sempre se domina pois basta a
multiplicar por uma constante para que seja maior
Operações com a notação O
que ela mesmo.
Operações com notação O
O mesmo vale para esta função O(f(n)) multiplicada
por uma constante, pois basta multiplicarmos f(n) por
Operações com a notação O
uma constante ainda maior para que seja dominada.
Operações com notação O
A soma de duas funções dominadas por f(n) é ainda
dominada por f(n) pois esta diferença ainda pode ser
Operações com a notação O
compensada por uma constante.
Operações com notação O
Se uma função é dominada por uma função dominada
por f(n), a primeira função é também dominada por
Operações com a notação O
f(n)
Operações com notação O
A soma de duas funções será dominada pela maior
função que
Operações comas domina.
a notação O
Operações com notação O
A multiplicação de duas funções será dominada pela
multiplicaçãoOperações
das funções que asOdominavam.
com a notação
Classes de algoritmos
n
• Dizemos que algoritmos O(2 ) têm complexidade
exponencial
• Quando n = 20, o tempo de execução já é cerca de 1
milhão
• Não são algoritmos muito úteis na prática
• Algoritmos típicos desta classe são os que usam
força-bruta para resolver problemas que envolvam
combinações
• São testadas todas as soluções possíveis até que
se encontre a certa
f(n) = O(n!)
• Dizemos que algoritmos O(n!) também têm
complexidade exponencial, apesar de serem
muito piores que O(2n)
• Não são úteis na prática
• Algoritmos típicos desta classe são os que
usam força-bruta para resolver problemas
que envolvam permutações
• O computador levaria séculos para
resolver problemas pequenos
Busca em Arranjos
• Como sabemos, arranjos são uma
maneira de se armazenar muita
informação em série
• Esta informação, em situações práticas,
é usualmente dividida em registros do
C++ com o recurso struct
• Precisamos de estratégias para
encontrar dados nestes arranjos
Busca em arranjos
• Usualmente, cada struct do arranjo
tem um campo que chamamos de chave
• Por exemplo, suponha um arranjo de
dados do tipo Aluno:
struct Aluno{
string nome;
double nota_prova1;
double nota_prova2;
int matricula;
}
Busca em arranjos
• Nesta estrutura, cada aluno terá um nome,
uma nota para cada prova e um número de
matrícula.
• Como os números de matrícula não se
repetem, eles são bons candidatos a serem o
membro chave dos alunos
struct Aluno{
string nome;
double nota_prova1;
double nota_prova2;
int matricula;
}
Busca em arranjos
• Neste caso prático, o problema da busca
em arranjo consiste em encontrar em
qual posição do arranjo a está o aluno
com número de matrícula x
?
Busca em arranjos
• Para apresentação didática dos
métodos, consideraremos arranjos de
int onde o próprio valor do int é sua
chave.
x 9 a 6 3 8 5 9 2 1
Elemento 9 está na
4 posição 4 do arranjo
Busca Sequencial
a 6 3 8 5 9 2 1 chave 9
a 6 3 8 5 9 2 1 chave 9
a 6 3 8 5 9 2 1 chave 9
a 6 3 8 5 9 2 1 chave 9
a[i]
i 0
Comparamos cada elemento do arranjo com o
elemento chave.
a 6 3 8 5 9 2 1 chave 9
a[i]
i 1
Comparamos cada elemento do arranjo com o
elemento chave.
a 6 3 8 5 9 2 1 chave 9
a[i]
i 2
Comparamos cada elemento do arranjo com o
elemento chave.
a 6 3 8 5 9 2 1 chave 9
a[i]
i 3
Comparamos cada elemento do arranjo com o
elemento chave.
a 6 3 8 5 9 2 1 chave 9
a[i]
i 4
Se o elemento chave foi encontrado, o valor de i, que
tem sua posição nesta iteração da repetição, é
retornado.
int buscaSequencial(int chave, int a[], int n){
for (int i=0; i<n; i++)
{
if (a[i] == chave)
{
true
return i;
}
}
return -1;
}
a 6 3 8 5 9 2 1 chave 9
a[i]
i 4
Se o elemento chave não foi encontrado e a
comparação deu false, passamos para a próxima
iteração de repetição.
int buscaSequencial(int chave, int a[], int n){
for (int i=0; i<n; i++)
{
if (a[i] == chave)
{
false
return i;
}
}
return -1;
}
a 6 3 8 5 9 2 1 chave 9
a[i]
i 1
Se em nenhuma das iterações o elemento foi
encontrado, saímos do for e retornamos -1. O
retorno de -1 avisa para a função chamadora que o
elemento não foi encontrado.
int buscaSequencial(int chave, int a[], int n){
for (int i=0; i<n; i++)
{
if (a[i] == chave)
{
return i;
}
}
return -1;
}
a 6 3 8 5 9 2 1 chave 9
a[i]
i 7
Análise
a 1 2 3 5 6 8 9 chave 6
a 1 2 3 5 6 8 9 chave 6
esq i dir
a 1 2 3 5 6 8 9 chave 6
esq 0 i dir 6
a 1 2 3 5 6 8 9 chave 6
a[esq] a[dir]
Entramos em uma estrutura de repetição onde a cada
passo um elemento i será comparado.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 0 i dir 6
a 1 2 3 5 6 8 9 chave 6
a[esq] a[dir]
O índice i aponta então para o elemento do meio do
arranjo.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 0 i 3 dir 6
a 1 2 3 5 6 8 9 chave 6
esq 0 i 3 dir 6
a 1 2 3 5 6 8 9 chave 6
esq 4 i 3 dir 6
a 1 2 3 5 6 8 9 chave 6
esq 4 i 3 dir 6
a 1 2 3 5 6 8 9 chave 6
esq 4 i 3 dir 6
a 1 2 3 5 6 8 9 chave 6
esq 4 i 3 dir 6
a 1 2 3 5 6 8 9 chave 6
esq 4 i 5 dir 6
a 1 2 3 5 6 8 9 chave 6
esq 4 i 5 dir 4
a 1 2 3 5 6 8 9 chave 6
a[esq] a[i]
a[dir]
O elemento a[i] ainda não é a chave que
procuramos e os índices esq e dir não se cruzaram
indicando que todo o arranjo já foi percorrido.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 4 i 5 dir 4
a 1 2 3 5 6 8 9 chave 6
a[esq] a[i]
a[dir]
Na próxima iteração, o elemento do meio marcado
por i é o único elemento ainda não considerado.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 4 i 4 dir 4
a 1 2 3 5 6 8 9 chave 6
a[esq] a[i]
a[dir]
Como a chave não é maior que a[i] (ela é igual),
deslocamos o índice dir mais uma vez para indicar
que não há mais elementos a se pesquisar.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 4 i 4 dir 4
a 1 2 3 5 6 8 9 chave 6
a[dir] a[esq]
a[i]
Como a chave foi encontrada e não há mais elementos
para se pesquisar, nenhuma das condições para se
continuar o laço é verdadeira.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 4 i 4 dir 4
a 1 2 3 5 6 8 9 chave 6
a[dir] a[esq]
a[i]
Como o elemento i que pesquisamos é a chave,
retornamos este índice.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 4 i 4 dir 4
a 1 2 3 5 6 8 9 chave 6
a[dir] a[esq]
a[i]
Se o elemento i não fosse a chave, seria porque o
elemento não estava no arranjo. Deveríamos então
retornar -1.
int buscaBinaria(int chave, int a[], int n){
int i;
int esq = 0;
int dir = n-1;
do {
i = (esq + dir)/2;
if (chave > a[i]){
esq = i + 1;
} else {
dir = i - 1;
}
} while (chave != a[i] && esq <= dir);
if (chave == a[i]){
return i;
} else {
return -1;
}
}
esq 4 i 4 dir 4
a 1 2 3 5 6 8 9 chave 6
a[dir] a[esq]
a[i]
Análise
Experimentos
para se encontrar um elemento em um arranjo. A Figura 15.4(a) apresenta resultados para
arranjos com até 3000 elementos enquanto a Figura 15.4(b) apresenta resultados para arranjos
com até 80 mil elementos. Como a busca sequencial tem custo O(n) e a busca binária tem custo
O(log n), vemos que à medida que os arranjos ficam maiores, a busca binária se torna cada vez
mais vantajosa. Porém, é possível que para arranjos pequenos, a busca sequencial seja mais
vantajosa. Nestes experimentos, isto ocorre para arranjos com menos de 350 elementos.
−6 −5
x 10 x 10
1.8 1.5
Busca sequencial Busca sequencial
Busca binária Busca binária
1.6
1.4
1.2 1
Tempo (segundos)
Tempo (segundos)
1
0.8
0.6 0.5
0.4
0.2
0 0
0 500 1000 1500 2000 2500 3000 0 1 2 3 4 5 6 7 8
Tamanho do arranjo Tamanho do arranjo 4
x 10
Figura 15.4: Tempo necessário pelos algoritmos de busca para se encontrar um elemento em
arranjos de vários tamanhos.
Exercício