Complexidade de Algoritmos
A complexidade de algoritmos é uma medida teórica que estima o desempenho de um algoritmo em termos de tempo de execução e uso de memória, com base no tamanho da entrada (geralmente representado por n).
Tipos de Complexidade
- Complexidade de tempo: indica quantas operações um algoritmo realiza à medida que o tamanho da entrada cresce.
- Complexidade de espaço: indica a quantidade de memória que o algoritmo utiliza.
Essas medidas ajudam a prever o comportamento do algoritmo em cenários reais, mesmo antes da implementação.
Notação Big-O
A notação Big-O (O-grande) expressa o crescimento da complexidade de forma simplificada, ignorando constantes e termos de menor ordem. Alguns exemplos comuns:
- \( \mathcal{O}(1) \): tempo constante (independe do tamanho da entrada);
- \( \mathcal{O}(\log n) \): tempo logarítmico (buscas em árvores balanceadas, por exemplo);
- \( \mathcal{O}(n) \): tempo linear (ex.: percorrer um vetor);
- \( \mathcal{O}(n \log n) \): tempo linearitmico (eficiente para ordenações complexas);
- \( \mathcal{O}(n^2) \): tempo quadrático (cresce rapidamente com o tamanho da entrada);
- \( \mathcal{O}(2^n) \): tempo exponencial (ineficiente para entradas grandes).
Exemplos
\( \mathcal{O}(1) \) – Tempo constante:
int retorna_primeiro(int vetor[]) {
return vetor[0]; // Acesso direto, tempo constante
}
\( \mathcal{O}(\log n) \) – Tempo logarítmico (busca binária):
int busca_binaria(int vetor[], int n, int valor) {
int inicio = 0, fim = n - 1;
while (inicio <= fim) {
int meio = (inicio + fim) / 2;
if (vetor[meio] == valor) return meio;
else if (vetor[meio] < valor) inicio = meio + 1;
else fim = meio - 1;
}
return -1;
}
\( \mathcal{O}(n) \) – Tempo linear (varre todo o vetor):
int soma_elementos(int vetor[], int n) {
int soma = 0;
for (int i = 0; i < n; i++) {
soma += vetor[i];
}
return soma;
}
\( \mathcal{O}(n \log n) \) - Tempo linearitmico
void mergeSort(int arr[], int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
// Divisão recursiva do vetor
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
// Mescla das metades ordenadas
merge(arr, l, m, r);
}
}
\( \mathcal{O}(n^2) \) – Tempo quadrático (duplo for):
void imprime_pares(int vetor[], int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
printf("(%d, %d)\n", vetor[i], vetor[j]);
}
}
}
\( \mathcal{O}(2^n) \) – Tempo exponencial (ex: cálculo de Fibonacci sem otimização):
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
A um custo mínimo de memória pode-se alterar o algoritmo acima para tornar a mesma função de complexidade \( \mathcal{O}(n) \), veja:
int fibonacci(int n) {
if (n <= 1) return n;
int a = 0, b = 1, temp;
for (int i = 2; i <= n; i++) {
temp = a + b;
a = b;
b = temp;
}
return b;
}
Nota: este é o objetivo de um engenheiro, fazer com USD 1.00 o que outros fazem com USD 2.00! Mas neste caso específico, o objetivo é reduzir o custo de processamento, que não deixa de ser uma economia 😄!
Importância na Engenharia de Controle e Automação
A análise de complexidade é fundamental para:
- Escolher algoritmos eficientes em sistemas com tempo real ou recursos limitados;
- Prever o comportamento do software em controladores embarcados;
- Otimizar rotinas de leitura de sensores, processamento de sinais e controle de processos;
- Avaliar viabilidade de algoritmos em função do tempo de resposta exigido pelo sistema.
Compreender a complexidade permite ao engenheiro tomar decisões conscientes ao implementar soluções que equilibram desempenho, precisão e uso de recursos.