MB Academy

Algoritmos

Aprenda o que são algoritmos, suas características e como criar um passo a passo para resolver problemas.

Agora que entendemos um pouco mais sobre as linguagens de programação, chegou a hora de explorarmos um dos conceitos mais fundamentais da computação: os algoritmos. A palavra “algoritmo” pode parecer complexa, mas ela representa algo que fazemos todos os dias de forma intuitiva e natural, seja ao cozinhar uma receita ou planejar uma tarefa. Em programação, algoritmos são o coração de qualquer aplicação, definindo as etapas lógicas para realizar uma tarefa de forma eficiente.

O Que é um Algoritmo?

Um algoritmo é uma sequência de passos bem definidos que levam à solução de um problema. Em programação, ele descreve as etapas para que o computador resolva algo específico, como ordenar uma lista ou calcular uma média. A importância dos algoritmos está no fato de que eles determinam não apenas o que o programa faz, mas também o quão eficientemente ele o faz.

Como Criar um Algoritmo: Passos e Organização Lógica

Criar um algoritmo eficiente é uma habilidade central na programação, e envolve transformar um problema em uma sequência organizada e lógica de etapas. Para desenvolver essa habilidade, é importante praticar a divisão de tarefas complexas em pequenas ações executáveis. Abaixo, exploramos cada um dos passos fundamentais para criar um algoritmo sólido:

  1. Entender o Problema Profundamente

    Antes de qualquer coisa, é essencial compreender a fundo o problema que estamos tentando resolver. Isso significa não apenas ler a descrição do problema, mas também fazer perguntas como: “Qual é o objetivo final?”, “Quais são as condições iniciais?”, “Quais são as possíveis exceções ou entradas incomuns?”. Um exemplo prático: ao criar um algoritmo para calcular o troco de uma compra, você precisa considerar quantas notas e moedas estão disponíveis, e se o cliente pagou com um valor exato ou não.

  2. Identificar as Entradas e Saídas do Problema

    Para que o algoritmo funcione de forma correta, é crucial saber quais informações ele vai precisar para iniciar (entradas) e o que ele deve retornar ao final (saídas). No exemplo do cálculo do troco, a entrada pode ser o valor da compra e o valor pago pelo cliente, enquanto a saída seria o valor do troco. Identificar essas variáveis ajuda a entender a estrutura básica do algoritmo.

  3. Dividir o Problema em Subproblemas

    Este passo consiste em quebrar o problema em partes menores e solucionáveis. Cada subproblema representa uma tarefa isolada que o algoritmo precisará realizar. Por exemplo, se você está criando um algoritmo para organizar nomes em ordem alfabética, um subproblema poderia ser comparar dois nomes para determinar a ordem deles, enquanto outro subproblema seria mover os elementos dentro de uma lista. Essa divisão torna a solução mais gerenciável e clara.

  4. Estabelecer a Sequência Lógica das Etapas

    Organizar as etapas em uma ordem lógica e sequencial é fundamental para que o algoritmo funcione de forma eficiente. A ordem das etapas deve ser determinada de maneira que cada ação seja executada no momento certo, com base no que foi concluído anteriormente. No caso do cálculo do troco, você primeiro verifica se o valor pago é maior que o valor da compra antes de calcular o troco.

  5. Testar e Ajustar o Algoritmo

    Depois de criar o algoritmo, teste-o com diferentes entradas para garantir que ele funcione conforme esperado. Isso inclui tentar casos especiais, como entradas inválidas ou extremos que o usuário pode fornecer. No exemplo do troco, você pode testar o algoritmo com valores altos e baixos para garantir que ele retorne corretamente o troco e que funcione com qualquer valor de entrada. Caso necessário, ajuste o algoritmo para lidar com essas variações.

  6. Refinar e Otimizar

    Uma vez que o algoritmo esteja funcionando, é hora de revisá-lo e procurar formas de torná-lo mais eficiente. Pergunte a si mesmo: "Este processo pode ser simplificado?", "Algum passo pode ser eliminado ou consolidado?", "Há maneiras de reduzir o número de operações?". Essa etapa de otimização é essencial para melhorar o desempenho do algoritmo, especialmente quando ele será aplicado em contextos de alta demanda.

Criar algoritmos pode parecer desafiador no início, mas com prática, esse processo se torna natural. Em seguida, veremos como podemos identificar as características que definem um bom algoritmo, ou seja, aqueles que são eficientes e robustos.

Características de um Bom Algoritmo

Nem todo algoritmo é criado da mesma forma. Alguns são mais rápidos, outros mais fáceis de entender e outros, ainda, são mais flexíveis. Para garantir que um algoritmo seja o mais eficiente e confiável possível, ele deve atender a certos critérios. Abaixo estão algumas das características que fazem um algoritmo ser considerado “bom”:

  1. Correção

    A correção é o requisito mais básico de qualquer algoritmo: ele deve resolver o problema para o qual foi criado. Isso significa que, independentemente dos valores das entradas (dentro do que é considerado válido), o algoritmo deve produzir a saída correta. Testar a correção envolve verificar se o algoritmo se comporta como esperado em uma ampla gama de casos, incluindo cenários extremos.

  2. Eficiência (Tempo e Espaço)

    A eficiência de um algoritmo é medida em duas dimensões principais: tempo de execução e uso de memória. Um algoritmo eficiente deve resolver o problema com o menor tempo possível e com o uso mínimo de recursos de memória. Esses fatores são especialmente críticos em sistemas que processam grandes volumes de dados ou operam em dispositivos com recursos limitados. No mundo da programação, esses fatores são conhecidos como complexidade temporal (tempo) e complexidade espacial (memória).

  3. Simplicidade e Clareza

    Algoritmos simples são mais fáceis de entender, manter e modificar. Manter um algoritmo simples também ajuda a evitar bugs, pois um código complexo é mais suscetível a erros. Um bom algoritmo deve ser escrito de maneira que outros programadores (ou o próprio autor, no futuro) possam entender sua lógica sem precisar gastar muito tempo. Em muitos casos, a simplicidade é preferida até mesmo à eficiência, especialmente quando o desempenho não é um fator crítico.

  4. Flexibilidade e Adaptabilidade

    Algoritmos flexíveis podem ser facilmente ajustados para resolver problemas similares. Esse atributo é útil em situações onde o algoritmo precisa lidar com diferentes tipos de entradas ou ser reutilizado em outros contextos. A flexibilidade torna o algoritmo mais reutilizável e extensível, facilitando seu uso em projetos diferentes sem precisar de muitas modificações.

  5. Robustez

    Robustez se refere à capacidade do algoritmo de lidar com entradas inválidas ou inesperadas sem falhar. Um algoritmo robusto pode, por exemplo, verificar se os valores de entrada estão dentro de um intervalo esperado e retornar mensagens de erro apropriadas ou valores padrão caso contrário. Isso é especialmente importante em programas de uso geral, onde o usuário pode inserir dados de forma incorreta ou incompleta.

  6. Determinismo

    Algoritmos determinísticos sempre produzem a mesma saída para a mesma entrada, sem variações. Isso é importante para garantir previsibilidade e repetibilidade dos resultados. Em contraste, algoritmos não determinísticos podem dar diferentes saídas para a mesma entrada, o que nem sempre é desejado, especialmente em aplicações onde consistência e precisão são fundamentais.

  7. Escalabilidade

    A escalabilidade é a capacidade de um algoritmo de lidar com o aumento de dados de forma eficiente. Um algoritmo escalável continua funcionando bem, mesmo quando aplicado a grandes conjuntos de dados. Por exemplo, um algoritmo que funciona bem em listas pequenas, mas começa a ficar muito lento em listas grandes, pode não ser escalável. Em cenários como análise de dados ou grandes sistemas, a escalabilidade é essencial para o bom desempenho do software.

Essas características ajudam a definir a qualidade e a confiabilidade de um algoritmo. Desenvolver algoritmos que equilibrem essas qualidades requer prática e uma análise cuidadosa das necessidades do problema. À medida que aprofundamos nosso conhecimento em programação, vamos explorando maneiras de refinar algoritmos para torná-los cada vez mais adequados e eficientes.

Exemplo Prático em TypeScript

Para entender como esses conceitos se aplicam na prática, vamos criar um algoritmo em TypeScript para calcular o troco em uma compra. O objetivo é mostrar o troco da forma mais otimizada possível, utilizando o menor número de notas e moedas.

Passo 1: Entender o Problema

Nosso objetivo é receber o valor da compra e o valor pago. Se o pagamento for suficiente, o algoritmo deve calcular o troco e retornar a quantidade de cada nota e moeda necessária.

Passo 2: Identificar as Entradas e Saídas

  • Entradas: valorCompra: number, valorPago: number.
  • Saídas: Um objeto representando o troco, ou uma mensagem de erro.

Passo 3: Dividir o Problema em Subproblemas

  1. Verificar se o valor pago é suficiente.
  2. Calcular o troco total.
  3. Distribuir o troco entre as notas e moedas disponíveis, priorizando as de maior valor.

Passo 4: Implementação em TypeScript

function calcularTroco(
  valorCompra: number,
  valorPago: number
): object | string {
  // Verifica se o valor pago é suficiente
  if (valorPago < valorCompra) {
    return "Valor pago é insuficiente.";
  }

  // Calcula o troco total
  let troco: number = valorPago - valorCompra;

  // Define as notas e moedas disponíveis
  const notasMoedas: number[] = [
    100, 50, 20, 10, 5, 2, 1, 0.5, 0.25, 0.1, 0.05, 0.01,
  ];
  const resultado: { [key: string]: number } = {};

  // Percorre as notas e moedas para calcular o troco em cada unidade
  for (const valor of notasMoedas) {
    if (troco >= valor) {
      const quantidade = Math.floor(troco / valor);
      resultado[valor.toString()] = quantidade;
      troco = parseFloat((troco % valor).toFixed(2));
    }
  }

  return resultado;
}

// Exemplo de uso:
console.log(calcularTroco(82.75, 100));
// Saída esperada: { '10': 1, '5': 1, '2': 1, '0.25': 1 }

Não se preocupe com a sintaxe do TypeScript neste momento; o foco aqui é entender a lógica por trás do algoritmo. Vamos analisar o que cada parte do código faz.

Explicação do Código

  1. Verificação do valor pago: A função começa verificando se o valor pago é suficiente para cobrir o valor da compra. Se não for, a função retorna uma mensagem de erro indicando que o pagamento é insuficiente.
  2. Cálculo do troco total: Se o pagamento é válido, o próximo passo é calcular a diferença entre o valor pago e o valor da compra, armazenando esse valor na variável troco.
  3. Estrutura de notas e moedas: Definimos um array notasMoedas que contém os valores de cada nota e moeda disponíveis, do maior para o menor. Esse array permite que o algoritmo comece tentando as notas e moedas de maior valor, minimizando o número total de unidades usadas para devolver o troco.
  4. Distribuição do troco: Usamos o loop for...of para percorrer cada valor em notasMoedas. Para cada valor, verificamos quantas unidades dessa nota ou moeda cabem no valor de troco atual. Armazenamos o número de unidades no objeto resultado, que ao final da execução conterá a quantidade de cada nota e moeda necessária.
  5. Atualização do Troco Restante: Após calcular a quantidade de cada nota ou moeda, o valor de troco é atualizado usando o operador % para obter o valor que ainda falta distribuir. Esse processo é repetido até que o troco restante seja zero.

Resultado e Teste

Se chamarmos a função com calcularTroco(82.75, 100), o algoritmo retornará um objeto que representa o troco de R$ 17,25 da forma mais otimizada:

{
  10: 1, // uma nota de 10
  5: 1,  // uma nota de 5
  2: 1,  // uma nota de 2
  0.25: 1 // uma moeda de 0.25
}

Esse resultado indica que o troco total de 17.25 será devolvido com 1 nota de 10, 1 nota de 5, 1 nota de 2 e 1 moeda de 0.25.

Esse exemplo mostra como a criação de um algoritmo eficiente segue uma estrutura organizada e lógica. Ao seguir esses passos, garantimos que o algoritmo resolva o problema de maneira precisa e clara.

Um detalhe importante sobre números decimais

A computação com números decimais (ponto flutuante) pode gerar pequenas imprecisões. Uma abordagem mais robusta para lidar com dinheiro é trabalhar apenas com números inteiros (centavos).

A Importância dos Algoritmos

Dominar a criação de algoritmos é uma das habilidades mais importantes para qualquer programador. Com o tempo, você aprenderá a desenvolver e otimizar algoritmos para abordar problemas complexos com confiança, transformando ideias em código funcional e eficiente.