- Considere que T(n)=3T(n/3)+1 e queremos provar pelo método da substituição que ele é O(n)
- Temo que provar que T(n)≤cn para alguma constante positiva c, por indução em n
- No passo indutivo, temos:
T(n)=3T(n/3)+1≤cn+1
- Entretanto, a nossa hipótese é que T(n)≤cn, (e não cn+1, como obtivemos no passo indutivo)
- Isso não significa em T(n)=O(n).
- O problema é que a expressão que usamos para provar nosso palpite não foi "forte" o suficiente. Vamos tentar provar que T(n)≤cn−d, que contina sendo O(n). No passo indutivo temos:
T(n)=3T(n/3)+1≤3(3cn−d)+1=cn−3d+1≤cn−d
- No caso base tesmo que T(1)=1≤c−d sempre que c≥d+1, portanto T(n)=O(cn−d)=O(n)
- Considere que T(n)=T(⌊n/2⌋+2)+1 e queremos provar pelo método da substituição que ele é O(logn), assumindo o caso base T(4)=1
- É possível provar T(n)≤clogn, que no passo indutivo fica
T(n)=T(⌊n/2⌋+2)+1≤clog(2n+2)+1=clog(2n+4)+1≤clog(3n/2)−c+1=clogn−c(2−log3)+1≤clogn
- em que a penúltima desigualdade vale para n>8 e a última para c≥1/(2−log3)
-
Entretanto, fortalecendo a hipótese indutiva para T(n)≤clog(n−a), no passo indutivo temos
T(n)=T(⌊n/2⌋+2)+1≤clog(2n+2−a)+1=clog(2n−a)+1=clog(n−a)−c+1≤clog(n−a)
-
em que a primeira desigualdade vale para a≥4 e a última para c≥1. Escolhendo a=4, T(6)=T(5)+1=T(4)+2=3≤clog(6−4) para c≥3. Portanto, fazendo a=4 e c≥3, temos que T(n) \leq c \log (n-a) para n≥6.
-
Em algumas situações associamos uma prioridade a cada item coleção, e gostaríamos de ter acesso rápido ao item de maior prioridade
-
É uma coleção dinâmica de elementos que possuem prioridades associadas e a operação de remoção deve sempre remover o elemento que possui maior prioridade.
-
O termo prioridade é genérico: ter maior prioridade não significa necessariamente o maior valor.
- Atendimento a idoso em um banco: maior idade → maior prioridade
- Gerenciamento de estoque: menor quantidade de items → maior prioridade
-
Constrói uma fila de prioridade a partir de um conjunto
-
Insere elemento na fila
-
Remove elemento com maior prioridade
-
Alteração da prioridade de um elemento
- Heap pode ser entendida com uma árvore
- quase completa:
- Todos os níveis exceto o último devem estar totalmente preenchidos.
- Se o seu último nível não estiver preenchido, os nós devem estar preenchidos da esquerda para a direita.
- satisfaz a propriedade heap:
- Um nó deve ter prioridade maior ou igual à prioridade de seus filhos, se eles existirem.
-
No entanto, heaps são normalmente implementadas em um vetor
-
Nesse caso, para uma determinada posição i
-
pai(i)=⌊2i⌋
-
filhoEsquerdo(i)=2∗i
-
filhoDireito(i)=2∗i+1
-
(obs): 2i e 2∗i podem ser implementados de maneira eficiente usando bit shifts
-
Um vetor ordenado é pela priopridade é um Heap
-
Um problema é que ordenar o vetor tem custo de (pelo menos) O(nlogn)
-
No entanto, o vetor não precisa estar completamente ordenado... somente os caminhos da "raíz" (primeiro nível) até as "folhas" (último nível) é que precisa estar ordenado
-
Suponha que um determinada subárvore cuja raíz é H[i] viola a propriedade heap (isto é, sua prioridade é menor que a de (pelo menos um) seus filhos)
-
Entretanto, as subárvores filhas H[2i] e H[2i+1] são heap.
-
Podemos corrigir a heap "descendo" o elmento H[i]:
- Trocar H[i] com o filho de maior prioridade
- Chamar recursivamente CorrigeDescendo para o filho trocado (pois a propriedade Heap pode continuar violada para ele)
- Exemplos de CorrigeHeapDescendo(H,2)
Seja hx=⌊log(n/x)⌋ a altura do nó que está na posição x
- Caso Base: hi=0 o nó é uma folha, portanto não tem filhos e a propriedade heap é válida
- Hipótese: Se H[2k] e H[2k+1] são heaps, CorrigeHeapDescendo(H,k) restaura a Heap para todo nó de altura hk<hi
- Se H[i] tem prioridade maior que seus filhos, o algoritmo não faz nada pois a proriedade heap já é valida. Temos que mostrar por indução que CorrigeHeapDescendo(H,i) está correto
- Se a prioridade de H[i] é menor que um de (pelo menos um de) seus filhos, o algoritmo troca H[i] com o maior deles, e chama recursivamente CorrigeHeapDescendo(H,maior). Como a altura do filho é menor que a do pai, pela Hipótese de indução, H[i] agora é uma haep.
-
CorrigeHeapDescendo(H,i) tem um tempo proporcional a altura de i, ou seja O(log(n−i)).
-
No pior caso, i é a raiz da árvore, então a complexidade é proporcianal a altura da árvore
-
Como a heap é uma árvore binária quase completa, sua altura é O(logn).
-
Portanto, a complexidade de CorrigeHeapDescendo é O(logn)
-
Considere que temos um vetor qualquer (não heap), e queremos transformá-lo em uma heap
-
Podemos transformá-lo em uma haep utilizando CorrigeHeapDescendo como uma subrotina:
- Observe que os nós folhas são heap (não tem filhos).
- Podemos chamar CorrigeHeapDescendo para os nós não folha, começando pelo primeiro elemento não folha H⌊n/2⌋ até o raíz H[1]
-
É fácil mostrar que ConstroiHeap funciona, já que ele faz suscessivas chamadas a CorrigeHeapDescendo
-
A invariante de laço é que a cada iteração do laço para indexado por i, para todo j tal que i+1≤j≤n , a ́arvore enraizada em H[j] e um heap.
-
Como CorrigeHeapDescendo é excutado H⌊n/2⌋ até H[i], a invariante de laço se mantém e por indução podemos provar que ConstroiHeap constrói uma heap para qualquer vetor.
-
Uma análise preliminar, podemos constantar que ConstroiHeap tem complexidade O(nlogn), uma vez CorrigeHeapDescendo tem complexidade O(logn) e ele é executado ⌊n/2⌋ vezes, que é O(n)
-
Entretanto, observe que O(logn) é um limite superior para a complexidade de ConstroiHeap. Na verdade sua complexidade é proporcianal à algura do elemente i, ou seja, O(log(n−i)).
-
Podemos usar esse fato para encontrar uma complexidade mais justa para o ConstroiHeap
- Uma estimativa quantidade de nós em uma dada altura h da heap é ⌈2h+1n⌉.
- Podemos escrever a complexidade do ConstroiHeap como:
T(n)=h=0∑log(n)⌈2h+1n⌉×O(h)=O⎝⎛n×h=0∑lg(n)2hh⎠⎞=O(n×h=0∑∞2hh)
- Observe que para i≥1, ((i+1)/2i+1)/(i/2i)<1 .
T(n)=O(n×h=0∑∞2hh)≤cn=O(n)
- Uma outra operação da heap é a remoção do item de maior prioridade
- Observe que o primeiro elmento da heap é o item de maior prioridade. Então acessá-lo é O(1)
- Para a remoção, precisamos colocar outro elmento no lugar.
- Uma alteração que mexe o mínimo possível na Heap é trocar o primeiro com o último elemento.
- Podemos então usar CorrigeHeapDescendo(H,1) para corrigir a heap
-
Observe que RemoveDaHeap viola apenas a propriedade heap do nó raiz, já que o último elemento é uma folha e não mexemos em outras posições da heap.
- CorrigeHeapDescendo(H,1) restaura a propriedade heap, portanto RemoveDaHeap está correto
-
A complexidade é dominada por CorrigeHeapDescendo(H,1), já que a troca é O(1). Portanto, a complexidade é O(logn)
- Caso haja espaço na heap, uma estratégia é inserir na primeira posição disponível
- Obviamente, isso pode violar propriedade heap
- No entanto, antes da inserção, a propriedade heap está mantida.
- O procedimento CorrigeHeapSubindo pode ser usado para restaurar a priopridade heap
- Dada uma posição H(i), se a prioridade de H(i) é maior que a de seu pai, podemos trocar H(i) com o seu pai.
- A propriedade heap das subárvores está restabelecida, mas eventualmente é preciso continuar subindo até chegar ao nó raiz.
-
Um argumento similar ao usado para CorrigeHeapDescendo pode ser usado para demonstrar por indução que CorrigeHeapSubindo está correto
-
De maneira similar, podemos mostrar que a complexidade de CorrigeHeapSubindo é O(nlogn)
-
Observe que InserirNaHeap viola apenas a propriedade heap do último nó, já que não mexemos em outras posições da heap.
- CorrigeHeapSubindo(H,1) restaura a propriedade heap, portanto InserirNaHeap está correto
-
A complexidade é dominada por CorrigeHeapSubindo, já que a inserção é O(1). Portanto, a complexidade é O(logn)
- Para alterar a prioridade de um elemento, podemos usar CorrigeHeapSubindo ou CorrigeHeapDescendo, dependendo se a prioridade do elemento i subiu ou desceu
- Se a prioridade cresceu, chamar CorrigeHeapSubindo(H,i)
- Se a prioridade desceu, CorrigeHeapDescendo(H,i)
- Como AlteraHeap é baseado em CorrigeHeapSubindo e CorrigeHeapDescendo, podemos concluir que ele está correto, e que a sua complexidade é O(logn)
-
A análise de corretude do selecion sort é similar a do heap sort (veremos mais adiante), assumindo que o algoritmo que seleciona o máximo funciona
-
A complexidade é O(n2)
-
Antes de cada iteração i do laço para, temos que:
- O subvetor A[i+1..n] contém os n−i+1 elmentos de A em ordem crescente
- O subvetor A[1..i] é uma heap
-
No início da iteração i, HeapSort troca A[1] (que por definição é o maior elemento da Heap) com A[i], e diminui o tamanho da heap em 1. - A[i] é menor que os elementos A[i+1..n], portanto A[i..n] continua ordenado.
-
Sabemos que CorrigeHeapDescendo(A,1), está correto. Portanto a propriedade se mantém, e podemos provar por indução que HeapSort está correto
-
Heap Sort tem complexidade O(nlogn) (cada chamada para refazer a heap tem custo O(logn), e é chamada n vezes).
-
Além disso, também há o custo adicional de construir a heap O(n).
-
A ordem original dos elementos iguais pode mudar, portanto ele não é estável