Como resolver desafios de código

Tutoriais - 15/Jul/2022 - por Campus Code

Antes de começar, um pequeno aviso: este artigo tem como principal objetivo dar algumas dicas para quem está participando de um dos processos seletivos da Campus Code, mais especificamente, na etapa de desafios de código. Para iniciantes em programação todo o processo pode ser bastante complexo e, se você estiver encontrando alguma dificuldade, talvez possa encontrar aqui um direcionamento para te ajudar a chegar na solução.

Trabalhar com desenvolvimento de software envolve elaborar soluções para problemas. Não estamos falando de problemas como erros que cometemos no nosso código, mas problemas do mundo real, que requerem soluções reais, por exemplo: como fazer um determinado cálculo com base em uma tabela de dados ou como agregar um conjunto de informações e obter dela exatamente o resultado esperado.

Neste artigo vamos falar de algumas estratégias que você pode usar para definir seu próprio processo de resolução de problemas.

Compreenda o problema com clareza

Antes de começar a escrever qualquer linha de código, é muito importante refletir sobre o problema em questão e ter certeza de que o compreendeu. É bastante comum termos a compreensão incorreta sobre um problema no primeiro contato, para em seguida nos depararmos com um código falho. Depois, ficamos reclamando que as coisas estão dando errado, sendo que, na realidade, nossa compreensão sobre o problema estava incorreta em primeiro lugar. Por isso, antes de começar a escrever seu código, certifique-se de que entendeu o problema com clareza.

Dito isso, o que podemos fazer para compreender melhor um problema? O primeiro passo é: ler a questão com calma. É impressionante a quantidade de vezes que ajudamos pessoas a resolver uma dificuldade apenas falando para ler a questão com calma. A ansiedade é nossa inimiga nessas horas!

Faça perguntas relevantes

Inicialmente, esse item pode parecer estranho, mas a ideia é que você se questione e procure as respostas dentro do problema que está tentando resolver. Existem elementos básicos em qualquer situação que podem ser identificados e que certamente vão te ajudar.

Qual é o resultado esperado? O resultado final para solucionar um problema pode ser de muitas naturezas: poderia ser uma string, um array, conteúdos salvos em um arquivo e etc. Mas é essencial saber exatamente o que é esperado para podermos elaborar uma estratégia adequada para chegar lá.

Quais são as informações prontamente disponíveis? Quais são os tipos de dados presentes (string, arrays, inteiros)? É por meio dessas informações que vamos chegar no resultado desejado. Somente conhecendo exatamente com o que podemos trabalhar que chegaremos lá. Com essas informações podemos começar a identificar qual é o arsenal de ferramentas que temos para tratar os dados. Se temos um dado inicial do tipo string podemos, por exemplo, procurar algum método que separe cada caracter dentro de um array. Se temos números inteiros, podemos fazer operações matemáticas e assim por diante. E para sabermos exatamente o que pode ser feito com cada tipo de dado, pode-se aplicar o item “Pesquisa” deste artigo.

Quais relações podem ser estabelecidas entre as informações disponíveis? Muitas vezes a solução do problema está escondida na relação entre os valores disponíveis. Se você encontrar essa relação, vai ficar bem mais fácil de resolvê-lo. Por exemplo, digamos que você precisa criar um algoritmo que recebe uma string e precisa retornar essa mesma sequência de caracteres, mas alternando maiúsculas e minúsculas. Se compreendemos que cada elemento da string equivale a um índice, podemos usar esses índices que alternam entre par e ímpar para definir como cada caractere deve ser retornado.

Quais são os pontos-chave do problema? Todo desafio de código possui pontos-chave que definem suas limitações, prioridades, condições etc, e é essencial que você encontre cada um deles. Por exemplo, considerando um site na internet que permite apenas o cadastro de pessoas maiores de idade, uma condição evidente que deve ser avaliada é a idade da pessoa. Para isso, o processo de preenchimento de formulário para criar um perfil poderia pedir a data de nascimento, fazer uma conta para saber se a pessoa tem mais de 18 anos e somente permitir a criação da conta depois que essa condição for avaliada.

Tendo em mente os pontos-chave do problema, podemos voltar a atenção ao processo: partindo dos dados iniciais, como podemos chegar no resultado desejado? Por exemplo, se temos uma string com nomes e precisamos retornar um array com cada nome separado, a primeira coisa que vem à mente é encontrar formas de lidar com essa strings. É óbvio que esse exemplo mostra um problema muito pontual e que problemas maiores vão exigir mais passos, mas esse ponto leva ao item seguinte…

Divida o problema em etapas menores

Tentar resolver um problema complexo provavelmente vai exigir uma sequência lógica de etapas que devem ser executadas. E isso é bom, porque resolver problemas menores vai ser muito mais fácil do que resolver um grande problema de uma só vez.

Encontre no problema que você está atacando cada um dos passos que você vai precisar executar e vá resolvendo um de cada vez, identificando os tipos de dados disponíveis e os dados retornados em cada etapa.

Para conseguir encontrar cada um dos passos necessários para resolver o problema, podemos usar a estratégia a seguir: pseudocódigo.

Escreva pseudocódigo

Agora que temos os dados disponíveis, o resultado esperado e chegamos mais ou menos a uma estrutura de passos que podemos seguir para chegar à solução, como podemos conferir se a solução imaginada atende nossas necessidades?

Antes de começar a escrever código “pra valer” numa linguagem de programação, podemos escrever pseudocódigo, que consiste em código escrito de forma que humanos podem entender facilmente. Não existem regras para isso, então o importante é que você consiga elaborar os passos da sua lógica de forma clara e ordenada, e assim possa visualizar o processo como um todo, conferindo se houve algum erro no raciocínio.

Vamos considerar o exemplo do algoritmo que recebe uma string e deve retornar essa mesma string alternando os caracteres em maiúsculas e minúsculas. Um pseudocódigo poderia ser algo como:

string = ‘pseudocodigo”

dividir cada caracter de string em p, s, e, u, d, o, c, o, d, i, g, o.

para cada letra de string, avaliar o índice da posição do elemento:

se índice for divisível por 2, letra deve ficar maiúscula.
se índice não for divisível por 2, letra deve ficar minúscula.

juntar letras modificadas para retornar PsEuDoCoDiGo.

É claro que talvez essa estratégia não funcione para qualquer tipo de problema, mas é uma forma interessante de lidar com algumas dificuldades para conferir a lógica dos seus algoritmos.

Pesquisa

Finalmente, chegamos ao tão esperado momento: escrever o código na linguagem de programação escolhida. Essa etapa vai variar muito de acordo com a sua experiência com programação, mas acho que as dicas são mais relevantes para pessoas menos experientes. Aqui vou usar Ruby como exemplo, mas você pode aplicar essas sugestões em qualquer linguagem de programação.

Em primeiro lugar: procure a documentação oficial da linguagem. Em Ruby costumo usar: https://ruby-doc.org/. Cada documentação vai ter seu próprio layout e ferramentas, mas a ideia vai ser sempre a mesma. Depois que você souber o que quer fazer, por exemplo “Preciso encontrar uma forma de quebrar uma string em caracteres”, você pode procurar na documentação as informações sobre strings. Seguindo nosso exemplo, eu poderia fazer uma busca na barra de pesquisa e acessar o link que aparece:

A imagem mostra a documentação da versão 3.1.2 do Ruby, mas você pode encontrar a versão que estiver utilizando. Na página da documentação sobre strings você pode começar a ler os métodos disponíveis e entender o que eles fazem. No começo vai ser confuso, mas aos poucos você se acostuma. Para facilitar um pouco esse processo, vamos falar sobre o método split. Considere o trecho abaixo:

split(pattern=nil, [limit]) → an_array

Esse trecho da documentação (na versão 3.1.2) mostra como o método split pode ser utilizado. O método divide uma string em substrings de acordo com um padrão (pattern) e um limite (limit), passados como parâmetros no método e, ao final, um array é retornado. O padrão será utilizado como referência para os intervalos de quebras e o limite, limita a quantidade de elementos que o array final deve ter. Vamos ver um exemplo:

# passando como parâmetro o padrão ‘-’ 
'a-b-c-d-e'.split('-')
 => ["a", "b", "c", "d", "e"] 

# passando como parâmetro o padrão ‘-’ e o limite 3
'a-b-c-d-e'.split('-', 3)
=> ["a", "b", "c-d-e"]  

As descrições sobre o funcionamento dos métodos podem parecer muito confusas inicialmente, mas aos poucos, com o tempo, isso vai ficando cada vez mais fácil. Também são apresentados exemplos de uso do método, que são bastante úteis para entender melhor como utilizá-los:

Você pode usar ferramentas como o IRB (Interactive Ruby Shell) que oferecem um ambiente em que pode testar código Ruby de forma fácil. Ele vai te ajudar a fazer testes rápidos de códigos para experimentar com métodos diferentes até entender o que cada um faz. Se tiver a linguagem de programação instalada no seu computador, basta rodar o comando irb no terminal.

Recomendamos sempre pesquisar na documentação oficial da linguagem, materiais didáticos e outros conteúdos de fontes confiáveis. Atente também para a versão que está utilizando.

Debug

Debug é o nome dado ao processo de localizar e resolver defeitos no seu código, que impedem um software de funcionar corretamente. Se você já souber como usar ferramentas de debug, use-as para entender melhor como seu código está funcionando e fazer correções conforme seja necessário. Mas se não souber temos algumas dicas para te ajudar.

Uma maneira simples de conferir o que seu código está fazendo a cada etapa é imprimir na tela os resultados parciais do processamento dos dados. Em Ruby, por exemplo, você pode usar o comando puts para imprimir valores:


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].each do |value|
  puts value if value.even?
end

# 0
# 2
# 4
# 6
# 8

O trecho de código acima, itera pelo array e imprime na tela o valor do elemento caso ele seja par. Se o código começa a ficar mais complexo, torna-se cada vez mais difícil manter em mente como os valores estão sendo processados, então é conveniente termos uma forma fácil de conferir como esses dados estão a cada passo. E você pode colocar quantos comandos puts quiser ao longo do código. Outra dica é que você pode usar interpolação para imprimir mensagens mais legíveis, por exemplo:


[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].each do |value|
  puts “O valor #{ value } é par” if value.even?
end

# O valor 0 é par
# O valor 2 é par                                                                                                                                         
# O valor 4 é par                                                                                                                                         
# O valor 6 é par                                                                                                                                         
# O valor 8 é par 

Dicas bônus: se você estiver usando algum framework de testes, como o Rspec para Ruby por exemplo, pode aproveitar que ele mostra os valores retornados pelos métodos testados. Isso vai ajudar a compreender melhor como seu código está funcionando. Além disso, o IRB também é um bom lugar para fazer testes rápidos do seu código (foi o que eu usei para testar o código acima).

Conclusão

Resolver problemas é uma habilidade importante em desenvolvimento de software! Mais do que saber de cabeça métodos diferentes de uma linguagem, é mais interessante saber como resolver o problema com as ferramentas que você tem prontamente disponíveis. Então não perca tempo tentando encontrar um método ou função mágica para resolver seu problema em uma linha. Talvez esse método nem exista. Você vai precisar equilibrar seu tempo entre pesquisar soluções prontas e elaborar algoritmos próprios que façam o que você precisa.

Campus Code