MO417 - Atas das Aulas

Assunto: Capítulo 6 - Heapsort


Ata da aula: 12/02/2003
Redator: André Santanchè
 

Tópicos:

  1. Heaps: heap mínimo, heap máximo;
  2. MAX-HEAPIFY;
  3. BUILD-MAX-HEAP;
  4. Heapsort e seu invariante;
  5. HEAP-MAXIMUM, HEAP-EXTRACT-MAX, HEAP-INCREASE-KEY, MAX-HEAP-INSERT

Heaps: heap mínimo, heap máximo

O Heap pode ser visto como uma árvore binária praticamente completa (semi-completa), cujo comportamento é caracterizado conforme um de seus dois tipos: Representação do heap em um vetor

Para representar o heap de modo linear em um vetor, utilizam-se as seguintes relações:

Dado um nó na posição i no vetor (posições começam em 1):
Pode-se observar que como as ligações entre pais e filhos são feitas através de relações de posição em um vetor, isto exige uma estrutura que permita um acesso direto a cada posição. Isto torna este tipo de organização de heap inadequada a uma lista encadeada, por exemplo.

MAX-HEAPIFY

Em primeiro lugar qual o objetivo da sub-rotina MAX-HEAPIFY?

Ela é responsável por manter a propriedade do heap, ou seja, dado qualquer nó, raiz de um heap, seu valor deve ser maior que o valor de seus filhos.

Para isto o MAX-HEAPIFY executa uma arrumação:

Inicialmente analisamos apenas o nível dos filhos imediatamente abaixo da raiz do heap, conforme o seguinte exemplo:
Note que a disposição dos valores nos nós não está atendendo a propriedade do heap, ou seja, o valor do pai (10) é inferior ao valor de ambos os filhos.

Neste caso o nó que possui maior valor (30) troca com o pai.

O procedimento pára neste nível?

Não. Para se garantir a propriedade do heap é necessário que ela seja válida para todos os níveis abaixo.

Quando ocorre uma troca, para ajuste do heap, apenas um dos filhos é afetado. No exemplo apresentado, apenas o lado do filho direito foi afetado. Deste modo, a sub-rotina MAX-HEAPIFY executa uma chamada recursiva a si mesma, para ordenar a sub-árvore (sub-heap) cuja raiz foi modificada. No exemplo apresentado será para a sub-árvore da direita.

É importante destacar algumas características do comportamento de MAX-HEAPIFY:

BUILD-MAX-HEAP

A sub-rotina BUILD-MAX-HEAP constrói um heap máximo de acordo com os seguintes passos: Qual a complexidade deste algoritmo?

Inicialmente se supõe que seja O(n lg n), dado que o MAX-HEAPIFY executa em tempo O(lg n) e o BUILD-MAX-HEAP executa aproximadamente n/2 iterações.

Uma análise mais detalhada demonstra que na árvore do heap existem no máximo teto(n/2h+1) nós raízes de sub-árvores de altura h. Como a alturas das sub-árvores na árvore principal variam entre 0 e piso(lg n) e o tempo que MAX-HEAPIFY processa um heap de altura h é O(h), o custo total para processar o heap completo será de:

Intuitivamente podemos entender este resultado percebendo que, na medida que nos deslocamos da raiz para as folhas, o número de nós aumenta, mas inversamente, o tempo demandado para o processamento de cada nó diminui.

O artifício de transformar o somatório de [0 a lg n] para [0 a infinito] foi utilizado pois, deste modo, pode-se facilmente calcular o resultado que será um valor constante (2). Como o segundo somatório é necessariamente maior que o primeiro a propriedade O se mantém.

O cálculo do número de sub-árvores de altura h pode ser entendido intuitivamente da seguinte maneira: na medida que nos deslocamos da raiz da árvore principal para as folhas, o número de sub-árvores dobra (exceto no último nível) e a altura diminui de um, ou seja, precisamos de uma equação que aumente o número de sub-árvores em uma proporção inversa a redução da altura destas sub-árvores em uma potência de dois.

Heapsort e seu invariante

O Heapsort completo funciona da seguinte maneira:

Inicialmente é acionada a sub-rotina BUILD-MAX-HEAP para construir um heap do vetor inteiro. Em seguida inicia-se um processo de ordenação baseada no heap, que repete as seguintes etapas para todos os elementos do vetor, iniciando-se do último e indo em direção ao segundo:

O tempo de execução do Heapsort é O(n lg n) pois, como foi demonstrado, o tempo de execução do BUILD-MAX-HEAP é O(n), adicionalmente o do MAX-HEAPIFY é O(lg n), mas ele é chamado n-1 vezes, o que torna todo o processo O(n lg n).

Duas coisas interessantes podem ser observadas neste processo de ordenação:

Loop Invariante do Heapsort

O Loop Invariante é dado no exercício 6.4-2:
HEAPSORT(A)
1  BUILD-MAX-HEAP(A)
for i <- comprimento[A] downto 2
3    do trocar A[1] <-> A[i]
4      tamanho-do-heap[A] <- tamanho-do-heap[A]-1
5      MAX-HEAPIFY(A, 1)

No início de cada iteração do loop for das linhas 2 a 5, o subarranjo A[1..i]  é um heap máximo contendo os i menores elementos A[1..n], e o subarranjo A[i+1..n] contém os n-i maiores elementos de A[1..n], ordenados.

Intuitivamente pode-se compreender este invariante percebendo-se que para cada valor de i do loop for, ele determina que todos os elementos do início do vetor até a posição i ainda fazem parte de um heap (com os menores elementos) e todos os elementos de posição acima de i já fazem parte de um vetor ordenado com os maiores elementos.

Este invariante é verdadeiro antes da primeira iteração do loop (inicialização) pois i é igual ao comprimento do vetor, deste modo, não há elementos com posição acima de i e todos os elementos até i fazem parte do heap recém construído.

O invariante é verdadeiro a cada iteração (manutenção), pois dentro do loop ele executa inicialmente a modificação de dois componentes o da posição 1 e o da posição i, trocando-os. O elemento da posição 1 é reordenado dentro do heap pelo MAX-HEAPIFY, garantindo que o heap se refaça até a posição i-1. O elemento da posição i, por sua vez, era anteriormente o maior do heap (raiz) que foi agora inserido no início da lista ordenada. Essa lista continuará ordenada dado que ela possui os maiores elementos de todo o vetor e agora recebe um valor que é por um lado o maior do heap e por outro menor que todos os elementos do vetor ordenado, assumindo sua posição exata no vetor ordenado final. Quando o loop fecha o laço, o i assume a próxima posição e a propriedade se verifica, dado que o heap (de um lado) e o vetor ordenado (de outro) se mantém corretamente.

Finalmente o invariante é verdadeiro no final (término) dado que o i estará na posição 1. Deste modo, o heap será de apenas um elemento e portanto está necessariamente correto, do outro lado todos os elementos acima da posição i estão ordenados e são os maiores.

O Heapsort realiza uma ordenação local, ao contrário do Mergesort que, para realizar a operação de Merge, necessita de espaço adicional fora do vetor, cujo tamanho não é constante.

O Heapsort é uma solução boa para registros de tamanho grande?

Nestes casos deve-se observar duas coisas:

HEAP-MAXIMUM, HEAP-EXTRACT-MAX, HEAP-INCREASE-KEY, MAX-HEAP-INSERT

Operações que se podem realizar com um heap quando este é utilizado em filas de prioridades: O autor (Thomas H. Cormen do livro Algoritmos) não apresenta um algoritmo para redução de chave (HEAP-DECREASE-KEY), mas é perfeitamente possível implementá-lo com eficiência, dado que, além do decremento da chave, o processo necessário para reordenação do heap será o MAX-HEAPIFY. Portanto é interessante acrescentar o HEAP-DECREASE-KEY como uma nova operação adicional para filas de prioridades.

Pode-se observar que a estrutura do heap é eficiente para algumas operações, tais como as apresentadas, mas não será para outras. Deste modo, é importante notar que cada estrutura é mais apta para um certo conjunto de operações.

O heap não é uma estrutura interessante para auxiliar em uma única operação de busca. Seu uso exigiria, para uma busca, a construção de um vetor ordenado, para posteriormente se realizar a busca. O processo total demandaria um tempo O(n lg n) que é pior do que o algoritmo de busca seqüencial, que possui tempo no pior caso O(n).