image

Access unlimited bootcamps and 650+ courses

50
%OFF
Article image
Herlon Costa
Herlon Costa09/03/2026 09:34
Share
Luizalabs - Back-end com Python - 2º EdiçãoRecommended for youLuizalabs - Back-end com Python - 2º Edição

Complexidade Algorítmica - Fundamentos importantes da Engenharia de Software

    A complexidade algorítmica é um dos conceitos mais importantes da ciência da computação e da engenharia de software. Ela descreve como o tempo de execução ou o consumo de memória de um algoritmo cresce à medida que o tamanho da entrada aumenta. Em sistemas modernos, especialmente aqueles que lidam com grandes volumes de dados, compreender a complexidade de um algoritmo pode ser a diferença entre uma aplicação eficiente e um sistema que se torna inviável em escala.

    Em JavaScript, especialmente no ambiente Node.js, esse conhecimento é fundamental. APIs, sistemas de processamento de dados, filas de mensagens e aplicações em tempo real frequentemente manipulam coleções grandes de dados. Quando um algoritmo é mal projetado, o impacto pode aparecer rapidamente em forma de latência elevada, alto consumo de CPU ou até mesmo indisponibilidade do serviço.

    Para analisar algoritmos, utilizamos a notação conhecida como Big O, que descreve o crescimento assintótico do tempo de execução conforme o tamanho da entrada aumenta.

    Complexidade constante: O(1)

    Um algoritmo possui complexidade constante quando o tempo de execução não depende do tamanho da entrada. Independentemente de trabalhar com 10 ou 10 milhões de elementos, a operação sempre executará em tempo constante.

    Um exemplo simples em Node.js envolve acesso direto a uma posição em um array.

    function getFirstUser(users) {
      return users[0];
    }
    
    const users = ["Ana", "Carlos", "Maria", "João"];
    
    console.log(getFirstUser(users));
    

    Nesse caso, acessar o primeiro elemento do array é uma operação direta. O tempo necessário para executar essa operação não aumenta com o crescimento do array.

    Outro exemplo comum em aplicações Node.js é o acesso a propriedades de objetos.

    function getUserEmail(user) {
      return user.email;
    }
    
    const user = {
      name: "Herlon",
      email: "herlon@email.com"
    };
    
    console.log(getUserEmail(user));
    

    Operações de acesso direto a propriedades possuem complexidade O(1), pois o motor JavaScript utiliza estruturas de hash para localizar rapidamente os valores.

    Complexidade linear: O(n)

    Um algoritmo possui complexidade linear quando o tempo de execução cresce proporcionalmente ao tamanho da entrada.

    Esse tipo de comportamento ocorre frequentemente quando precisamos percorrer uma lista inteira para encontrar um elemento específico.

    Considere um cenário comum em um backend Node.js: buscar um usuário dentro de um array.

    function findUser(users, email) {
      for (const user of users) {
        if (user.email === email) {
          return user;
        }
      }
    
      return null;
    }
    

    Agora podemos testar esse comportamento com um pequeno script em Node.js.

    const users = [];
    
    for (let i = 0; i < 1000000; i++) {
      users.push({
        id: i,
        email: `user${i}@email.com`
      });
    }
    
    console.time("search");
    
    const result = findUser(users, "user999999@email.com");
    
    console.timeEnd("search");
    
    console.log(result);
    

    Nesse exemplo, o algoritmo precisa percorrer potencialmente todos os elementos do array. Se o número de usuários dobrar, o tempo de execução tende a dobrar também. Esse é o comportamento característico da complexidade O(n).

    Complexidade quadrática: O(n²)

    Algoritmos quadráticos surgem quando temos loops aninhados, onde cada elemento da entrada precisa ser comparado com todos os outros.

    Um exemplo comum aparece ao tentar encontrar valores duplicados em uma lista.

    function findDuplicates(numbers) {
      const duplicates = [];
    
      for (let i = 0; i < numbers.length; i++) {
        for (let j = i + 1; j < numbers.length; j++) {
          if (numbers[i] === numbers[j]) {
            duplicates.push(numbers[i]);
          }
        }
      }
    
      return duplicates;
    }
    

    Se utilizarmos um conjunto grande de dados, o custo cresce rapidamente.

    const numbers = [];
    
    for (let i = 0; i < 10000; i++) {
      numbers.push(Math.floor(Math.random() * 1000));
    }
    
    console.time("duplicates");
    
    const result = findDuplicates(numbers);
    
    console.timeEnd("duplicates");
    
    console.log(result.length);
    

    Nesse caso, se o tamanho da entrada dobrar, o número de operações tende a quadruplicar. Em sistemas reais, algoritmos O(n²) podem rapidamente se tornar um gargalo.

    Otimizando algoritmos com estruturas de dados

    Um dos princípios fundamentais da engenharia de software é substituir algoritmos ineficientes por soluções com melhor complexidade.

    Vamos reescrever o algoritmo anterior utilizando um Set, estrutura disponível no JavaScript que oferece operações rápidas de busca.

    function findDuplicatesOptimized(numbers) {
      const seen = new Set();
      const duplicates = new Set();
    
      for (const number of numbers) {
        if (seen.has(number)) {
          duplicates.add(number);
        } else {
          seen.add(number);
        }
      }
    
      return [...duplicates];
    }
    

    Agora testamos novamente.

    console.time("optimized");
    
    const result = findDuplicatesOptimized(numbers);
    
    console.timeEnd("optimized");
    
    console.log(result.length);
    

    Esse algoritmo possui complexidade O(n), pois percorre a lista apenas uma vez.

    Essa diferença é extremamente relevante em sistemas reais. Em aplicações Node.js que processam milhões de registros, reduzir a complexidade de O(n²) para O(n) pode representar uma melhoria de desempenho de várias ordens de magnitude.

    Complexidade logarítmica: O(log n)

    Algoritmos logarítmicos aparecem frequentemente em estruturas como árvores balanceadas e em algoritmos de busca eficientes.

    Um exemplo clássico é a busca binária, que funciona apenas em listas ordenadas.

    function binarySearch(array, target) {
      let left = 0;
      let right = array.length - 1;
    
      while (left <= right) {
        const mid = Math.floor((left + right) / 2);
    
        if (array[mid] === target) {
          return mid;
        }
    
        if (array[mid] < target) {
          left = mid + 1;
        } else {
          right = mid - 1;
        }
      }
    
      return -1;
    }
    

    Vamos testar com um array grande.

    const data = [];
    
    for (let i = 0; i < 1000000; i++) {
      data.push(i);
    }
    
    console.time("binarySearch");
    
    const index = binarySearch(data, 900000);
    
    console.timeEnd("binarySearch");
    
    console.log(index);
    

    A busca binária reduz o espaço de busca pela metade a cada iteração. Por isso, mesmo com milhões de elementos, o número de operações permanece relativamente pequeno.

    Impacto real em aplicações Node.js

    Em aplicações backend, complexidade algorítmica impacta diretamente a escalabilidade. Um endpoint que executa um algoritmo O(n²) pode funcionar bem com poucos dados, mas tornar-se inviável quando o sistema cresce.

    Imagine um endpoint que verifica conflitos em um sistema de agendamento médico:

    function hasScheduleConflict(appointments, newAppointment) {
      for (const appointment of appointments) {
        if (
          appointment.date === newAppointment.date &&
          appointment.time === newAppointment.time
        ) {
          return true;
        }
      }
    
      return false;
    }
    

    Esse algoritmo possui complexidade O(n), o que é aceitável para muitos cenários. No entanto, se o sistema crescer e precisar comparar múltiplas agendas simultaneamente, pode ser necessário utilizar índices ou estruturas mais eficientes para evitar degradação de performance.

    Conclusão

    Complexidade algorítmica é uma ferramenta essencial para qualquer engenheiro de software. Ela permite prever como um sistema se comportará conforme o volume de dados cresce e orienta decisões arquiteturais importantes.

    No contexto do Node.js, onde muitas aplicações precisam lidar com grandes volumes de dados em tempo real, compreender o custo computacional de um algoritmo é fundamental para construir sistemas escaláveis e eficientes. Algoritmos com complexidade constante ou linear tendem a se comportar bem mesmo em grandes escalas, enquanto algoritmos quadráticos ou exponenciais podem rapidamente se tornar gargalos.

    Dominar esse conceito não significa apenas conhecer a notação Big O, mas desenvolver a capacidade de identificar padrões de complexidade no código e escolher estruturas de dados adequadas para cada problema. Essa habilidade é um dos principais diferenciais entre um programador iniciante e um engenheiro de software capaz de projetar sistemas robustos e de alto desempenho.

    Share
    Recommended for you
    Luizalabs - Back-end com Python - 2º Edição
    TOTVS - Fundamentos de Engenharia de Dados e Machine Learning
    Riachuelo - Cibersegurança
    Comments (0)
    Recommended for youLuizalabs - Back-end com Python - 2º Edição