You are on page 1of 8

Curso de Engenharia de Computação   

Laboratório de AEDS1   –     2016/1
Trabalho Prático 1

Complexidade de Tempo:  Análise e Medições

O trabalho é individual. Trabalhos iguais terão notas também iguais a zero.

Este trabalho contém três exercícios, os dois primeiros valem 5 pontos cada e o terceiro vale 10 pontos.
Instruções para o relatório estão no final deste documento.

1. Uma   causa   importante   de   erros   em   programação   em   C   é   ultrapassar   os   limites   numéricos


estabelecidos para cada tipo de variável. O objetivo desse exercício é mostrar como e porque
ocorrem esses erros. Os limites numéricos que cada tipo inteiro estão definidos na biblioteca da
linguagem LIMITS.H, na forma de constantes. Escreva um programa que use essa biblioteca e
imprima os limites mínimos e máximos que podem ser assumidos por variáveis dos tipos char,
int, short int, unsigned int, long int, unsigned long int, long long
int,   unsigned   long   long   int.   Apresente   os   resultados   em   uma   tabela.   Faça   um
pequeno programa usando uma variável à qual é atribuída um valor maior do que seu tipo
permite para demonstrar o problema que ocorre. 

2. Usando   o   código   a   seguir,   medir   o   tempo   de   execução   do   laço   com   comando   vazio,   para
diferentes   valores   de   N.     Anote   os   resultados   em   uma   tabela.   A   seguir,   repetir   o   exercício
incluindo a função   rand( )  no laço.   Plote os tempos em função do N no mesmo gráfico e
compare. A partir das medições estime a ordem de grandeza do tempo de execução de uma
instrução no computador de teste. Mostre como fez esta estimativa, explique seu raciocínio.
Escreva suas conclusões. 

#include <stdio.h>
#include <time.h>

# define N 10000000000LLU

int main(){

  unsigned long long int i;
  clock_t t1, t2;   
  t1 = clock ();
  for (i=0; i < N ; i++) ;
  t2 = clock ();   

  printf ("tempo = %.3e segundos\n", 
         ((double)t2 ­ (double)t1) / (double)CLOCKS_PER_SEC);
}
3. Dados dois conjuntos de números inteiros S1 e S2, cada um com N elementos, armazenados em
vetores,  e  um   número K,   projete  e  implemente  um   algoritmo  para   verificar  se há   pares de
elementos, um de S1 e o outro de S2, cuja soma é K. Use números aleatórios para gerar os
valores.   Apresente   uma   descrição   detalhada   do   seu   algoritmo,   usando   texto   e   figuras   que
auxiliem a explicação do mesmo. Apresente a análise de complexidade do seu algoritmo. Faça
medições de tempo e do número de operações realizadas por seu algoritmo para comprovar a
sua análise de complexidade.  Apresente um relatório com a descrição do algoritmo, a análise
de complexidade, as medições realizadas, suas análises dos resultados e conclusões. Anexe o
código impresso. 
 

RELATÓRIOS DE TRABALHOS PRÁTICOS

Para cada exercício faça um pequeno relatório do experimento, descrevendo a solução, os resultados, a
conclusão   e   o   código   em   anexo.   Os   relatórios   devem   ser   entregues   grampeados   em   um  único
documento. Não colocar capa.  Os relatórios devem conter os seguinte itens: 
• sua descrição do experimento: apresente o que foi feito, discuta as limitações e dificuldades
encontradas bem como as decisões tomadas durante a realização do experimento;
• apresentação   dos   resultados:   apresente   de   forma   clara   e   distinta   as   tabelas   e   os   gráficos
gerados; 
• apresente sua análise dos resultados;
• apresente sua conclusão do experimento;
• ao final da redação do relatório, verifique se tudo o que foi pedido está apresentado.

Eu ouço e esqueço; eu vejo e lembro; eu faço e compreendo.    

Confúcio, filósofo chinês (551­479 BC)    
AEDS 1: TRABALHO PRÁTICO 1
nome: Ana Luiza Sanches data de entrega: 18/03/2016

PARTE I
Objetivos
- Determinar os limites numéricos de cada tipo de variável;
- Identificar o erro que ocorre quando esse limite é ultrapassado;

Solução
O algoritmo desenvolvido utiliza a biblioteca limits.h e exibe as constantes que definem os
valores máximos e mínimos que cada tipo de variável possui. Para testar o que ocorre quando o
valor máximo é ultrapassado, foi criada uma variável var, do tipo int. A ela foi atribuído o valor
máximo suportado pelo tipo int, e em seguida esse valor foi incrementado.

Resultados e Análise
A tabela abaixo informa os limites de cada tipo de variável. Variáveis unsigned(sem sinal)
não podem representar números negativos, logo o limite mínimo é 0. Os limites do tipo long int
equivalem aos limites de long long int no computador testado, assim como os limites do tipo
unsigned long int equivalem aos limites de unsigned long long int. É importante saber que esses
limites podem variar de computador pra computador, ou seja, existe um outro computador em que
os limites do int são equivalentes aos limites do long int.
Tipo de variável Valor mínimo Valor máximo
char -128 127
int -2147483648 2147483647
short int -32768 32767
unsigned int 0 4294967295
long int -9223372036854775808 9223372036854775807
unsigned long int 0 18446744073709551615
long long int -9223372036854775808 9223372036854775807
unsigned long long int 0 18446744073709551615
Tabela 1: Valores máximos e mínimos de cada tipo de variável

O algoritmo também imprime o número máximo inteiro, em seguida o número máximo


acrescido de 1. Percebe-se que o número máximo acrescido de 1 se torna o valor mínimo que o tipo
int representa. Esse erro é denominado Overflow e ocorre quando se ultrapassa o limite máximo.
instrução Valor as variável maximo
int maximo; 2147483647
maximo+1; -2147483648
Tabela 2: Overflow
Conclusão
Conhecer os intervalos de valores dos tipos variáveis utilizadas é de grande importância para
a escolha adequada do tipo que será utilizado. O erro de overflow, que ocorre quando o valor
máximo suportado pela variável é ultrapassado, pode gerar diversos erros em tempo de execução,
portanto é desejável que o programador saiba usar cada tipo de modo correto.
PARTE II
Objetivos
- Estimar a ordem de grandeza do tempo de execução de uma instrução
- Verificar o impacto da chamada de função no tempo de execução

Solução
Foi utilizado o algoritmo presente no roteiro como base. O algoritmo base é composto por
um laço for que se repete N vezes. Para automatizar o experimento foi criado um segundo laço for,
que executa o trecho do algoritmo base 5 vezes, cada vez com um número diferente para N. O
algoritmo implementado foi executado duas vezes. A primeira para a medição dos tempos de
execução para um loop vazio, e a segunda, para um loop com chamada de função (rand()).

Resultados e Análise

Tamanho da Tempo de execução com Tempo de execução com


entrada N loop vazio (s) chamada de função (s)
10⁷ 0,032 (3.232e-02) 0,087 (8.679e-02)
10⁸ 0,205 (2.051e-01) 0,862 (8.621e-01)
10⁹ 1,692 (1.692e+00) 8,615 (8.615e+00)
10¹⁰ 16,91 (1.691e+01) 86,18 (8.618e+01)
10¹¹ 170,6 (1.706e+02) 866,6 (8.666e+02)
Tabela 3: tamanho da entrada e tempo de execução
A partir das medições, observa-se que os tempos de execução possuem uma relação linear
com a entrada, pois assim como as entradas são potencias de 10, os tempos coletados variam de
linha para linha a uma proporcão de 10 (cada linha é aproximadamente 10 vezes maior que a linha
anterior). Isso fica bem evidente no gráfico 1, que representa duas retas.

Gráfico 1: Tempo x Entrada – representação quantitativa (gerado pelo


software SciDAVis)
Além disso, é possível ver que o algoritmo que tem chamada de função possui seu tempo de
execução aumentado drasticamente em relação ao loop vazio, em entradas superiores a 10⁹ (O que
pode ser melhor vizualizado no gráfico 2).
Tempo x Entrada
1000
loop vazio
900
800
700
600
tempo (s)

500
400
300
200
100
0
10⁷ 10⁸ 10⁹ 10¹⁰ 10¹¹
entrada

Gráfico 2: Tempo x Entrada - representação qualitativa (gerado pelo software LibreOffice Calc)
Para estimar a ordem de grandeza do tempo de execução de uma instrução no computador
de teste, basta ter o tempo de execução total do programa e o número de vezes que essa instrução é
executada. O algoritmo de loop vazio é o melhor para se fazer esse calculo pois é possível
determinar todas a instruções realizadas (não se sabe as instruções realizadas na função rand()).
Primeiramente, sabe-se que o algoritmo depende apenas de N para determinar o tempo de
execução. Dessa forma a comparação i<N, que se encontra no loop, pode ser escolhida como a
instrução relevante. Como o loop é executado de 1 até N, então a instrução sempre será executada N
vezes. Assim, divide-se o tempo total de execução pelo número de vezes que ela é executada (N,
que é o tamanho da entrada) : tempo de execução da instrução = tempo total de execução / N.

Tamanho da Tempo de execução total (s) Tempo de execução estimado da


entrada N instrução I < N (s)
10⁷ 0,032 (3.232e-02) (3.232e-09)
10⁸ 0,205 (2.051e-01) (2.051e-09)
10⁹ 1,692 (1.692e+00) (1.692e-09)
10¹⁰ 16,91 (1.691e+01) (1.691e-09)
10¹¹ 170,6 (1.706e+02) (1.706e-09)
Tabela 4: Estimativa de tempo de execução de uma instrução

A tabela mostra diversos valores para o tempo de execução de uma instrução, no entanto, o
importante, é apenas determinar a ordem de grandeza. Observa-se que a ordem de grandeza de
uma instrução, em todos os casos foi 10⁻⁹ s. Essa ordem de grandeza da instrução parece adequada
visto que a frequência do clock do computador utilizado é da ordem de 10⁹ Hz.
Conclusão
A chamada de função aumenta o tempo de execução de um programa, ou seja, representa um
aumento de custo do algoritmo. Entretanto, as funções são estremamente importantes na
legibilidade e organização do código e portanto devem ser utilizadas sempre que necessário.
O tempo de execução de uma instrução pelo computador é tão pequeno(10⁻⁹ s) que não
pode ser percebido por humanos, o faz dele uma máquina eficiente.
PARTE III
Objetivos
- Analisar a complexidade de um algoritmo;
- Determinar os casos de complexidade;
Solução
O algoritmo desenvolvido possui 3 funções: a função principal(main()); uma função que
preenche 2 vetores(de forma aleatória) e uma função que verifica se há dois elementos(cada um de
um vetor) que somados resultem em uma constante K.

A função de preenchimento de dois vetores utiliza a função rand() para gerar números
aleatórios.
A função par (última citada) recebe como parametros 2 vetores s1 e s2, o tamanho dos
vetores N, e a constante K.

Resultados e Análise
Tamanho da entrada Tempo de execução (s) Nº de instruções Foi encontrado?
N
10³ 1.003e-02 1000000 não
10⁴ 4.227e-01 100000000 não
10⁵ 9.288e+00 2496789847 sim
10⁶ 2.098e+00 548270191 sim
Tabela 5: Tempo de execução, nº de instruções para diferentes tamanhos de entrada

Tamanho da entrada N Tempo de execução (s) Nº de instruções (s)


10⁶ 3.748e+00 996227633
10⁶ 1.300e+01 3400911432
10⁶ 3.818e+00 1016915804
10⁶ 1.017e+01 2693757360
10⁶ 3.359e-01 85264749
Tabela 6: Tempo de execução para a mesma entrada

Conclusão

APENDICE – códigos
Parte I
main() {
printf("\nchar: min: %d \t max: %d \t",CHAR_MIN, CHAR_MAX) ;
printf("\nint: min: %d \t max: %d \t",INT_MIN, INT_MAX) ;
printf("\nshort int: min: %d \t max: %d \t",SHRT_MIN, SHRT_MAX) ;
printf("\nunsigned int: min: 0 \t max: %d \t",UINT_MAX) ;
printf("\nlong int: min: %ld \t max: %ld \t",LONG_MIN, LONG_MAX) ;
printf("\nunsigned long int: min: 0 \t max: %ld \t", ULONG_MAX) ;
printf("\nlong long int: max: %lld \t min: %lld \t",LLONG_MIN, LLONG_MAX) ;
printf("\nunsigned long long int: max: 0 \t min: %lld \t", ULLONG_MAX) ;
int var = INT_MAX+1;
printf("\nint maximo %d", INT_MAX) ;
printf("\nint maximo+1 %d\n", var ) ;
}

Parte II
# define N 10000000LLU

int main() {
unsigned long long int i;
unsigned long long int x;
clock_t t1, t2;
for(x = 1; x < 100000; x*=10) {
t1 = clock () ;
for (i=0; i < N*x; i++) ;
//rand() ;
t2 = clock () ;
printf ("%llu : %.3e segundos\n", (N*x) ,
((double) t2 - (double) t1) / (double) CLOCKS_PER_SEC) ;
}
}
Parte III
#include <stdio.h>
#include <time.h>

# define N 1000000LLU

/*Funcao que preenche dois vetores*/


void preencher(int v1[N],int v2[N]) ;

int par(int v1[N], int v2[N], int K, unsigned long long int *instrucoes) ;

main() {
clock_t t1, t2;
unsigned long long int i, j, instrucoes = 0;
int s1[N], s2[N], K = rand() ;

preencher(s1,s2) ;

t1 = clock () ;
if( par(s1, s2, K, &instrucoes) ) {
t2 = clock () ;
printf ("\ntempo = %.3e segundos\n",
((double) t2 - (double) t1) / (double) CLOCKS_PER_SEC) ;
}else{
t2 = clock () ;
printf ("\ntempo = %.3e segundos(nao encontrado) \n",
((double) t2 - (double) t1) / (double) CLOCKS_PER_SEC) ;
}
printf("entrada: %llu instrucoes: %llu\n",N,instrucoes) ;
}

void preencher(int v1[N],int v2[N]) {


unsigned long long int i;
srand(time(0) ) ;
for (i = 0; i < N; i++) {
v1[i] = rand() ;
v2[i] = rand() ;
}
}

int par(int v1[N],int v2[N],int K, unsigned long long int *instrucoes) {


unsigned long long int i, j;
for (i = 0; i < N; i++) {
for(j = 0; j < N; j++) {
(*instrucoes) ++;
if(v1[i] + v2[j] == K)
return 1;
}
}
return 0;
}

You might also like