DEV Community

Matheus Sena
Matheus Sena

Posted on

Entendendo Big O: Como Avaliar a Eficiência de um Algoritmo

Image description

Como podemos medir o quanto complexo é um algoritmo?
Vamos explorar o que é o Big O, uma notação usada para descrever a eficiência dos algoritmos, com exemplos do mundo real e implementações em Java para facilitar o entendimento.

O Que é Big O? 🤔

Big O é uma notação matemática usada para descrever a eficiência de um algoritmo, particularmente em termos de tempo de execução (tempo necessário para executar o algoritmo) ou uso de memória (quantidade de memória necessária).

Ou seja, à medida que o tamanho da entrada (n) aumenta, o Big O nos ajuda a entender como o algoritmo se comporta.

Exemplo do Mundo Real 🛒

Vamos usar uma analogia simples do mundo real: você está em uma livraria com uma lista de livros. Imagine que você deseja encontrar um livro específico nessa lista. Existem duas maneiras principais de fazer isso:

  1. Busca Sequencial: Você começa no topo da lista e verifica cada livro na estante até encontrar o que está procurando.
  2. Lista Ordenada: Imagine que a lista está ordenada alfabeticamente. Você pode usar uma busca mais eficiente para encontrar o item.

Aplicando o Conceito de Big O 🧠

  1. Busca Sequencial (O(n)): Este método verifica cada item um por um. Se houver n itens na lista, no pior caso, você precisará verificar todos os n itens. Portanto, o tempo de execução cresce linearmente com o tamanho da lista. Isso é representado como O(n).

  2. Busca Binária em Lista Ordenada (O(log n)): Se a lista estiver ordenada, você pode realizar uma busca binária, que divide a lista pela metade a cada passo. O tempo de execução cresce logaritmicamente, o que é muito mais rápido para listas grandes. Isso é representado como O(log n).

Exemplos em Java 🖥️

1. Busca Linear (O(n)) 🚶

Este é o exemplo de uma busca linear, onde verificamos cada elemento da lista até encontrar o que estamos procurando.

public class BuscaLinear {
    public static int buscar(int[] array, int valor) {
        for (int i = 0; i < array.length; i++) {
            if (array[i] == valor) {
                return i; // Retorna o índice do valor
            }
        }
        return -1; // Retorna -1 se não encontrar
    }

    public static void main(String[] args) {
        int[] numeros = {10, 20, 30, 40, 50};
        int indice = buscar(numeros, 30);
        System.out.println("Encontrado no índice: " + indice);
    }
}
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, o tempo de execução depende diretamente do tamanho da lista. Se a lista tiver 1000 elementos, pode ser necessário verificar todos eles no pior caso.

2. Busca Binária (O(log n)) 🚀

Se a lista estiver ordenada, podemos usar a busca binária para encontrar o elemento de forma mais eficiente.

import java.util.Arrays;

public class BuscaBinaria {
    public static int buscar(int[] array, int valor) {
        int inicio = 0;
        int fim = array.length - 1;

        while (inicio <= fim) {
            int meio = (inicio + fim) / 2;
            if (array[meio] == valor) {
                return meio;
            } else if (array[meio] < valor) {
                inicio = meio + 1;
            } else {
                fim = meio - 1;
            }
        }
        return -1; // Retorna -1 se não encontrar
    }

    public static void main(String[] args) {
        int[] numeros = {10, 20, 30, 40, 50};
        Arrays.sort(numeros); // Certifica-se de que o array está ordenado
        int indice = buscar(numeros, 30);
        System.out.println("Encontrado no índice: " + indice);
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui, a busca binária divide a lista pela metade em cada iteração, permitindo que você encontre o item desejado muito mais rapidamente do que na busca linear.

Tipos Comuns de Big O

1. O(1) - Tempo Constante

O tempo de execução é constante e não depende do tamanho da entrada.

Exemplo de Mundo Real: Acender uma luz em uma sala. O tamanho da sala não afeta o tempo que leva para acender a luz.

Exemplo em Java:

public class ExemploO1 {
    public static void main(String[] args) {
        int[] numeros = {10, 20, 30, 40, 50};
        int primeiroNumero = numeros[0]; // O(1)
        System.out.println("Primeiro número: " + primeiroNumero);
    }
}
Enter fullscreen mode Exit fullscreen mode

2. O(n) - Tempo Linear 🚶

O tempo de execução cresce linearmente com o tamanho da entrada.

Exemplo de Mundo Real: Percorrer um corredor reto em uma biblioteca. Quanto maior o corredor, mais tempo leva para atravessá-lo.

Exemplo em Java:

public class ExemploOn {
    public static void main(String[] args) {
        int[] numeros = {10, 20, 30, 40, 50};
        int soma = 0;
        for (int numero : numeros) { // O(n)
            soma += numero;
        }
        System.out.println("Soma: " + soma);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. O(log n) - Tempo Logarítmico 📉

O tempo de execução cresce logaritmicamente à medida que a entrada cresce.

Exemplo de Mundo Real: Procurar um nome em um diretório telefônico ordenado, onde a cada etapa você reduz pela metade o número de páginas a serem verificadas.

Exemplo em Java:

import java.util.Arrays;

public class ExemploOlogn {
    public static int buscaBinaria(int[] array, int valor) {
        int inicio = 0;
        int fim = array.length - 1;

        while (inicio <= fim) {
            int meio = (inicio + fim) / 2;
            if (array[meio] == valor) {
                return meio; // Valor encontrado
            } else if (array[meio] < valor) {
                inicio = meio + 1;
            } else {
                fim = meio - 1;
            }
        }
        return -1; // Valor não encontrado
    }

    public static void main(String[] args) {
        int[] numeros = {10, 20, 30, 40, 50};
        Arrays.sort(numeros); // Certifica-se de que o array está ordenado
        int indice = buscaBinaria(numeros, 30); // O(log n)
        System.out.println("Valor encontrado no índice: " + indice);
    }
}
Enter fullscreen mode Exit fullscreen mode

4. O(n^2) - Tempo Quadrático 🏢

O tempo de execução cresce quadraticamente com o tamanho da entrada.

Exemplo de Mundo Real: Uma competição onde cada pessoa deve apertar a mão de todas as outras. Com n pessoas, haverá n * n apertos de mão.

Exemplo em Java:

public class ExemploOn2 {
    public static void main(String[] args) {
        int[] numeros = {64, 25, 12, 22, 11};
        selectionSort(numeros); // O(n^2)
        System.out.println("Array ordenado: ");
        for (int numero : numeros) {
            System.out.print(numero + " ");
        }
    }

    public static void selectionSort(int[] array) {
        int n = array.length;

        for (int i = 0; i < n - 1; i++) {
            int indiceMin = i;
            for (int j = i + 1; j < n; j++) {
                if (array[j] < array[indiceMin]) {
                    indiceMin = j;
                }
            }
            int temp = array[indiceMin];
            array[indiceMin] = array[i];
            array[i] = temp;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

5. O(2^n) - Tempo Exponencial 🌋

O tempo de execução cresce exponencialmente com o tamanho da entrada.

Exemplo de Mundo Real: Tentar adivinhar uma senha de n caracteres onde cada caractere pode ser uma letra ou número. O número de combinações possíveis cresce exponencialmente.

Exemplo em Java:

public class ExemploO2n {
    public static void main(String[] args) {
        int n = 10;
        System.out.println("Fibonacci de " + n + " é " + fibonacci(n)); // O(2^n)
    }

    public static int fibonacci(int n) {
        if (n <= 1) {
            return n;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusão 🏁

Compreender a complexidade de tempo e a eficiência de um algoritmo é fundamental para otimizar o desempenho do seu código. O Big O fornece uma maneira de descrever como o tempo de execução ou o uso de memória de um algoritmo

Top comments (0)