DEV Community

Lucas dos Prazeres
Lucas dos Prazeres

Posted on • Updated on

Solucionando problemas em 5 etapas: Uma abordagem em programação.

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) // ???
Enter fullscreen mode Exit fullscreen mode

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) // {}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
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;
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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.

Links úteis

Javascript Algorithms and Data Structures Masterclass

Top comments (0)