Projeto de Algoritmos

". . . there is a huge difference between programs that merely work
and programs that are well-engineered,
just as there is a huge difference
between a log thrown over a river and a well-engineered bridge."

" . . . there is a huge difference between good programs and working programs.
A good program not only works, but is easy to read and maintain."

—P. A. Darnell, Ph. E. Margolis,  Software Engineering in C

"Programs must be written for people to read, and incidentally for machines to execute."
— H. Abelson, G. Sussman, The Structure and Interpretation of Computer Programs

"Any fool can write code that a computer can understand.
Good programmers write code that humans can understand."
— R. Fowler, Refactoring: Improving the Design of Existing Code

Busca em vetor

Suponha que v é um vetor (= array) de inteiros e que a parte relevante do vetor é   v[0..n-1].   Nosso problema:

Verificar se um dado inteiro  x  está em  v[0..n-1].

O problema faz sentido para qualquer   n ≥ 0.   Se  n  é  0,  o vetor é vazio e portanto x não está em v[0..n-1].

0               n-1    
443 222 555 111 333 444 555 666 888 777 888 999
x v

Solução iterativa

Eis uma solução iterativa do problema:

/* A função devolve 1 se x está em v[0..n-1] e
 * devolve 0 em caso contrário.
 ******************************/

int verifica (int x, int n, int v[])
{
   int j;
   j = 0;
   while (j < n && v[j] != x)  
      j += 1;
   if (j < n) return 1;
   else return 0; 
}

Êta solução elegante!  Veja como ela funciona bem mesmo quando a parte relevante do vetor está vazia, ou seja, quando n  é igual a  0.   Eis uma implementação igualmente elegante:

   int j;
   for (j = 0; j < n; j += 1)
      if (v[j] == x) return 1; 
   return 0; 

Em contraste, eis um jeito muito popular mas muito deselegante de fazer a mesma coisa (usa uma variável a mais, sem necessidade):

   int achou = 0, j = 0;
   while (j < n && achou == 0) { /* deselegante */
      if (v[j] == x) achou = 1;
      else j += 1;
   }
   if (achou == 0) return 0;
   else return 1;

Eis outra solução popular mas deselegante (o caso especial em que o vetor está vazio é tratado em separado, sem necessidade):

   if (n == 0) return 0; /* deselegante */
   else {
      int j = 0;
      while (j < n && v[j] != x) 
         j += 1;
      if (j < n) return 1;
      else return 0; 
   } 

(Eu poderia trocar as duas últimas linhas por return (j < n) mas isso não tornaria as coisas melhores.)  Mais uma solução muito popular mas deselegante e ineficiente (continua calculando mesmo depois de ter encontrado a resposta):

   int j, achei = 0;
   for (j = 0; j < n; j++) /* ineficiente */
      if (v[j] == x) achei = 1; /* deselegante */
   return achei; 

Também existem "soluções" que parecem razoáveis mas estão simplesmente erradas. No primeiro dos exemplos abaixo, a instrução if (v[j] == x) dá resultados imprevisíveis quando j ≥ n.  No segundo exemplo, a ordem das comparações no while está errada: o certo é (j < n && v[j] != x).  No terceiro exemplo, a lógica está errada.

   int j = 0;
   while (j < n && v[j] != x) j += 1;
   if (v[j] == x) /* errado! */
      return 1;
   else return 0; 
   int j = 0;
   while (v[j] != x && j < n) /* errado! */
      j += 1;
   if (j < n) return 1;
   else return 0;
   int achei;
   for (j = 0; j < n; ++j) {
      if (v[j] == x) achei = 1;
      else achei = 0; /* errado! */
   }
   return achei;

Exercícios

  1. Qual a invariante do processo iterativo da função verifica?  [Solução]

Outra solução iterativa

Eis uma idéia mais refinada: que tal devolver o índice de uma ocorrência de x em v[0..n-1]?  É preciso começar com uma decisão de projeto: que fazer se x não está no vetor?  Nossa decisão: devolver -1 nesse caso. Para fazer isso, convém varrer o vetor "de trás pra frente":

/* A função devolve um índice i em 0..n-1 tal que
   x == v[i]. Se tal i não existe, devolve -1. */

int onde (int x, int n, int v[])
{
   int j;
   j = n;
   while (j > 0 && v[j-1] != x) 
      j -= 1;
   return j - 1;
}

Que beleza! Curto e limpo!  Em contraste, eis uma solução muito popular mas ineficiente e deselegante (usa uma variável a mais e complica a lógica sem necessidade):

   int j, k = -1;
   for (j = n-1; j >= 0; --j)
      if (v[j] == x)  k = j; /* deselegante */
   return k; 

Eis uma versão errada do código (esquece de comparar j com 0):

   int j = n-1;
   while (v[j] != x) /* errado! */
      --j;
   return j; 

Com sentinela

Podemos tomar uma decisão de projeto diferente: devolver n se x não estiver em v[0..n-1]. Nesse caso, faz mais sentido percorrer o vetor da esquerda para a direita:

   int j = 0;
   while (j < n && v[j] != x)
      j += 1;
   return j; 

A função fica melhor ainda se usarmos uma sentinela para evitar o grande número de comparações de j com n. Para que isso funcione, v deve ter sido declarado com mais que n elementos.

   int j = 0;
   v[n] = x;  /* sentinela */
   while (v[j] != x)
      j += 1;
   return j;

Solução recursiva

Eis uma solução recursiva do nosso problema de busca:

/* Devolve 1 se x é igual a algum elemento de
   v[0..n-1] e devolve 0 em caso contrário. */

int verifica1 (int x, int v[], int n)
{
   if (n == 0) return 0;
   if (x == v[n-1]) return 1;
   return verifica1 (x, v, n-1);
}

Como a coisa funciona? O número de elementos relevantes de v é n. Se n é 0 então v[0..n-1] é vazio e portanto x não está em v[0..n-1]. Agora suponha que n > 0; nesse caso, x está em v[0..n-1] se e somente se

x é igual a v[n-1]   ou   x está em v[0..n-2].

Eis uma solução recursiva mais suja e deselegante que a anterior, pois coloca o caso n=1 na base da recursão. Ela só funciona se n ≥ 1:

int verifeio (int x, int v[], int n) {
   if (n == 1) { /* deselegante */
      if (x == v[0]) return 1; /* deselegante */
      else return 0;
   }
   if (x == v[n-1]) return 1;
   return verifeio (x, v, n-1);
}

A versão seguinte é ainda pior que a anterior (por que?):

int veripior (int x, int v[], int n) {
   if (n == 0) return 0;
   else {
      int achei;
      achei = veripior (x, v, n-1); /* ineficiente */
      if (achei || x == v[n-1]) return 1;
      else return 0;
   }
}

Exercícios

  1. Critique a seguinte função recursiva. O autor afirma que ela decide se x está em v[0..n-1].
    int verifica (int x, int n, int v[]) {
       if (v[n-1] == x) return 1;
       else return verifica (x, n-1, v);
    }
    
  2. Escreva uma versão recursiva da função onde. Ao receber um inteiro x, um vetor v e um inteiro n, a função deve devolver j no intervalo fechado 0..n-1 tal que   v[j] == x;   se tal j não existe, a função deve devolver -1.
  3. Escreva uma função recursiva que recebe um inteiro x, um vetor v e inteiros ini e fim e devolve j tal que   ini ≤ j ≤ fim-1   e   v[j] é igual a x;   se tal j não existe então devolve ini-1.
  4. Escreva um programa para testar o funcionamento de uma versão recursiva da função onde. O seu programa deve gerar um vetor aleatório para fazer o teste. Os demais detalhes do programa ficam a cargo do leitor.

Busca de um segmento em um vetor

Eis um problema mais geral que o estudado acima. Suponha que queremos verificar se um vetor x[0..m-1] é um segmento de um vetor v[0..n-1]. Exemplo, com m igual a 3 e n igual a 7:

0,172,0  é segmento de  272,165,0,0,172,0,324

mas não é segmento de 272,165,0,0,172,324,0.

Vamos tomar a seguinte decisão de projeto: nossa função devolverá 1 se x é segmento de v e 0 em caso contrário. [Poderíamos ter adotado outras convenções. Por exemplo, devolver j tal que x[0..m-1] é igual a v[j..j+m-1] se tal j existe; caso contrário devolver n.]  Eis uma bela solução:

/* Devolve 1 se x[0..m-1] é segmento de v[0..n-1]
 * e devolve 0 em caso contrário.
 *******************************/

int segmento (int m, int x[], int n, int v[])
{
   int i, j;
   for (j = 0; j <= n-m; ++j) {
      i = 0;
      while (i < m && x[i] == v[j+i]) 
         i += 1;
      if (i == m) return 1;
   }
   return 0;
}

Cada execução do while verifica se x[0..m-1] é igual a v[j..j+m-1]; isso é repetido para j = 0,1, . . . ,n-m.

Veja como a coisa funciona bem até mesmo quando m > n. Veja como funciona bem até mesmo quando n vale 0. Note a ordem em que as comparações são feitas no while: teria sido uma péssima idéia escrever  while (x[i] == v[j+i] && i < m).

Eis outra versão muito boa. Ao contrário da anterior, ela só tem uma só malha e não duas malhas encaixadas. Veja como esta versão funciona bem até mesmo quando m > n ou n0.

   int i = 0, j = 0;
   while (i < m && j+i < n) {
      if (x[i] == v[j+i]) i += 1;
      else {
         i = 0;
         j += 1;
      }
   }
   return i == m;

No início de cada repetição do while temos a seguinte situação: já sabemos que x[0..i-1] é igual a v[j..j+i-1];  se i for igual a m então já temos uma resposta afirmativa;  senão, é preciso comparar x[i] com v[j+i].

Agora considere uma versão deselegante e torta. Vários casos especiais são tratados em separado sem necessidade:

   int i, j;
   if (m > n) return 0; /* deselegante */
   for (j = 0; j <= n-m; j++) 
      if (x[0] == v[j]) { /* deselegante */
         i = 1;
         while (i < m && x[i] == v[j+i]) 
            i += 1;
         if (i == m) return 1;
      }
   return 0;

Solução recursiva

Para terminar, eis uma boa solução recursiva. A idéia é simples: se x[0..m-1] é segmento de v[0..n-1] então ou x[0..m-1] é igual a v[n-m..n-1] ou x[0..m-1] é segmento de v[0..n-2].

/* A função segmento1 devolve 1 se x[0..m-1] for
   segmento de v[0..n-1] e devolve 0 em caso
   contrário. */

int segmento1 (int m, int x[], int n, int v[])
{
   if (m > n) return 0;
   else {
      int i = 0;
      while (i < m && x[i] == v[n-m+i]) 
         i += 1;
      if (i == m) return 1;
      return segmento1 (m, x, n-1, v);
   }
}

Para entender uma função recursiva é essencial dizer, de maneira explícita e precisa, o que a função faz.

Exercícios

  1. Decidir se x[0..m-1] é subseqüência de v[0..n-1], ou seja, decidir se x é o que sobra depois que alguns dos elementos de v são apagados.   Quer uma definição mais formal? Diz-se que x[0..m-1] é subseqüência de v[0..n-1] se

    x[0] = v[s[0]],   x[1] = v[s[1]],   . . .  ,   x[m-1] = v[s[m-1]]

    para algum vetor s[0..m-1] tal que 0s[0] < s[1] < . . . < s[m-1]n-1.   Por exemplo, 12,13,10,3 é uma subseqüência de 11,12,13,11,10,9,7,3,3 mas não de 11,12,10,11,13,9,7,3,3.

  2. Um segmento horizontal de um vetor x[0..n-1] é um subvetor x[p..q] tal que

    x[p] == x[p+1] == ... == x[q].

    O tamanho de um tal subvetor é q-p+1. Um segmento horizontal é máximo se não existe segmento horizontal de tamanho maior. Escreva uma função que receba um vetor crescente não vazio x[0..n-1] e devolva o tamanho de um segmento horizontal máximo no vetor. Procure escrever uma função simples e limpa.

 


URL of this site: www.ime.usp.br/~pf/algoritmos/
1998 | Last modified: Tue Oct 17 08:03:55 BRST 2006
Paulo Feofiloff
IME-USP

Valid HTML 4.0 Transitional     Valid CSS!