Como os Algoritmos Funcionam
Chapter 4 Searching Algorithms

Capítulo 4: Algoritmos de Pesquisa

A pesquisa é uma operação fundamental na ciência da computação que envolve encontrar um item ou conjunto de itens específicos dentro de uma coleção de dados. Algoritmos e estruturas de dados de pesquisa eficientes são cruciais para muitas aplicações, desde bancos de dados e sistemas de arquivos até recuperação de informações e geometria computacional. Neste capítulo, exploramos vários algoritmos e estruturas de dados de pesquisa importantes, incluindo árvores de pesquisa binária, árvores de pesquisa balanceadas e tabelas hash. Também discutimos várias aplicações da pesquisa em cenários do mundo real.

Tabelas de Símbolos e Estruturas de Dados

Uma tabela de símbolos é um tipo de dado abstrato que associa chaves a valores, fornecendo operações para inserir pares de chave-valor, pesquisar um valor dado sua chave e excluir pares de chave-valor. As tabelas de símbolos também são conhecidas como dicionários ou matrizes associativas em diferentes linguagens de programação. Elas são estruturas de dados fundamentais usadas em uma ampla gama de aplicações, como:

  • Compiladores, onde as tabelas de símbolos são usadas para armazenar informações sobre variáveis, funções e outros identificadores.
  • Bancos de dados, onde os índices são construídos usando tabelas de símbolos para permitir pesquisa e recuperação rápidas de registros.
  • Roteadores de rede, que usam tabelas de símbolos para armazenar informações de roteamento para encaminhamento eficiente de pacotes.

Existem várias estruturas de dados que podem ser usadas para implementar tabelas de símbolos, cada uma com seus próprios compromissos em termos de desempenho de pesquisa, inserção e exclusão. Nesta seção, nos concentramos em duas estruturas de dados importantes: árvores de pesquisa binária e tabelas hash.

Árvores de Pesquisa Binária (BSTs)

Uma árvore de pesquisa binária (BST) é uma estrutura de dados hierárquica que armazena pares de chave-valor de forma a permitir operações eficientes de pesquisa, inserção e exclusão. Cada nó em uma BST contém uma chave, um valor associado e referências para seus nós filhos esquerdo e direito. A chave em cada nó é maior que todas as chaves em seu subárvore esquerda e menor que todas as chaves em seu subárvore direita.Aqui está a tradução em português do arquivo Markdown, com os comentários traduzidos, mas o código não traduzido:

e. Esta propriedade, conhecida como o invariante BST, permite uma pesquisa eficiente, fazendo decisões binárias em cada nó.

Aqui está um exemplo de um BST simples:

      4
    /   \
   2     6
  / \   / \
 1   3 5   7

A pesquisa em um BST envolve comparar a chave-alvo com a chave no nó atual e pesquisar recursivamente na subárvore esquerda ou direita com base na comparação. Se a chave-alvo for encontrada, o valor associado é retornado. Se a chave-alvo não for encontrada após atingir uma referência nula, a pesquisa termina sem sucesso.

A inserção em um BST segue um processo semelhante à pesquisa. Comparamos a chave do novo nó com as chaves no BST e percorremos a árvore até encontrar uma referência nula, onde anexamos o novo nó como uma folha. A exclusão em um BST é um pouco mais complexa, pois requer o tratamento de três casos: excluir um nó folha, excluir um nó com um filho e excluir um nó com dois filhos.

O tempo de complexidade do caso médio para pesquisa, inserção e exclusão em um BST é O(log n), onde n é o número de nós na árvore. No entanto, no pior caso (por exemplo, quando o BST se degenera em uma lista encadeada), a complexidade de tempo se torna O(n). Para mitigar esse problema, podemos usar BSTs autobalanceados, como árvores AVL ou árvores rubro-negras, que mantêm uma estrutura de árvore quase balanceada e garantem um desempenho de pior caso O(log n) para todas as operações.

Tabelas Hash

Uma tabela hash é uma estrutura de dados que fornece pesquisa, inserção e exclusão médias rápidas, usando uma função hash para mapear chaves para índices em uma matriz, chamados de buckets. A função hash recebe uma chave como entrada e retorna um índice inteiro, que é usado para localizar o bucket correspondente na matriz. Idealmente, a função hash deve distribuir as chaves uniformemente entre os buckets para minimizar as colisões (ou seja, várias chaves mapeando para o mesmo bucket).

Quando ocorre uma colisão, existem duas abordagens principais para resolvê-la:

  1. Encadeamento separado: Cada bucket é implementado como umaAqui está a tradução em português do arquivo Markdown, com os comentários traduzidos, mas o código não traduzido:

Lista encadeada, e todos os pares chave-valor que se hash para o mesmo bucket são armazenados na lista encadeada desse bucket.

  1. Endereçamento aberto: Quando ocorre uma colisão, a tabela hash procura outros buckets em uma sequência predefinida até encontrar um bucket vazio. Técnicas comuns de sondagem incluem sondagem linear, sondagem quadrática e duplo hashing.

Aqui está um exemplo de uma tabela hash com encadeamento separado:

+---+    +-------+
| 0 |--->| (1,A) |
+---+    +-------+
| 1 |--->| (5,B) |---->| (9,C) |
+---+    +-------+     +-------+
| 2 |
+---+
| 3 |--->| (7,D) |
+---+    +-------+
| 4 |
+---+

Neste exemplo, as chaves 1, 5 e 9 todas se hash para o bucket 1, então elas são armazenadas em uma lista encadeada anexada a esse bucket. A chave 7 se hash para o bucket 3 e é o único par chave-valor nesse bucket.

O tempo de complexidade médio para pesquisa, inserção e exclusão em uma tabela hash bem projetada é O(1), tornando-a uma das estruturas de dados mais rápidas para essas operações. No entanto, o pior caso de complexidade de tempo pode se degradar para O(n) se a função hash for mal escolhida ou se houver muitas colisões. Para manter um bom desempenho, é essencial usar uma função hash de alta qualidade e redimensionar a tabela hash quando o fator de carga (ou seja, a razão entre o número de pares chave-valor e o número de buckets) exceder um determinado limite, tipicamente em torno de 0,75.

Árvores de Busca Balanceadas

Embora as árvores de busca binárias forneçam operações de pesquisa, inserção e exclusão eficientes em média, seu desempenho pode se degradar significativamente no pior caso. As árvores de busca balanceadas são uma família de estruturas de dados que mantêm uma estrutura de árvore quase balanceada, garantindo um bom desempenho no pior caso. Nesta seção, discutiremos duas árvores de busca balanceadas populares: as árvores AVL e as árvores rubro-negras.

Árvores AVL

Uma árvore AVL, nomeada após seus inventores Adelson-Velsky e Landis, é uma árvore de busca binária auto-balanceada na qual as alturas das subárvores esquerda e direita de qualquer nó diferem no máximo em um. Essa diferença de alturaAqui está a tradução em português do arquivo Markdown, com os comentários traduzidos, mas o código não traduzido:

A diferença entre a altura do filho esquerdo e a altura do filho direito de um nó é chamada de fator de equilíbrio. Quando uma operação de inserção ou exclusão viola a propriedade AVL (ou seja, o fator de equilíbrio se torna maior que 1 ou menor que -1), a árvore é rebalanceada por meio de uma ou mais rotações.

Existem quatro tipos de rotações usadas para rebalancear as árvores AVL:

  1. Rotação à esquerda: Realizada quando o fator de equilíbrio de um nó é maior que 1 e o fator de equilíbrio do seu filho direito é não negativo.

  2. Rotação à direita: Realizada quando o fator de equilíbrio de um nó é menor que -1 e o fator de equilíbrio do seu filho esquerdo é não positivo.

  3. Rotação esquerda-direita: Realizada quando o fator de equilíbrio de um nó é maior que 1 e o fator de equilíbrio do seu filho direito é negativo.

  4. Rotação direita-esquerda: Realizada quando o fator de equilíbrio de um nó é menor que -1 e o fator de equilíbrio do seu filho esquerdo é positivo.

Ao manter a propriedade AVL, as árvores AVL garantem uma complexidade de tempo no pior caso de O(log n) para as operações de busca, inserção e exclusão. No entanto, os fatores constantes envolvidos nas operações de árvore AVL são um pouco mais altos em comparação com as árvores binárias de busca comuns, devido à sobrecarga de manter os fatores de equilíbrio e realizar as rotações.

Árvores Rubro-Negras

Uma árvore rubro-negra é outra árvore binária de busca auto-balanceada que mantém uma estrutura quase balanceada. Cada nó em uma árvore rubro-negra é colorido de vermelho ou preto, e a árvore satisfaz as seguintes propriedades:

  1. O nó raiz é sempre preto.
  2. Todos os nós folha (NIL) são pretos.
  3. Se um nó é vermelho, ambos os seus filhos são pretos.
  4. Cada caminho de um nó a qualquer um de seus nós folha descendentes contém o mesmo número de nós pretos.

Essas propriedades garantem que o caminho mais longo da raiz a qualquer folha seja no máximo o dobro do comprimento do caminho mais curto, garantindo uma complexidade de tempo no pior caso de O(log n) para as operações de busca, inserção e exclusão.

Quando uma operação de inserção ou exclusão viola qualquer uma das propriedades da árvore rubro-negra, a árvore é rebalanceada por meio de uma série de inversões de cor e rotações.Aqui está a tradução em português deste arquivo markdown. Para o código, não traduzi o código, apenas os comentários.

Aplicações da Pesquisa

Algoritmos de pesquisa e estruturas de dados têm inúmeras aplicações em vários domínios. Nesta seção, discutiremos alguns exemplos para ilustrar a importância e a versatilidade da pesquisa em cenários do mundo real.

Bancos de Dados e Recuperação de Informações

Bancos de dados e sistemas de recuperação de informações dependem muito de técnicas de pesquisa eficientes para fornecer acesso rápido aos dados. Em bancos de dados relacionais, índices são construídos usando estruturas de dados como árvores B ou tabelas hash para permitir buscas rápidas de registros com base em atributos específicos. Esses índices permitem que os bancos de dados executem eficientemente consultas com condições em atributos indexados, reduzindo muito o espaço de busca e melhorando o desempenho das consultas.

Em sistemas de recuperação de informações, como mecanismos de busca na web, índices invertidos são usados para mapear termos para os documentos que os contêm. Um índice invertido é essencialmente uma tabela de símbolos onde as chaves são termos e os valores são listas de identificadores de documentos. Quando um usuário envia uma consulta, o mecanismo de busca procura os termos da consulta no índice invertido e recupera as listas de documentos correspondentes, que são então combinadas e classificadas para produzir os resultados finais da pesquisa.

Projeto de Compiladores

Compiladores usam tabelas de símbolos extensivamente para acompanhar identificadores (por exemplo, nomes de variáveis, nomes de funções) e seus atributos (por exemplo, tipo de dados, escopo) durante o processo de compilação. Quando o compilador encontra um identificador no código-fonte, ele pesquisa na tabela de símbolos para determinar seu significado e propriedades. A pesquisa eficiente é crucial para o desempenho do compilador, pois um compilador típico pode precisar processar milhões de identificadores em um### Bioinformática e Biologia Computacional

Na bioinformática e biologia computacional, os algoritmos de busca desempenham um papel vital na análise e compreensão de dados biológicos. Alguns exemplos incluem:

  • Alinhamento de sequências: Algoritmos como BLAST (Basic Local Alignment Search Tool) e Smith-Waterman são usados para procurar semelhanças entre sequências de DNA, RNA ou proteínas. Esses algoritmos empregam várias técnicas de busca para encontrar eficientemente as melhores correspondências entre as sequências, ajudando os pesquisadores a identificar relações evolutivas, semelhanças funcionais e possíveis mutações.

  • Montagem de genomas: Algoritmos de busca são usados para localizar sobreposições entre pequenos fragmentos de DNA (leituras) gerados por máquinas de sequenciamento, permitindo a reconstrução da sequência genômica original. Uma busca eficiente é essencial para lidar com as enormes quantidades de dados gerados em projetos de sequenciamento modernos.

  • Identificação de genes e motivos: Os pesquisadores usam algoritmos de busca para localizar padrões ou motivos específicos dentro de sequências de DNA ou proteínas, como sítios de ligação de fatores de transcrição, sítios de splicing ou domínios conservados. Esses padrões podem fornecer insights sobre a regulação gênica, a função de proteínas e a conservação evolutiva.

Cibersegurança e Criptografia

Os algoritmos de busca são empregados em vários aspectos da cibersegurança e criptografia, incluindo:

  • Detecção de intrusão: Sistemas de detecção de intrusão em redes muitas vezes usam algoritmos de busca como Aho-Corasick ou Boyer-Moore para corresponder eficientemente os padrões de tráfego de rede a um banco de dados de assinaturas de ataques conhecidos. Isso permite a detecção e prevenção em tempo real de ameaças de segurança.

  • Quebra de senhas: Os atacantes podem usar algoritmos de busca para procurar eficientemente em grandes dicionários de senhas ou gerar possíveis combinações de senhas ao tentar quebrar hashes de senhas. Técnicas como tabelas arco-íris e trocas de tempo-memória dependem de uma busca eficiente para acelerar o processo de recuperação de senhas.Aqui está a tradução em português do arquivo markdown, com os comentários do código traduzidos:

  • Criptoanálise: Na criptografia, algoritmos de busca são usados para analisar e potencialmente explorar fraquezas em sistemas criptográficos. Por exemplo, algoritmos como baby-step giant-step ou rho de Pollard são usados para resolver o problema do logaritmo discreto, que está na base da segurança de determinados esquemas criptográficos.

Conclusão

Algoritmos de busca e estruturas de dados são ferramentas fundamentais na ciência da computação, com aplicações em uma ampla gama de domínios. Desde bancos de dados e recuperação de informações até computação científica, bioinformática e cibersegurança, a capacidade de pesquisar e localizar dados de forma eficiente é crucial para resolver problemas complexos e extrair insights valiosos.

Ao compreender os princípios e técnicas por trás dos algoritmos de busca, como árvores de busca binária, árvores de busca balanceadas e tabelas hash, os desenvolvedores podem tomar decisões informadas ao projetar e implementar sistemas que dependem de capacidades de busca eficientes. A escolha do algoritmo de busca e da estrutura de dados apropriados depende de fatores como o tamanho e a natureza dos dados, a frequência das operações de busca e os requisitos específicos da aplicação.

À medida que a quantidade de dados gerados e processados continua a crescer exponencialmente, a importância de algoritmos de busca eficientes só aumentará. Pesquisadores e profissionais de várias áreas continuarão a confiar nessas ferramentas fundamentais para enfrentar os desafios impostos pelos big data e desbloquear novas oportunidades de descoberta e inovação.