A recursão é uma característica em si?


116

... ou é apenas uma prática?

Estou perguntando isso por causa de uma discussão com meu professor: perdi o crédito por chamar uma função recursivamente com base no fato de que não cobrimos a recursão em aula, e meu argumento é que aprendemos isso implicitamente por meio de aprendizado returne métodos.

Estou perguntando aqui porque suspeito que alguém tem uma resposta definitiva.

Por exemplo, qual é a diferença entre os dois métodos a seguir:

public static void a() {
    return a();
    }

public static void b() {
    return a();
    }

Além de " acontinuar para sempre" (no programa real, ele é usado corretamente para alertar o usuário novamente quando fornecido com uma entrada inválida), há alguma diferença fundamental entre ae b? Para um compilador não otimizado, como eles são tratados de maneira diferente?

Em última análise, tudo se resume a saber se, aprendendo a return a()partir de bque também nós mesmos aprendemos a return a()partir a. Nós?


24
Excelente debate. Eu me pergunto se você explicou assim para o seu professor. Se você fez isso, acho que ele deveria lhe dar o crédito perdido.
Michael Yaworski

57
A recursão nem é um conceito exclusivo da ciência da computação. A função de Fibonacci, o operador fatorial e muitas outras coisas da matemática (e possivelmente outros campos) são (ou pelo menos podem ser) expressos recursivamente. O professor exige que você também fique alheio a essas coisas?
Theodoros Chatzigiannakis

34
Em vez disso, o professor deveria dar-lhe crédito extra, por apresentar uma maneira elegante de resolver um problema, ou por, digamos, pensar fora da caixa.

11
Qual foi a missão? Esse é um problema sobre o qual sempre me pergunto, quando você envia uma atribuição de programação, o que está sendo marcado, sua capacidade de resolver um problema ou de usar o que aprendeu. Esses dois não são necessariamente iguais.
Leon

35
FWIW, solicitar entrada até que esteja certo não é um bom lugar para usar recursão, é muito fácil estourar a pilha. Para este caso específico, seria melhor usar algo como a() { do { good = prompt(); } while (!good); }.
Kevin

Respostas:


113

Para responder à sua pergunta específica: Não, do ponto de vista de aprendizagem de um idioma, a recursão não é um recurso. Se seu professor realmente reduziu suas notas por usar um "recurso" que ele ainda não havia ensinado, isso estava errado.

Lendo nas entrelinhas, uma possibilidade é que, ao usar a recursão, você evite usar um recurso que deveria ser um resultado de aprendizado para o curso. Por exemplo, talvez você não tenha usado iteração ou talvez tenha usado apenas forloops em vez de usar ambos forewhile . É comum que uma tarefa tenha como objetivo testar sua capacidade de fazer certas coisas e, se você evitar fazê-las, seu professor simplesmente não poderá conceder a você as notas reservadas para esse recurso. No entanto, se essa foi realmente a causa de suas notas perdidas, o professor deve considerar isso como uma experiência de aprendizagem própria - se a demonstração de certos resultados de aprendizagem for um dos critérios para uma tarefa, isso deve ser explicado claramente aos alunos .

Dito isso, concordo com a maioria dos outros comentários e respostas de que a iteração é uma escolha melhor do que a recursão aqui. Existem algumas razões e, embora outras pessoas as tenham tocado em alguma extensão, não tenho certeza se elas explicaram completamente o que está por trás delas.

Stack Overflows

O mais óbvio é que você corre o risco de receber um erro de estouro de pilha. Realisticamente, o método que você escreveu é muito improvável de realmente levar a um, uma vez que um usuário teria que fornecer uma entrada incorreta muitas vezes para realmente acionar um estouro de pilha.

No entanto, uma coisa a ter em mente é que não apenas o método em si, mas outros métodos superiores ou inferiores na cadeia de chamadas estarão na pilha. Por causa disso, engolir casualmente o espaço disponível na pilha é uma coisa muito indelicada para qualquer método. Ninguém quer ter que se preocupar constantemente com o espaço livre da pilha sempre que escrever código, devido ao risco de que outro código possa ter usado muito dele desnecessariamente.

Isso faz parte de um princípio mais geral em design de software chamado abstração. Essencialmente, quando você liga DoThing(), tudo o que você precisa se preocupar é que a coisa está feita. Você não deve se preocupar com os detalhes de implementação de como isso é feito. Mas o uso ganancioso da pilha quebra esse princípio, porque cada pedaço de código precisa se preocupar com quanta pilha pode assumir com segurança que sobrou para ela por código em outro lugar na cadeia de chamadas.

Legibilidade

O outro motivo é a legibilidade. O ideal que o código deve aspirar é ser um documento legível por humanos, onde cada linha descreve simplesmente o que está fazendo. Use estas duas abordagens:

private int getInput() {
    int input;
    do {
        input = promptForInput();
    } while (!inputIsValid(input))
    return input;
}

versus

private int getInput() {
    int input = promptForInput();
    if(inputIsValid(input)) {
        return input;
    }
    return getInput();
}

Sim, ambos funcionam e, sim, são muito fáceis de entender. Mas como as duas abordagens podem ser descritas em inglês? Eu acho que seria algo como:

Vou solicitar a entrada até que a entrada seja válida e, em seguida, retornarei

versus

Vou pedir uma entrada, então, se a entrada for válida, eu a retornarei, caso contrário, obtenho a entrada e retorno o resultado dela.

Talvez você possa pensar em uma formulação um pouco menos desajeitada para o último, mas acho que sempre descobrirá que o primeiro será uma descrição mais precisa, conceitualmente, do que você está realmente tentando fazer. Isso não quer dizer que a recursão seja sempre menos legível. Para situações em que ela brilha, como travessia de árvore, você poderia fazer o mesmo tipo de análise lado a lado entre a recursão e outra abordagem e quase certamente descobriria que a recursão fornece um código que é mais claramente autodescritivo, linha por linha.

Isoladamente, ambos são pequenos pontos. É muito improvável que isso realmente leve a um estouro de pilha, e o ganho em legibilidade é mínimo. Mas qualquer programa será uma coleção de muitas dessas pequenas decisões; portanto, mesmo que isoladamente elas não importem muito, é importante aprender os princípios por trás de sua execução.


8
Você pode expandir sua afirmação de que a recursão não é um recurso? Argumentei em minha resposta que sim, porque nem todos os compiladores necessariamente o suportam.
Harry Johnston

5
Nem todas as linguagens suportam necessariamente recursão, então não é necessariamente apenas uma questão de escolher o compilador certo - mas você está certo em dizer que "recurso" é uma descrição inerentemente ambígua, então é justo. Seu segundo ponto também é justo do ponto de vista de alguém que está aprendendo a programar (como agora é de costume) sem ter qualquer experiência em programação de código de máquina primeiro. :-)
Harry Johnston

2
Observe que o problema de 'legibilidade' é um problema de sintaxe. Não há nada inerentemente "ilegível" sobre a recursão. Na verdade, a indução é a maneira mais fácil de expressar estruturas de dados indutivos, como loops e listas e sequências e assim por diante. E a maioria das estruturas de dados são indutivas.
nomen

6
Eu acho que você empilhou o baralho com suas palavras. Você descreveu a versão iterativa funcionalmente e vice-versa. Acho que uma descrição mais justa linha por linha de ambos seria “Solicitarei sugestões. Se a entrada não for válida, continuarei repetindo o prompt até obter uma entrada válida. Então eu vou devolvê-lo. ” vs “Vou solicitar a entrada. Se a entrada for válida, irei devolvê-la. Caso contrário, retornarei o resultado de uma reformulação. ” (Meus filhos entenderam o conceito funcional de recomeçar quando estavam na pré-escola, então acho que é um legítimo resumo em inglês do conceito recursivo aqui.)
pjs

2
@HarryJohnston A falta de suporte para recursão seria uma exceção aos recursos existentes, em vez da falta de um novo recurso. Em particular, no contexto desta questão, um "novo recurso" significa "comportamento útil que ainda não ensinamos existe", o que não é verdade para a recursão porque é uma extensão lógica dos recursos que foram ensinados (ou seja, que os procedimentos contêm instruções e chamadas de procedimento são instruções). É como se o professor ensinasse adição a um aluno e depois o repreendesse por adicionar o mesmo valor mais de uma vez porque "não cobrimos a multiplicação".
nmclean

48

Para responder à pergunta literal, ao invés da meta-pergunta: a recursão é uma característica, no sentido de que nem todos os compiladores e / ou linguagens necessariamente permitem isso. Na prática, isso é esperado de todos os compiladores modernos (comuns) - e certamente de todos os compiladores Java! - mas não é universalmente verdadeiro.

Como um exemplo inventado de por que a recursão pode não ser suportada, considere um compilador que armazena o endereço de retorno de uma função em um local estático; pode ser o caso, por exemplo, de um compilador para um microprocessador que não tem uma pilha.

Para tal compilador, quando você chama uma função como esta

a();

é implementado como

move the address of label 1 to variable return_from_a
jump to label function_a
label 1

e a definição de um (),

function a()
{
   var1 = 5;
   return;
}

é implementado como

label function_a
move 5 to variable var1
jump to the address stored in variable return_from_a

Esperançosamente, o problema quando você tenta chamar a()recursivamente em tal compilador seja óbvio; o compilador não sabe mais como retornar da chamada externa, porque o endereço de retorno foi sobrescrito.

Para o compilador que eu realmente usei (final dos anos 70 ou início dos 80, eu acho) sem suporte para recursão, o problema era um pouco mais sutil do que isso: o endereço de retorno seria armazenado na pilha, assim como nos compiladores modernos, mas as variáveis ​​locais não eram 't. (Teoricamente, isso deveria significar que a recursão era possível para funções sem variáveis ​​locais não estáticas, mas não me lembro se o compilador suportava isso explicitamente ou não. Ele pode ter precisado de variáveis ​​locais implícitas por algum motivo.)

Olhando para o futuro, posso imaginar cenários especializados - sistemas fortemente paralelos, talvez - onde não ter que fornecer uma pilha para cada thread pode ser vantajoso e onde, portanto, a recursão só é permitida se o compilador puder refatorá-la em um loop. (É claro que os compiladores primitivos que discuto acima não eram capazes de tarefas complicadas como refatorar código.)


Por exemplo, o pré-processador C não oferece suporte à recursão em macros. As definições de macros se comportam de maneira semelhante às funções, mas você não pode chamá-las recursivamente.
sth

7
Seu "exemplo inventado" não é tão inventado: o padrão Fortran 77 não permitia que funções se autodenominassem recursivamente - o motivo é exatamente o que você descreve. (Acredito que o endereço para o qual pular quando a função foi concluída foi armazenado no final do código da própria função, ou algo equivalente a esse arranjo.) Veja aqui um pouco sobre isso.
alexis

5
Linguagens de shader ou linguagens GPGPU (digamos, GLSL, Cg, OpenCL C) não suportam recursão, por exemplo. Na medida em que, o argumento "nem todas as línguas o suportam" é certamente válido. A recursão presume o equivalente a uma pilha (não precisa ser necessariamente uma pilha , mas precisa haver um meio de armazenar endereços de retorno e quadros de função de alguma forma ).
Damon

Um compilador Fortran em que trabalhei um pouco no início dos anos 1970 não tinha uma pilha de chamadas. Cada sub-rotina ou função tinha áreas de memória estática para o endereço de retorno, parâmetros e suas próprias variáveis.
Patricia Shanahan

2
Mesmo algumas versões do Turbo Pascal desabilitavam a recursão por padrão, e você tinha que definir uma diretiva do compilador para habilitá-la.
dan04

17

O professor quer saber se você estudou ou não. Aparentemente você não resolveu o problema da maneira que ele te ensinou ( o bom caminho ; iteração) e, portanto, considera que não. Sou totalmente a favor de soluções criativas, mas neste caso tenho que concordar com seu professor por um motivo diferente:
se o usuário fornecer entrada inválida muitas vezes (ou seja, mantendo a tecla Enter pressionada), você terá uma exceção de estouro de pilha e seu solução irá falhar. Além disso, a solução iterativa é mais eficiente e fácil de manter. Acho que é por isso que seu professor deveria ter lhe dado.


2
Não fomos instruídos a realizar essa tarefa de uma maneira específica; e aprendemos sobre métodos, não apenas iteração. Além disso, eu deixaria o que é mais fácil de ler de acordo com a preferência pessoal: eu escolhi o que parecia bom para mim. O erro do SO é novo para mim, embora a ideia de que a recursão é um recurso em si ainda não pareça fundamentada.

3
“Eu deixaria qual é mais fácil de ler de acordo com a preferência pessoal”. Acordado. A recursão não é um recurso Java. Esses são.
mike

2
@Vality: Eliminação da chamada final? Algumas JVMs podem fazer isso, mas lembre-se de que também é necessário manter um rastreamento de pilha para exceções. Se permitir a eliminação da chamada final, o rastreamento de pilha, gerado ingenuamente, pode se tornar inválido, portanto, algumas JVMs não realizam o TCE por esse motivo.
icktoofay

5
De qualquer maneira, confiar na otimização para tornar seu código quebrado menos é uma forma muito ruim.
cHao de

7
+1, veja que no Ubuntu recentemente a tela de login foi quebrada quando o usuário pressionou o botão Enter continuamente, o mesmo aconteceu com o XBox
Sebastian

13

Deduzir pontos porque "não cobrimos a recursão na aula" é horrível. Se você aprendeu como chamar a função A, que chama a função B, que chama a função C, que retorna para B, que retorna para A, que retorna para o chamador, e o professor não lhe disse explicitamente que essas funções devem ser diferentes (que seria o caso em versões antigas do FORTRAN, por exemplo), não há razão para que A, B e C não possam ter a mesma função.

Por outro lado, teríamos que ver o código real para decidir se, em seu caso específico, usar a recursão é realmente a coisa certa a fazer. Não há muitos detalhes, mas parece errado.


10

Há muitos pontos de vista a serem observados em relação à pergunta específica que você fez, mas o que posso dizer é que, do ponto de vista da aprendizagem de um idioma, a recursão não é uma característica por si só. Se o seu professor realmente reduziu a sua pontuação por usar um "recurso" que ele ainda não havia ensinado, isso estava errado, mas como eu disse, existem outros pontos de vista a serem considerados aqui que na verdade fazem o professor estar certo ao deduzir pontos.

Pelo que posso deduzir de sua pergunta, usar uma função recursiva para pedir entrada em caso de falha de entrada não é uma boa prática, já que todas as chamadas de funções recursivas são colocadas na pilha. Uma vez que essa recursão é orientada pela entrada do usuário, é possível ter uma função recursiva infinita e, portanto, resultando em um StackOverflow.

Não há diferença entre esses 2 exemplos que você mencionou em sua pergunta no sentido do que eles fazem (mas diferem em outras maneiras) - Em ambos os casos, um endereço de retorno e todas as informações de método estão sendo carregados na pilha. Em um caso de recursão, o endereço de retorno é simplesmente a linha logo após a chamada do método (claro que não é exatamente o que você vê no código em si, mas sim no código que o compilador criou). Em Java, C e Python, a recursão é bastante cara em comparação com a iteração (em geral) porque requer a alocação de um novo quadro de pilha. Sem mencionar que você pode obter uma exceção de estouro de pilha se a entrada não for válida muitas vezes.

Acredito que o professor deduziu pontos, já que a recursão é considerada um assunto próprio e é improvável que alguém sem experiência em programação pense em recursão. (Claro que não significa que não vão, mas é improvável).

IMHO, acho que o professor está certo ao deduzir os pontos. Você poderia facilmente ter levado a parte de validação para um método diferente e usá-lo assim:

public bool foo() 
{
  validInput = GetInput();
  while(!validInput)
  {
    MessageBox.Show("Wrong Input, please try again!");
    validInput = GetInput();
  }
  return hasWon(x, y, piece);
}

Se o que você fez pode realmente ser resolvido dessa maneira, então o que você fez foi uma má prática e deve ser evitado.


O propósito do método em si é validar a entrada e, em seguida, chamar e retornar o resultado de outro método (é por isso que ele retorna a si mesmo). Para ser mais específico, ele verifica se a jogada em um jogo Tic-Tac-Toe é válida e, em seguida, retorna hasWon(x, y, piece)(para verificar apenas a linha e coluna afetadas).

você poderia facilmente pegar APENAS a parte da validação e colocá-la em outro método chamado "GetInput", por exemplo, e então usá-la como escrevi na minha resposta. Eu editei minha resposta com a forma como ela deveria ser. Claro que você pode fazer GetInput retornar um tipo que contém as informações de que você precisa.
Yonatan Nir

1
Yonatan Nir: Quando a recursão foi uma prática ruim? Talvez o JVM exploda porque o Hotspot VM não pôde otimizar por causa da segurança do código de byte e outras coisas seriam um bom argumento. Como o seu código é diferente do que usa uma abordagem diferente?

1
A recursão nem sempre é uma prática ruim, mas se puder ser evitada e manter o código limpo e fácil de manter, deve ser evitada. Em Java, C e Python, a recursão é bastante cara em comparação com a iteração (em geral) porque requer a alocação de um novo quadro de pilha. Em alguns compiladores C, pode-se usar um sinalizador do compilador para eliminar essa sobrecarga, que transforma certos tipos de recursão (na verdade, certos tipos de chamadas finais) em saltos em vez de chamadas de função.
Yonatan Nir

1
Não está claro, mas se você substituiu um loop por um número indefinido de iterações com recursão, isso é ruim. Java não garante otimização de chamada final, então você pode facilmente ficar sem espaço de pilha. Em Java, não use recursão, a menos que seja garantido que você tenha uma quantidade limitada de iterações (geralmente logarítmica em comparação com o tamanho total dos dados).
hyde de

6

Talvez seu professor ainda não tenha ensinado, mas parece que você está pronto para aprender as vantagens e desvantagens da recursão.

A principal vantagem da recursão é que os algoritmos recursivos costumam ser muito mais fáceis e rápidos de escrever.

A principal desvantagem da recursão é que algoritmos recursivos podem causar estouro de pilha, uma vez que cada nível de recursão requer que um quadro de pilha adicional seja adicionado à pilha.

Para código de produção, onde o dimensionamento pode resultar em muitos mais níveis de recursão na produção do que nos testes de unidade do programador, a desvantagem geralmente supera a vantagem e o código recursivo é frequentemente evitado quando prático.


1
Qualquer algoritmo recursivo potencialmente arriscado pode sempre ser trivialmente reescrito para usar uma pilha explícita - a pilha de chamadas é, afinal, apenas uma pilha. Nesse caso, se você reescrever a solução para usar uma pilha, parecerá ridículo - mais uma evidência de que a resposta recursiva não é muito boa.
Aaronaught

1
Se o estouro de pilha for um problema, você deve usar uma linguagem / tempo de execução que ofereça suporte à otimização de chamada final, como .NET 4.0 ou qualquer linguagem de programação funcional
Sebastian

Nem todas as recursões são chamadas de cauda.
Warren Dew

6

Em relação à questão específica, a recursão é uma característica, estou inclinado a dizer que sim, mas depois de reinterpretar a questão. Existem opções de design comuns de linguagens e compiladores que tornam a recursão possível, e existem linguagens Turing-completas que não permitem a recursão de forma alguma . Em outras palavras, a recursão é uma habilidade que é habilitada por certas escolhas no design da linguagem / compilador.

  • O suporte a funções de primeira classe torna a recursão possível sob suposições mínimas; veja um exemplo de loops de escrita em Unlambda , ou esta expressão obtusa do Python que não contém referências, loops ou atribuições:

    >>> map((lambda x: lambda f: x(lambda g: f(lambda v: g(g)(v))))(
    ...   lambda c: c(c))(lambda R: lambda n: 1 if n < 2 else n * R(n - 1)),
    ...   xrange(10))
    [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
    
  • Linguagens / compiladores que usam vinculação tardia ou que definem declarações futuras tornam a recursão possível. Por exemplo, embora Python permita o código abaixo, isso é uma escolha de design (ligação tardia), não um requisito para um sistema Turing-completo . Funções mutuamente recursivas geralmente dependem do suporte para declarações de encaminhamento.

    factorial = lambda n: 1 if n < 2 else n * factorial(n-1)
    
  • Linguagens tipadas estaticamente que permitem tipos definidos recursivamente contribuem para habilitar a recursão. Veja esta implementação do Y Combinator em Go . Sem tipos recursivamente definidos, ainda seria possível usar recursão em Go, mas acredito que o combinador Y especificamente seria impossível.


1
Isso fez minha cabeça explodir, especialmente o Unlambda +1
John Powell

Os combinadores de ponto fixo são difíceis. Quando decidi aprender programação funcional, me forcei a estudar o combinador Y até entendê-lo e depois aplicá-lo para escrever outras funções úteis. Demorou um pouco, mas valeu a pena.
wberry

5

Pelo que posso deduzir de sua pergunta, usar uma função recursiva para solicitar entrada em caso de falha na entrada não é uma boa prática. Por quê?

Porque toda chamada de função recursiva é colocada na pilha. Uma vez que esta recursão é conduzida pela entrada do usuário, é possível ter uma função recursiva infinita e, portanto, resultando em um StackOverflow :-p

Ter um loop não recursivo para fazer isso é o caminho a percorrer.


A maior parte do método em questão e o propósito do método em si é validar a entrada por meio de várias verificações. O processo começa tudo de novo se a entrada for inválida até que a entrada esteja correta (conforme instruído).

4
@fay Mas se a entrada for inválida muitas vezes, você receberá um StackOverflowError. A recursão é mais elegante, mas, a meu ver, geralmente é mais um problema do que um loop regular (devido às pilhas).
Michael Yaworski

1
Esse é um ponto interessante e bom, então. Eu não havia considerado esse erro. Porém, o mesmo efeito pode ser alcançado while(true)chamando o mesmo método? Nesse caso, eu não diria que isso suporta qualquer diferença entre recursão, bom saber como é.

1
@fay while(true)é um loop infinito. A menos que você tenha uma breakdeclaração, não vejo sentido nisso, a menos que você esteja tentando travar seu programa lol. Meu ponto é, se você chamar o mesmo método (que é recursão), às vezes ele vai dar a você um StackOverflowError , mas se você usar um loop whileou for, não vai. O problema simplesmente não existe com um loop regular. Talvez eu tenha entendido mal, mas minha resposta para você é não.
Michael Yaworski

4
Isso para mim, honestamente, parece ser provavelmente o verdadeiro motivo pelo qual o professor tirou as notas =) Ele pode não ter explicado muito bem, mas é uma reclamação válida dizer que você estava usando de uma forma que seria considerada um estilo muito ruim, caso contrário completamente falha em um código mais sério.
Comandante Coriander Salamander

3

Recursão é um conceito de programação , um recurso (como iteração) e uma prática . Como você pode ver no link, há um grande domínio de pesquisa dedicado ao assunto. Talvez não precisemos nos aprofundar tanto no assunto para entender esses pontos.

Recursão como recurso

Em termos simples, Java o suporta implicitamente, porque permite que um método (que é basicamente uma função especial) tenha "conhecimento" de si mesmo e de outros métodos que compõem a classe a que pertence. Considere uma linguagem em que este não seja o caso: você seria capaz de escrever o corpo desse método a, mas não seria capaz de incluir uma chamada para adentro dele. A única solução seria usar iteração para obter o mesmo resultado. Em tal linguagem, você teria que fazer uma distinção entre funções cientes de sua própria existência (usando um token de sintaxe específico) e aquelas que não o fazem! Na verdade, todo um grupo de linguagens faz essa distinção (veja o Lisp e lambdas ) para se chamar recursivamente (novamente, com uma sintaxe dedicada). ML famílias por exemplo). Curiosamente, Perl permite até funções anônimas (as chamadas

sem recursão?

Para linguagens que nem mesmo suportam a possibilidade de recursão, muitas vezes há outra solução, na forma do combinador de ponto fixo , mas ainda requer que a linguagem suporte funções como os chamados objetos de primeira classe (ou seja, objetos que podem ser manipulado dentro da própria linguagem).

Recursão como prática

Ter esse recurso disponível em um idioma não significa necessariamente que seja idiomático. No Java 8, as expressões lambda foram incluídas, então pode se tornar mais fácil adotar uma abordagem funcional para a programação. No entanto, existem considerações práticas:

  • a sintaxe ainda não é muito favorável à recursão
  • compiladores podem não ser capazes de detectar essa prática e otimizá-la

O resultado final

Felizmente (ou mais precisamente, para facilidade de uso), Java permite que os métodos reconheçam a si mesmos por padrão e, portanto, suportam a recursão, então este não é realmente um problema prático, mas ainda permanece teórico, e suponho que seu professor queria abordá-lo especificamente. Além disso, à luz da evolução recente da língua, pode se tornar algo importante no futuro.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.