Executando verificação de segurança...
11

A grande utilidade do operador % (Módulo)

Na maioria das linguagens de programação, temos o operador módulo, que normalmente é representado pelo símbolo de "%". De início muitos tem estranhesa com esse operador, já que não é tão claro para que serve apenas pelo nome (diferente dos operadores de divisão, multiplicação, potenciação e etc.), eu mesmo não fazia ideia do porquê estavam me ensinando isso quando comecei a aprender a programar.

Normalmente o seu uso acaba se limitando a testar se um número é par, denotar o intervalo em que números aleatórios vão estar e etc. Mas a sua utilidade é muito maior do que isso, já adianto que, dentre as suas utilidades, uma das que se destaca é o fato de a operação conseguir gerar conjuntos enumeráveis de números naturais a partir de qualquer conjunto numérico enumerável ou não.

Mas afinal, o que seria o módulo?

Resto da divisão

De forma simples e rápida Módulo é o resto da divisão. Isso tem a ver com o algorítimo da divisão e é um concento bem simples.

Pense que você comprou uma pizza de 16 fatias e deseja distribuir as fatias igualmente entre 3 amigos. Naturalmente nós sabemos que 16 não tem uma divisão inteira por 3, mas 15 tem. Assim, você pode separar uma das fatias e terá agora 15 fatias para distribuir igualmente entre 3 pessoas, cada uma recebendo 5 fatias da pizza. Mas e a 16º fatia? Esse é o resto.

Na matemática temos uma propriedade muito importante chamada reversibilidade em algumas funções e operações, esse conceito é mais conhecido popularmente por nós como prova real, onde aplicando uma função ou operação inversa em um número que passou pela transformação por meio de uma função ou operação ela nos retorna o número original.

Mas que p*** é essa?

Pense que você executou a operação de somar um número n por 1: n + 1.
Tomemos n como sendo 2: 2 + 1 = 3. Pela propriedade da reversabilidade, ao fazermos a operação inversa (subtrair 1 de um número n) devemos ter o mesmo n de antes. Assim: 3 - 1 = 2. E isso é a propriedade da reversabilidade.

O grande ponto é que a divisão possui essa propriedade, onde podemos incorporar também o resto da divisão. É basicamente isso: Dividendo = Quociente * Divisor + Resto.
Lembrando:

  • Dividendo: número que será dividido
  • Divisor: número que divide
  • Quociente: resultado da divisão
  • Resto: resto da divisão

Em termos de números: 5 = 2*2 + 1

Desse conceito vêm alguns teoremas importantes na matemática como o teorema do resto no estudo de polinômios e a congruência de módulo m.

Por que módulo?

Dentro do campo da Álgebra estudamos a relação entre números, variáveis, funções e conjuntos. Uma das relações mais conhecidas é a de congruência, que pode aparecer de várias maneiras. Congruência de Módulo m é uma das primeiras que aprendemos em todo curso de matemática, que basicamente significa que, caso dois números sejam congruentes em relação a um módulo m eles tem o mesmo resto de divisão. Por exemplo:

3 = 5 módulo(2) [Leia "=" como "conguente a"].

Isso pois o resto da divisão de 3 e 5 por 2 é o mesmo (1). Daí vem também o nome módulo para essa operação, que calcula o resto da divisão de um número por outro. Isso em alguns casos pode ser bem importante, por exemplo: Qual é o último algarismo do número 7²³³? Esse problema nós resolvemos com congruência de módulo m, afinal, quem em sã consciência calcularia essa expressão?
É claro que na computação poderíamos fazer essa conta, mas perderiamos muita performance utilizando uma solução analógica ao invés de analítica.

Onde se aplica?

Aqui vem o motivo da qual eu escrevi esse post. Eu sou estudante de Matemática Aplicada Computacional na UFRGS, mas também estudo programação e pretendo iniciar uma carreira como desenvolvedor. O que acontece é que a base matemática que tenho faz com que eu possa resolver vários problemas de forma mais analítica, o que em alguns casos torna o meu programa mais legível, performático ou menos complexo. Essa é a grande importância da matemática para a programação que muitos deixam para traz.

Ontem, estudando sobre estruturas de dados fiz uma implementação de uma CircleLinkedList, que é uma lista ligada onde o último nó se liga com o primeiro, fazendo com que não existam referências nulas e seja possível fazer uma busca por absolutamente qualquer índice na lista.
Um exemplo de implementação em C:

Tipos utilizados:

typedef struct node {
    int value;
    struct node *next;
} Node;

typedef struct circleLinkedList {
    node *head;
    node *tail;
} CircleLinkedList;

Precisamos de duas funções: add e getNode:

void add(int value, CircleLinkedList* list) {
    Node *newNode = malloc(sizeof(Node));

  newNode->value = value;
  newNode->next = NULL;

  if(list->length == 0) {
    list->head = newNode;
    list->tail = newNode;
  } else {
    newNode->next = list->head;
    list->tail->next = newNode;
    list->tail = newNode;
  }

  list->length++;
}

Essa função apenas adiciona um novo nó no final da lista. Agora o principal: a função getNode. Como princípio básico da estrutura, temos que o último elemento aponta para o primeiro, implicando que podemos encontrar qualquer elemento da lista com um índice inteiro positivo, independente do número. Isso significa que não importa o tamanho da lista, qualquer índice positivo é válido.

Node getNode(int index, CircleLinkedList *list) {
  int i;
  Node *current = list->head;

  for(i = 0; i < index; i++)
    current = current->next;

  return *current;
}

Pronto! Temos a nossa função pronta e exercício resolvido! Óbvio que não né, olha para essa função, se eu colocar um índice 1 milhão, a função irá iterar 1 milhão de vezes mesmo que a lista tenha apenas dois elementos. Essa implementação faz com que a nossa função getNode precise sempre iterar o número total do índice, independentemente do tamanho da lista. Listas ligadas no geral possuem complexidade O(x) para buscas, o que significa, basicamente, que na pior das hipóteses, teremos que percorrer toda a nossa lista para encontrar um elemento. O que acontece é, que na forma atual da função getNode a nossa complexidade pode ser aumentada. Veja a seguinte situação:

int main() {
    CircleLinkedList mylist = {NULL, NULL, 0};
    Node result;
    
    add(0, &mylist);
    add(1, &mylist);
    add(2, &mylist);
    
    result = getNode(6, &mylist);
    printf("getNode(6) = %d\n", result.value); // getnode(6) = 0
}

Nesse caso a complexidade do nosso algoritmo foi O(x²), pois iteramos sobre a lista duas vezes. Isso significa que, a complexidade do algoritmo aumenta proporcionalmente com o quão menor é o tamanho da lista em relação ao índice. Isso é péssimo para o desempenho da nossa função, pois para índices muito grandes teremos uma perda desnecessária em listas pequenas.

Como resolvemos isso? Utilizando o operador módulo!. Como o módulo é o resto da divisão, ao dividirmos um número temos um número limitado de valores possíveis, no caso de 3 são apenas 0, 1 e 2, pode testar aí :). Vamos imaginar que a nossa lista é uma array, ela seria assim: {0, 1, 2} e se fizéssimos mylist[0] = 0, mylist[1] = 1 e mylist[2] = 2, caso fosse uma array índices maiores causariam erros. Ótimo! Já sabemos quais índices realmente importam e são exatamente os retornos possíveis de index % 3. De forma mais resumida, os índices que realmente importam são aqueles que vão de 0 até mylist.index, implementando isso:

Node getNode(int index, CircleLinkedList *list) {
  int i, finalIndex = index % list->length;
  Node *current = list->head;

  for(i = 0; i < finalIndex; i++)
    current = current->next;

  return *current;
}

Pronto! Agora se fizermos getNode(10000000000, &mylist) a função irá iterar no máximo 3 vezes, sem perda de performance significativa como anteriormente. O principal uso do operador módulo se dá quando queremos limitar um certo intervalo positivo de números, com ele podemos garantir que todos os valores estarão nesse intervalo.

Conclusão

Quando tiver um problema parecido lembre-se dessa poderosa ferramenta que praticamente todas as linguagens de programação têm e também que soluções analíticas são muito melhores e performáticas quando precisamos de escalabilidade, soluções analógicas, embora simples e equiparáveis em problemas pequenos, costumam causar problemas conforme as nossas necessidades aumentam.

Carregando publicação patrocinada...
4

Só pra complementar meu outro comentário, seguem outros usos do operador %.


Obter os últimos dígitos de um número

Você até encontra "soluções" por aí que sugerem transformar o número em string e depois pegar os últimos caracteres. Mas na verdade, para obter os últimos N dígitos, só precisa pegar o resto da divisão por 10N:

var n = 1898453;
var ultimoDigito = n % 10; // 3
var doisUltimosDigitos = n % 100; // 53

Daí a solução pro clássico exercício de somar os dígitos de um número:

var n = 1234;
var soma = 0;
while (n > 0) {
    soma += n % 10; // soma o último dígito
    n = Math.floor(n / 10); // divide por 10, assim o último dígito "cai fora"
}
console.log(soma); // 10

Geralmente a solução matemática é mais rápida e eficiente do que converter o número para string e depois pegar cada caractere e converter de volta para número.

Lembrando que para números negativos, nem sempre funciona, como já comentado aqui. No caso acima, bastaria usar Math.abs(n) antes de usar %, mas claro que cada caso é um caso.


O clássico problema do caixa eletrônico

Esse é um exercício clássico que muitos já devem ter feito. A ideia da solução é relativamente simples: se o valor é 357 eu primeiro divido pela maior nota (por exemplo, 100) para saber quantas eu preciso (no caso, 3). Depois eu pego o resto da divisão por 100 (olha o % aí!), e o resultado será o valor restante (57). E a partir daí eu continuo verificando a próxima nota, e repito isso até o valor zerar.

Algumas "soluções" fazem um loop e vão subtraindo o valor da nota e incrementando um contador, mas o uso de divisão e % simplifica esse processo.

Claro, a solução abaixo é o caso mais simples, em que não há limite na quantidade de notas. E como a menor nota é 1, sempre haverá uma combinação possível. Se quiser uma solução que leva em conta a quantidade de cada nota, além de verificar quando não é possível (por exemplo, valor é 75 mas só tem notas de 100 e 50) veja aqui.

var valor = 357;
var notas = [100, 50, 20, 10, 5, 1];
let quantidades = new Map(); // Map para guardar as quantidades de cada nota
for (const nota of notas) {
    if (valor >= nota) {
        const qtd = Math.floor(valor / nota);
        valor %= nota;
        if (qtd == 0)
            continue;
        quantidades.set(nota, qtd);
        if (valor == 0)
            break; // se o valor zerou, não preciso continuar
    }
}

for (const [nota, qtd] of quantidades)
    console.log(`- ${qtd} nota${qtd == 1 ? "" : "s"} de ${nota}`);

Saída:

- 3 notas de 100
- 1 nota de 50
- 1 nota de 5
- 2 notas de 1

Pegar uma duração total em segundos e converter para horas, minutos e segundos

A menos que a linguagem já ofereça alguma lib pronta, não é difícil fazer os cálculos manualmente, e o operador % pode te ajudar:

// 12123 segundos são quantas horas/minutos/segundos?
var totalSegundos = 12123;

var horas = Math.floor(totalSegundos / 3600);
var minutos = Math.floor(totalSegundos / 60) % 60;
var segundos = totalSegundos % 60;

console.log(`${horas} horas, ${minutos} minutos e ${segundos} segundos`); // 3 horas, 22 minutos e 3 segundos

Ação específica a cada X iterações

Suponha que você quer iterar mil vezes, mas só quer fazer uma ação específica a cada 27 itens:

for (var i = 1; i <= 1000; i++) {
    if (i % 27 == 0) {
        // ação específica feita a cada 27 itens
    }
    // restante das ações
}

Criptografia

Algoritmos criptográficos costumam fazer uso extenso desse operador. Entre alguns exemplos, temos o RSA, Diffie-Hellman, etc. De forma geral, a aritmética modular possui várias características fundamentais para os sistemas criptográficos modernos.

3

Um uso bem comum (similar ao seu exemplo) é de percorrer um array de maneira circular (ou, no caso mais geral, quando você precisa "voltar ao início" caso ultrapasse o final de uma sequência):

var i = 6;
var array = [ 0, 1, 2, 3, 4, 5, 6, 7 ];

// avança 4 posições
i = (i + 4) % array.length;
console.log(array[i]); // 2

No exemplo acima, a posição atual é 6. Eu preciso avançar 4, mas se eu só somar 4, vou cair na posição 10, que não existe no array. Então o % faz com que eu "volte ao início". É como se, ao chegar na última posição, eu continuasse a contagem a partir da primeira.

E tem vários outros usos similares, quando vc precisa voltar ao início da sequência, caso ultrapasse o fim da mesma (a Cifra de César é um exemplo clássico).


Vale lembrar ainda que o operador % não se comporta da mesma maneira em todas as linguagens, quando há números negativos envolvidos. Por exemplo, -4 % 26 pode dar 22 ou -4, dependendo da linguagem (ou seja, se usarmos o mesmo algoritmo acima, mas para voltar posições, pode ou não funcionar). Mas aí já é outra história...

2

Aprendi a usar o operador Módulo quando implementei o algoritmo de Raycasting e foi uma explosão na minha cabeço do quão massa a matemática é quando aplicada de forma concreta, em casos que são legais de ver o resultado. É uma engenharia muito legal.

gabrielTapes muito obrigado por trazer este tipo de conteúdo para o TabNews 🤝

2
2
1
1

Que top mn. Mês que vem vou começar a estudar Ciência da Computação na UNIFAP e um dos primeiros assuntos é estruturas de dados. Tô ansioso pra implementar essas ideias

Obrigado

1

A primeira vez que vislumbrei a utilidade do módulo na programação foi realizando um dos primeiros exercícios do curso CS50 (esse), onde era preciso criar um código em C para verificar a autenticidade de números de cartões de crédito. Nesse caso, serviu para obter dígitos específicos na sequência númerica do cartão, a grosso modo determinando o caractere no array com base em sua posição, obtida com o módulo do número (lembrando um pouco o que o @kht trouxe). Um exercício bem simples, mas que abriu minha cabeça para algumas possibilidades de código e do uso da matemática tanto para realizar tarefas simples (lembrando que sou bem iniciante).
Sempre me enquadrei no estereótipo da "pessoa ruim em matemática", mas desde que comecei a estudar programação ela tem me facinado cada vez mais. Acho incrível o poder que ela "libera" (como disse o amigo @obrunoanastacio), principalmente quando aplicada à performance de código.
Obrigado por compartilhar seus conhecimentos!