Estruturas de Dados e Ordenação Baseado em: The Algorithm Design Manual Steven S. Skiena Introdução (1) A alteração de uma estrutura de dados em um programa lento pode mudar sensivelmente seu tempo de execução. Esta alteração não muda a corretude do programa. É importante projetar os programas tendo as estruturas de dados como centro. Introdução (2) É importante construir os programas de forma que implementações alternativas possam ser experimentadas. É importante separar os componentes da estrutura de dados de sua interface. Esta abstração de dados é importante para a limpeza, leitura e modificação dos programas. Observações (1) A construção de algoritmos em torno de estruturas de dados tais como dicionários e filas de prioridades leva a estruturas limpas e bons desempenhos. A escolha errada de uma estrutura de dados pode ser desastroso para o desempenho. A escolha da melhor estrutura de dados não é tão crítica, pois podem existir outras escolhas que se comportam similarmente. Observações (2) A ordenação é uma das partes principais de um algoritmo. Deveria ser a primeira coisa a ser feita para buscar eficiência. A ordenação pode ser usada para ilustrar muitos paradigmas de projeto de algoritmos. Técnicas de estruturas de dados, divisão e conquista, randomização e construção incremental levam a algoritmos de ordenação. Tipos de dados fundamentais Um tipo de dado abstrato é uma coleção de operações bem definidas que podem ser feitas em uma estrutura particular. São estas operações que definem o que a estrutura faz, mas não como ela funciona. Containers É uma estrutura de dados que permite o armazenamento e a recuperação de dados independentemente de seu conteúdo. As operações fundamentais são: Put(C,x) – insere o dado x no container C. Get(C) – recupera o próximo item do container C. Tipos diferentes de containers admitem diferentes tipos de recuperação, baseado na ordem ou posição de inserção. Containers (2) A utilidade de containers é maior quando a quantidade de dados é limitada e a ordem de recuperação é pré-definida ou irrelevante. Tipos de containers: Pilhas Filas Tabelas Pilhas LIFO. Simples de implementar. É o container correto quando a ordem de recuperação não importa. Operações: push (Put) e pop (Get). Filas FIFO. Parece ser a melhor maneira de controlar tempos de espera. São mais difíceis de implementar do que pilhas e são apropriadas apenas em aplicações em que a ordem é importante. Operações: enqueue (Put) e dequeue (Get). Tabelas Aceitam recuperação pela posição. As operações de Put e Get aceitam um índice como argumento. Tabelas são geralmente implementadas utilizando arrays. Implementação de containers Pode-se usar arrays ou listas ligadas. A escolha entre uma ou outra depende do conhecimento da quantidade de dados a ser armazenada. Com o uso de arrays, as operações Put e Get podem ser implementadas em tempo constante. Dicionários São uma forma de container que permite acesso aos dados armazenados pelo seu conteúdo. As principais operações são: Search(D,k) – Dada uma chave k, retorna um ponteiro para um elemento em D cujo valor é k, se existir. Insert(D,x) – Insere o dado de valor x em D. Delete(D,x) – Dado um ponteiro para um item de valor x, remove-o de D. Implementação Lista ligada desordenada Array ordenada Inserção e remoção em tempo constante. Consulta atravessa a lista toda. Consulta por busca binária em tempo O(log n). Inserção e remoção levam tempo linear. Árvore de Busca Binária Tabelas de Hash Ávores de Busca Binária Operações sobre dicionário são rápidas. É uma árvore com raiz na qual cada nó contém no máximo dois filhos. Os filhos recebem os rótulos right ou left, conforme sua posição em relação ao pai. Os nós são rotulados com as chaves de forma que todos os nós da subárvore à esquerda de um nó x tenham valores menores que x e todos à direita tenham valores maiores que x. Consulta BinaryTreeQuery(x,k) if (x=nil) or (k=key[x]) then return x if (k < key[x]) then return BinaryTreeQuery(left[x],k) else return BinaryTreeQuery(right[x],k) Tempo O(h), h é a altura da árvore. Inserção O item a ser inserido deve ser colocado na última posição em que pode ser encontrado. Esta posição é determinada ao fazer-se uma busca pelo valor a ser inserido. A inserção propriamente dita leva tempo constante após a determinação deste local (que leva tempo O(h)). Remoção É mais difícil que a inserção, pois o nó a ser removido pode não ser uma folha. A remoção de folhas é fácil, mas a simples remoção de um nó interno não permite acesso aos itens abaixo dele. Uma reestruturação ou rerotulação dos nós, de forma a levar o item a ser removido para uma folha, é necessário. Implementação (1) Todas as operações levam tempo O(h). A menor altura possível é O(log n), quando a árvore está perfeitamente balanceada. Ao inserirmos aleatoriamente os dados em uma árvore, a probabilidade da árvore resultante ter altura O(log n) é grande. Implementação (2) O pior caso ocorre quando a árvore é um caminho. Para evitar o desbalanceamento usa-se estruturas mais sofisticadas como, por exemplo, árvores rubro-negras. Filas de Prioridade (1) Utilizadas em aplicações onde os itens são processados em uma ordem particular. Por exemplo, o escalonamento de tarefas. As filas de prioridade apresentam vantagem sobre a ordenação pois as tarefas entram no sistema em intervalos arbitrários. É mais caro reordenar do que manter os dados em uma fila de prioridade. Filas de Prioridade (2) As principais operações são: Insert(Q,x) – inserir o item x na fila Q. Find_Minimum(Q) ou Find_Maximum(Q) – retorna um ponteiro para o menor ou maior valor em Q. Delete_Minimum(Q) ou Delete_Maximum(Q) – remove da lista Q o item com menor ou maior valor. Implementação (1) Qualquer uma delas leva tempo O(log n), representando os dados por uma árvore de busca binária. A busca do menor (maior) valor pode ser feita buscando-se o valor mais à esquerda (direita) na árvore. A inserção é feita como na árvore. A remoção consiste em uma busca seguida da remoção de um nó da árvore. Estruturas de Dados Especializadas (1) As estruturas vistas mostram uma representação para um conjunto desestruturado de dados de forma a facilitar operações de recuperação. Existem também estruturas de dados potentes utilizadas para representar tipos de objetos mais bem estruturados tais como pontos no espaço, cadeias e grafos. Estruturas de Dados Especializadas (2) Para estes dados: Existe um conjunto de operações básicas que devem ser feitas repetidamente. Procuramos estruturas de dados que executem estas operações eficientemente. Cadeias (Strings) Representadas geralmente por arrays de caracteres, com possivelmente um caracter especial para designar o fim da cadeia. Árvores de sufixos e Arrays de sufixos são estruturas de dados especiais para o pré-processamento de cadeias para tornar a busca de padrões rápida. Dados Geométricos Consiste geralmente de coleções de pontos e regiões. Regiões no plano são descritas por polígonos, cujas fronteiras são dadas por cadeias de segmentos de reta. Estruturas de dados espaciais, tais como kd-trees organizam os pontos e regiões pela localização geométrica de forma a tornar a busca rápida. Grafos Geralmente são representados por matrizes de adjacência ou por listas de adjacência. A escolha da representação é importante no algoritmo resultante. Conjuntos Subconjuntos de itens são geralmente representados usando dicionários, de forma a tornar as consultas de pertinência rápidas. Ordenação (1) A ordenação é a base na qual muitos algoritmos são construídos. Entendendo a ordenação, tem-se conhecimento para resolver outros problemas. Computadores gastam mais tempo ordenando do que fazendo qualquer outra coisa. A ordenação aparece em muitos problemas na prática. Ordenação (2) É o problema mais estudado em ciência da computação. Muitas das idéias usadas no projeto de algoritmos aparecem no contexto de ordenação, tais como divisão e conquista, estruturas de dados e algoritmos randomizados. Aplicações da Ordenação Busca Par mais próximo Unicidade de elementos Distribuição de freqüência Seleção Casco convexo Busca Uma busca binária em um dicionário com os dados ordenados leva tempo O(log n). Provavelmente esta seja a mais simples e importante aplicação da ordenação. Par mais próximo Dado um conjunto de n números, como encontrar o par de números cuja diferença entre eles seja a menor possível? Com os números ordenados a solução é simples. Solução em tempo O(n logn) . Unicidade de elemento Dado um conjunto de n elementos, existem elementos duplicados? Novamente, a melhor solução possível usa a ordenação. É um caso particular do problema do par mais próximo em que a diferença entre vizinhos é zero. Distribuição de freqüência Dado um conjunto de n elementos, qual elemento ocorre o maior número de vezes? Seleção Qual o k-ésimo maior elemento no conjunto? Com os dados ordenados, esta informação é determinada em tempo constante. Em particular, a mediana (o n/2-ésimo elemento). Casco convexo (1) Dado um conjunto de n pontos no plano cartesiano, qual o polígono convexo de menor área que contém todos estes pontos? Os pontos são ordenadas pelas abscissas dos pontos. A partir do ponto de menor abscissa, o casco é construído inserindo-se os pontos. Estruturas de Dados Ordenação por seleção. Implementação usando um vetor desordenado, leva: O(1) para reposicionar o elemento, e O(n) para localizar o elemento. Ordenação por seleção leva tempo O(n²). Se usarmos um heap ou árvore de busca balanceada o algoritmo leva tempo O(n log n), heapsort Inserção incremental Ordenação por inserção É um dos exemplos mais simples da técnica de inserção incremental. É uma técnica particularmente útil em algoritmos geométricos. Divisão e conquista Ordenação por intercalação Um problema grande pode ser dividido em problemas menores que são resolvidos. A solução de cada um deles é então combinada para obter-se a solução do problema inicial. Randomização Quicksort Os pivôs são escolhidos aleatoriamente. Com esta escolha, o algoritmo tem alta probabilidade de ser executado em tempo Q(n log n). No pior caso é quadrático. Técnica de Encestamento Segundo algum critério os dados são agrupados em cestos (buckets) e os cestos são tratados individualmente. Esta idéia esta presente em tabelas de hash, kd-trees, e em outras estruturas de dados práticas. A técnica é boa quando os dados ficam uniformemente distribuídos entre os cestos.