CORAZZA.DEV

Estação #02

Capa de Big O, Hash Tables e Árvores Binárias

Big O, Hash Tables e Árvores Binárias

Marcos Corazza | 28 junho 2026
#ciencia-de-dados#algebra#fundamentos

Na Estação 2 da nossa jornada, mergulhamos nas engrenagens da Ciência da Computação que viabilizam o processamento de Big Data. Exploramos a notação Big O para entender como avaliar a eficiência e a escalabilidade dos algoritmos. Em seguida, dissecamos as Funções Hash, o segredo por trás de buscas instantâneas, e as Árvores Binárias, a estrutura hierárquica que fundamenta tanto bancos de dados modernos quanto poderosos modelos de Machine Learning. A base da performance revelada.

RESUMO
Palavras:~2.130
Leitura:10 min
Linha01 Fundamentos
Progresso:11.1% [■---------]

Se a Álgebra Linear, vista na nossa primeira estação, fornece a teoria matemática para entender os dados, a Ciência da Computação nos dá as ferramentas práticas para processá-los. Em Data Science, não basta que um modelo matemático esteja correto; ele precisa ser computacionalmente viável.

Treinar um modelo de Inteligência Artificial ou cruzar dados de milhões de clientes pode demorar frações de segundo ou milhares de anos, dependendo exclusivamente de como os dados estão organizados e de como o algoritmo os percorre. É exatamente nesse ponto de intersecção entre a viabilidade e o tempo que entram a análise de complexidade (Notação Big O) e as estruturas de dados fundamentais: Funções Hash e Árvores Binárias.

Neste capítulo denso, vamos afastar a abstração do código de alto nível e entender como o computador armazena, busca e recupera informações nos bastidores. Dominar esses conceitos é o que separa um analista que escreve códigos que travam o servidor de um verdadeiro Cientista de Dados capaz de otimizar pipelines para processar Terabytes de informação.

1. Notação Big O: A Métrica da Escalabilidade

A Notação Big O (pronuncia-se "Big Ou" ou "Grande O") é a linguagem matemática utilizada na Ciência da Computação para descrever a complexidade de tempo (quanto tempo um algoritmo leva para rodar) e a complexidade de espaço (quanta memória ele consome) à medida que o volume de dados de entrada aumenta em direção ao infinito.

Em Data Science, raramente nos importamos se um algoritmo leva 10 milissegundos ou 20 milissegundos para processar 100 linhas de dados. Nosso hardware moderno mascara ineficiências em pequenas escalas. O que a notação Big O responde é: "O que acontece com o tempo de execução do meu código quando minha base de dados salta de 1.000 linhas para 1 bilhão de linhas?"

O "O" representa a ordem de grandeza da função, e o "n" representa o tamanho dos dados de entrada. Vamos explorar as complexidades mais comuns do melhor para o pior cenário.

O(1) - Tempo Constante

É o Santo Graal da performance. Um algoritmo O(1) executará no mesmo tempo, independentemente de haver dez itens ou dez bilhões de itens na base de dados.

  • Exemplo Prático: Acessar um elemento específico em uma lista pelo seu índice (ex: minha_lista[45]). O computador sabe exatamente o endereço de memória e vai direto lá. O tempo não escala com o tamanho da lista.

O(log n) - Tempo Logarítmico

Algoritmos logarítmicos são incrivelmente eficientes para grandes volumes de dados. Eles geralmente operam dividindo o problema pela metade a cada passo.

  • Exemplo Prático (A Lista Telefônica): Imagine procurar o nome "Silva" em uma lista telefônica de 1 milhão de páginas. Você não lê página por página. Você abre no meio (letra M). Sabe que "S" está na segunda metade. Você rasga a primeira metade e joga fora. Abre o restante no meio novamente (letra R). Rasga a primeira metade. A cada operação, você elimina 50% dos dados. Mesmo com bilhões de registros, você encontra a resposta em pouquíssimos passos. Esta é a essência da Busca Binária.

O(n) - Tempo Linear

O tempo de execução cresce de forma diretamente proporcional ao tamanho dos dados de entrada. Se você dobrar os dados, dobrará o tempo de execução.

  • Exemplo Prático: Procurar um valor específico em uma lista não ordenada. Você precisa usar um loop for e verificar o item 1, depois o item 2, depois o item 3... Se o item procurado for o último de uma lista de 1 bilhão, você fará 1 bilhão de verificações.

O(n^2) - Tempo Quadrático

Aqui os problemas de performance começam a inviabilizar projetos de dados. O tempo de execução é o quadrado do número de entradas. Geralmente ocorre quando temos loops aninhados (um loop for dentro de outro loop for).

  • Exemplo Prático: Comparar cada cliente de uma base de dados com todos os outros clientes da mesma base para encontrar perfis similares. Para 1.000 clientes, são 1.000.000 de operações. Para 100.000 clientes, são 10.000.000.000 (dez bilhões) de operações. Em Big Data, um algoritmo O(n^2) é quase sempre um atestado de falha.

Compreender o O(n) obriga o Cientista de Dados a pensar profilaticamente. Antes de acionar um .apply() no pandas ou um .map() no Spark, a primeira pergunta deve ser: qual é a complexidade dessa operação?

2. Funções Hash e Tabelas Hash: A Magia do O(1)

Agora que sabemos que buscar dados um por um (Tempo Linear, O(n)) é ineficiente para grandes volumes, como podemos encontrar uma informação específica instantaneamente? A resposta reside nas Funções Hash.

O que é uma Função Hash?

Uma função de hash é um algoritmo matemático que recebe uma entrada de dados de qualquer tamanho (uma palavra, um número, um documento de texto inteiro ou uma imagem) e retorna uma string de tamanho fixo, geralmente uma sequência alfanumérica incompreensível. Esse valor de saída é chamado de hash ou digest.

Propriedades fundamentais de uma boa função hash:

  1. Determinística: A mesma entrada sempre gerará exatamente o mesmo hash de saída. Se você passar a palavra "Data" pela função um milhão de vezes, o hash resultante será idêntico todas as vezes.

  2. Distribuição Uniforme: Entradas muito parecidas (como "Data" e "data") devem gerar hashes completamente diferentes.

  3. Velocidade: Deve ser computacionalmente rápida de calcular.

A Tabela Hash (Dicionários)

Na programação e na Ciência de Dados, o principal uso das funções hash não é a criptografia, mas a indexação rápida através de Tabelas Hash (conhecidas em Python como Dicionários ou dict, e as estruturas set).

Imagine que você precisa armazenar o salário de milhões de funcionários. O método ingênuo seria criar uma lista de tuplas [("Ana", 5000), ("Carlos", 6000)]. Para achar o salário do Carlos, você teria que varrer a lista (complexidade O(n)).

Como uma Tabela Hash resolve isso operando em O(1)?

  1. Você quer inserir "Carlos". O computador passa a palavra "Carlos" por uma função hash, que retorna um número inteiro, digamos, 847.

  2. O computador usa esse número 847 como o endereço exato (o índice) na memória do computador e guarda o salário 6000 lá.

  3. Amanhã, você pede ao computador: "Qual o salário do Carlos?". O sistema não varre nenhuma lista. Ele simplesmente recalcula o hash de "Carlos", que sempre será 847, vai direto ao endereço de memória 847 e recupera o valor. A busca é instantânea, não importa se você tem dez funcionários ou toda a população mundial no sistema.

O Problema das Colisões: Como a memória do computador é finita e as possibilidades de dados de entrada são infinitas, ocasionalmente a função hash gerará o mesmo endereço de memória para duas palavras diferentes (uma colisão). A Ciência da Computação resolve isso criando uma pequena lista (Linked List) naquele endereço de memória para guardar os dois valores. Uma boa função hash minimiza as colisões, mantendo a eficiência média próxima de O(1).

Em Data Science, sempre que você faz um "Join" entre duas tabelas no Pandas ou no SQL, os motores por trás dos panos frequentemente constroem Tabelas Hash temporárias para cruzar os dados instantaneamente em vez de comparar linha por linha de forma quadrática.

3. Árvores Binárias: A Natureza Hierárquica dos Dados

Enquanto as Tabelas Hash são perfeitas para buscar um valor exato instantaneamente, elas falham em um aspecto crucial: elas não mantêm a ordem dos dados. Se você quiser encontrar "todos os funcionários com salário maior que 5000", uma Tabela Hash não ajuda, pois os dados estão espalhados caoticamente na memória pelos algoritmos de hash.

Para buscas ordenadas e buscas de intervalos (range queries), introduzimos a estrutura de dados em formato de grafo direcionado: a Árvore Binária.

Anatomia de uma Árvore

Na Ciência da Computação, desenhamos árvores de cabeça para baixo.

  • Nó (Node): O bloco básico de construção. Contém o dado em si e os ponteiros para seus descendentes.

  • Raiz (Root): O nó superior, o ponto de entrada da árvore.

  • Filhos (Children): Os nós que ramificam a partir de um nó pai. Em uma árvore binária, cada nó pode ter no máximo dois filhos (o filho da esquerda e o filho da direita).

  • Folhas (Leaves): Os nós nas extremidades inferiores da árvore que não possuem filhos.

A Árvore Binária de Busca (BST - Binary Search Tree)

A verdadeira mágica acontece quando impomos uma regra rigorosa de organização na árvore:

Para qualquer nó, todos os valores na sua subárvore à esquerda devem ser menores que o valor do nó, e todos os valores na subárvore à direita devem ser maiores.

Essa estrutura é a materialização perfeita do algoritmo de busca logarítmica (O(log n)) da lista telefônica que discutimos antes.

Exemplo de Busca: Imagine uma árvore cuja raiz é o número 50. Você está procurando o número 75.

  1. Você olha para a raiz (50). Como 75 é maior que 50, você ignora todo o lado esquerdo da árvore (eliminando metade dos dados em um único passo) e vai para o nó filho à direita.

  2. O filho à direita é 70. Como 75 é maior, você vai novamente para a direita.

  3. Você encontra o nó 75.

Em uma árvore perfeitamente balanceada com 1 milhão de registros, você precisa de no máximo 20 passos para encontrar qualquer valor.

Impacto Direto em Data Science

Você pode nunca precisar programar uma Árvore Binária do zero em sua carreira como Cientista de Dados, mas você consumirá essa estrutura diariamente:

  1. Índices de Banco de Dados (B-Trees): Quando você cria um índice em uma coluna de um banco de dados relacional (como PostgreSQL ou MySQL) para deixar a consulta mais rápida, o banco de dados está criando uma B-Tree (uma versão generalizada e auto-balanceada de uma árvore de busca). É isso que permite que a cláusula WHERE id = 500 execute instantaneamente.

  2. Machine Learning (Árvores de Decisão): Modelos preditivos poderosos como Decision Trees, Random Forests e XGBoost são, na sua essência matemática e computacional, árvores binárias. No ML, em vez de o nó conter apenas um número, ele contém uma regra de divisão (ex: "Idade > 35?"). Os dados fluem da raiz até as folhas (onde estão as predições) seguindo os ponteiros da direita ou da esquerda dependendo de satisfazerem ou não as regras matemáticas de separação que minimizam a entropia.

4. Resumo e Próximos Passos

A compreensão de Big O, Hashes e Árvores não é apenas teoria acadêmica; é o que dita a engenharia por trás do processamento massivo de dados.

  • Use Dicionários (Hashes) quando precisar de acesso instantâneo O(1) a chaves exclusivas.

  • Confie em Estruturas de Árvores para ordenação, filtros de faixa (maior/menor) e para entender o comportamento das bibliotecas de Machine Learning.

  • E, acima de tudo, mantenha sempre a Notação Big O em mente antes de escrever um loop que interage com grandes matrizes de dados.

Com as estruturas de dados fundamentais dominadas, nossa próxima estação nos levará a um ambiente onde essas estruturas vivem e respiram: a Álgebra Relacional e os Bancos de Dados. Mas antes, vamos ver como esses conceitos se manifestam no código Python.

Mão na Massa: Implementação em Python

Nesta seção prática, vamos provar a eficiência de O(1) versus O(n) usando estruturas nativas do Python, demonstrar o uso do Hash e implementar de forma simplificada como um nó de uma Árvore Binária funciona por baixo dos panos.

import time # --------------------------- # DEMONSTRANDO O(n) VS O(1) # --------------------------- tamanho_dados = 10_000_000 elemento_procurado = 9_999_999 lista_dados = list(range(tamanho_dados)) set_dados = set(lista_dados) inicio = time.time() encontrado_lista = elemento_procurado in lista_dados fim = time.time() tempo_lista = fim - inicio print(f"Tempo Lista O(n): {tempo_lista:.6f} s") inicio = time.time() encontrado_set = elemento_procurado in set_dados fim = time.time() tempo_set = fim - inicio print(f"Tempo Set O(1): {tempo_set:.6f} s") # ------------------------ # FUNÇÕES HASH NO PYTHON # ------------------------ palavra1 = "DataScience" palavra2 = "datascience" print(f"Hash de '{palavra1}': {hash(palavra1)}") print(f"Hash de '{palavra2}': {hash(palavra2)}\n") # ---------------------------------------- # IMPLEMENTANDO UM NÓ DE ÁRVORE BINÁRIA # ---------------------------------------- class Node: def __init__(self, valor): self.valor = valor self.esq = None self.dir = None def inserir(self, novo_valor): if novo_valor < self.valor: if self.esq is None: self.esq = Node(novo_valor) else: self.esq.inserir(novo_valor) elif novo_valor > self.valor: if self.dir is None: self.dir = Node(novo_valor) else: self.dir.inserir(novo_valor) raiz = Node(50) raiz.inserir(30) raiz.inserir(70) raiz.inserir(20) raiz.inserir(40) print("Árvore construída com a Raiz: 50") print(f"Filho à esquerda: {raiz.esq.valor}") print(f"Filho à direita: {raiz.dir.valor}") print(f"Esquerda da esquerda): {raiz.esq.esq.valor}")