Introdução
Chegou a hora de finalmente entender uma das frases mais famosas no mundo da programação, engenharia e em outras áreas: "Seja um solucionador de problemas". Mais do que isso, você entenderá o sentido prático disso, por meio de um fluxo de trabalho que pode te ajudar a compreender e resolver problemas de um jeito novo.
Para que isso aconteça, você precisa resistir ao impulso de sair escrevendo código assim que se depara com um desafio e passe a enxergar a solução como um conjunto de etapas que, só então, serão implementadas em uma linguagem de programação. Dessa forma, é possível fazer da sua solução mais clara e independente da ferramenta usada para implementa-lá.
Esse material é uma síntese do que aprendi em uma das seções do curso Javascript Algorithms and Data Structures Masterclass, do Colt Steele. Recomendo que você confira caso sinta a necessidade de desenvolver uma base nesse assunto, por isso vou deixar o link para acessar o curso, ao final desse post.
Quem se importa?
Você deveria. Resolver problemas de diferentes níveis é algo extremamente recorrente no dia a dia de um dev, além de ser um componente chave em muitas entrevistas de emprego na área. Por isso, é importante ter ao menos uma noção de como começar a trabalhar, caso um problema novo surja de repente. Tendo em mente o segundo exemplo, o cenário desenvolvido nesse post tem como base uma entrevista fictícia, onde os entrevistadores pedirão a você para implementar algum tipo de solução e você deve mostrá-los a sua abordagem para a situação dada.
Apresentando o cenário
Imagine que, em uma entrevista técnica para uma vaga em programação, é solicitado que você desenvolva uma solução que verifique a ocorrência de cada caractere em uma string qualquer.
Apesar de ser um desafio simples e você pode até já ter pensado em uma forma de implementar isso, resista ao impulso de disparar código no editor de texto, de imediato. Você verá que há muita coisa a ser discutida.
Fluxo de trabalho
O fluxo apresentado no curso que mencionei sugere que abordemos cada novo problema em 5 passos: Entender o problema, Explorar exemplos, Decompor em etapas, Resolver/Simplificar o problema e, finalmente, Refatorar. Isso vai garantir que você (e os entrevistadores) visualizem a sua linha de pensamento e onde você pretende chegar, antes mesmo de escrever uma única linha de código.
Passo 1: Entenda o problema
O primeiro passo é garantir que você realmente compreendeu o problema e possa esclarecer aspectos importantes sobre o que é preciso ser feito. Para fazer isso, Steele recomenda que você faça as seguintes perguntas:
- Posso repetir o problema em minhas próprias palavras?
- Quais entradas devem ser introduzidas?
- Quais saídas são esperadas, dessa solução?
- As saídas podem ser deduzidas das entradas? Ou seja, tenho informações suficientes para gerar o resultado esperado?
- Como eu deveria rotular os dados presentes nessa solução?
As três primeiras podem ser feitas aos entrevistadores, caso estejam abertos a consulta, e as duas últimas devem ser seus questionamentos.
Reformulando o problema, você declara que "Devo construir uma função que analise uma string e devolva o número de vezes em que cada caractere apareceu". Confirmada a informação, seguimos adiante.
Digamos que, após tentar esclarecer o problema, você obteve as informações de que a entrada deve ser uma string simples e que a saída pode ser expressa por meio de um objeto cujas chaves são os caracteres e os valores, suas respectivas ocorrências.
Passo 2: Explore exemplos
Agora que você tem uma noção clara do que deve ser inserido e o que deve ser retornado como resultado, você pode rabiscar, em seu editor (ou quadro, se for o caso), alguns exemplos práticos do que deve ser esperado. Nesse momento, é recomendado que você busque explorar:
- casos simples
- casos mais complexos
- casos com entradas vazias
- casos com entradas inválidas
Assim, você chega a um esboço como esse
charCount("hello"); // { h: 1, e: 1, l: 2, o: 1 }
charCount("Hello World!"); // ???
charCount("My phone number is 23453"); // ???
charCount(); // ???
charCount(""); // ???
charCount(true); // ???
charCount(12345) // ???
Note que agora você tem alguns casos especiais em seu rascunho, levando aos questionamentos "Devo considerar espaços, números e caracteres especiais? E quanto aos minúsculos e maiúsculos? O que fazer em casos de entradas inválidas?"
Esclarecidas essas infomações, é definido, por exemplo, que apenas caracteres alfanuméricos devem ser considerados na contagem, todos devem ser analisados em minúsculo e entradas inválidas devem retornar um objeto vazio como resultado.
charCount("hello"); // { h: 1, e: 1, l: 2, o: 1 }
charCount("Hello World!"); // { h: 1, e: 1, l: 3, o: 2, w: 1, d:1 }
charCount("My phone number is 23453"); // { m: 2, p: 1, ..., 2: 1, 3: 2, ... }
charCount(); // {}
charCount(""); // {}
charCount(true); // {}
charCount(12345) // {}
Passo 3: Decomponha em etapas
Definida a interface da sua solução, é preciso fazer um esboço das etapas necessárias para atingir os resultados esperados. Você pode fazer isso utilizando Pseudocódigo ou apenas comentando as etapas, de maneira clara.
function charCount(str) {
// criar o objeto de resultado
// verificar entradas inválidas e, se for o caso, retornar objeto vazio
// remover caracteres indesejados e espaços
// iterar sobre a string modificada
// se o caractere já estiver adicinado ao objeto, incrementar 1
// se o caractere não estiver, criar chave e adicionar o valor 1
//retornar objeto de resultado
}
Percebe-se, agora, que o processo de resolução ficou extremamente claro e, mesmo que você não implemente a solução completamente, é possível saber com clareza aonde você pretendia chegar.
Passo 4: Resolva/Simplifique
Chegou a hora de codar a solução desenvolvida e ver algumas dicas para nos prepararmos para alguns imprevistos que podem ocorrer.
function charCount(str) {
// criar o objeto de resultado
const result = {};
// verificar entradas inválidas e, se for o caso, retornar objeto vazio
if (typeof str != 'string') {
return result;
}
// remover caracteres indesejados e espaços
const sanitizedString = str.toLowerCase().replace(/[^a-z0-9]/g, '');
// iterar sobre a string modificada
for (let i = 0; i < sanitizedString.length; i++) {
let char = sanitizedString[i];
if (result[char] > 0) {
// se o caractere já estiver adicinado ao objeto, incrementar 1
result[char]++;
} else {
// se o caractere não estiver, criar chave e adicionar o valor 1
result[char] = 1;
}
}
//retornar objeto de resultado
return result;
}
Temos aqui uma implementação que resolve o problema dado, mas nem sempre pode ser tão simples. Por exemplo, você pode acabar esquecendo a expressão regular que seleciona os caracteres que devem ser excluídos ou até mesmo o método que deixa strings em caixa baixa. Quando checar a resposta para esses pequenos problemas não for uma opção, você pode utilizar duas abordagens. A primeira consiste em identificar a dificuldade e simplificar a solução, eliminando os pontos que você não lembra como implementar e ignorando-os, temporariamente. Nesse caso, mantendo caracteres especiais e letras maiúsculas na string e prosseguindo com a solução. Em algumas situações você pode lembrar qual a "peça" que está faltando, enquanto faz outra coisa. O outro caminho é ser comunicativo a respeito a sua dificuldade, dizendo coisas como "não consigo lembrar qual método converte strings em minúsculas, nesse momento" ou qualquer outra dificuldade similar. Isso vai sinalizar que você sabe o que está fazendo e, quem sabe, fazer os entrevistadores darem pequenas dicas.
Passso 5: Volte e Refatore
A solução resolveu o problema, isso quer dizer que chegou a hora de fechar o notebook e finalizar o desafio, certo?
Nada disso.
É extremamente tentador se contentar com a solução que simplismente funciona. Em casos de demandas muito urgentes, é a única opção, mas nem sempre é assim. Agora é o momento da autoavaliação e de sermos curiosos, persistentes e julgadores do nosso próprio trabalho. Remover variáveis desnecessárias, renomear elementos e considerar soluções alternativas são boas práticas. Novamente, Steele nos deixa com uma lista de perguntas que podemos fazer a nós mesmos ou a outras pessoas, quando nos deparamos com a primeira implementação funcional:
- Você pode checar os resultados?
- Você desenvolveria isso de outra forma?
- Consegue entender a solução de primeira?
- É possível utilizar este resultado para solucionar outro problema?
- Você consegue melhorar a performance da sua solução?
- Consegue pensar em maneiras de refatorar isso?
- Como é que outras pessoas resolveram esse mesmo problema?
Nem é preciso dizer o quanto essas perguntas podem mudar o aspecto do seu código. Com relação ao último item, é recomendado que você procure em sites como o Github ou outras plataformas, para entender as diferentes abordagens. Um detalhe importante é que nem sempre o que você procura estará implementado na linguagem que você está utilizando no momento, por isso é extremamente recomendado ver códigos feitos com outras ferramentas (desde que você se sinta confortável com isso).
Para finalizar, deixo algumas alternativas para solucionar o mesmo problema, que variam em estética, performance e até linha de raciocínio utilizada.
function charCount(str) {
const result = {};
if (typeof str != 'string') {
return result;
}
const sanitizedString = str.toLowerCase().replace(/[^a-z0-9]/g, '');
for (let char of sanitizedString) {
result[char] = ++result[char] || 1;
}
return result;
}
function charCount(str) {
const result = {};
for (let char of str) {
char = char.toLowerCase();
if (/[a-z0-9]/.test(char)) {
result[char] = ++result[char] || 1;
}
}
return result;
}
function charCount(str) {
const result = {};
for (let char of str) {
if (isAlphaNumeric(char)) {
char = char.toLowerCase();
result[char] = ++result[char] || 1;
}
}
return result;
}
function isAlphaNumeric(char) {
let code = char.charCodeAt(0);
if (!(code > 47 && code < 58) && // numeric (0-9)
!(code > 64 && code < 91) && // upper alpha (A-Z)
!(code > 96 && code < 123)) { // lower alpha (a-z)
return false
}
return true
}
Conclusão
Encarar problemas de maneira metódica e planejar seu ataque, antes de iniciá-lo, pode fazer você enxergar novas possibilidades em suas soluções atuais, ou melhorar (e muito) as próximas. Lembre-se de que a programação é sobre fazer mais gastando menos recursos, isso significa parar para analisar cuidadosamente os caminhos disponíveis e, ainda assim, saber que existem opções que ainda não foram descobertas.
Top comments (0)