Algortimos de ordenação ordenam uma sequência de valores
vamos assumir por enquanto que os valores a serem ordenados são todos distintos entre si.
Ordenação por inserção
Dado um vetor A com n números, a ordenação por inserção irá executar n rodadas de instruções em que:
A cada rodada, temos um subvetor de A ordenado que contém um elemento a mais do que o subvetor da rodada anterior:
Ao fim da i−1-ésima rodada, o subvetor A[1..i−1] estará ordenado
Sabendo que o A[1..i−1] está ordenado, é fácil encaixar o elemento A[i] na posição correta, para deixar o subvetor A[1..i] ordenado:
Seja atual=A[i]. Iterativamente troque o elemento atual com os anteriores enquanto eles forem maiores que atual.
Ordenação por inserção
Algoritmo InsertionSort
Ordenação por inserção
Ordenação por inserção
Uma possível implementação em Python
definsertion_sort(A):for i inrange(1,len(A)):
atual = A[i]
j = i -1while j >=0and A[j]> atual:
A[j+1]= A[j]
j -=1
A[j+1]= atual
(observe que em Python a posição inicial do vetor é 0, então os índices foram ajustados adequadamente.)
Análise do InsertionSort
Vamos reponder as três perguntas com relação a análise de algoritmos:
O algoritmo está correto?
Quantos recursos o algoritmo consome?
É possível fazer melhor?
Corretude do InsertionSort
Algoritmos frequentemente inicializam, modificam, apagam ou inserem novos dados
Existe uma maneira de provar que o algoritmo funciona, sem checar (as infinitas) entradas?
Ideia principal: encontrar um invariante.
para raciociar sobre o comportamento de um algoritmo, frequentemente é útil identificar coisas que não mudam.
Invariante do InsertionSort
Suponha que temos o vetor [1,2,3,5,6,7,8∣4], em que o subvetor [1,2,3,5,6,7,8] já está ordenado e o elemento atual é o 4.
Iserir o 4 imediatamente à direita do maior elemento do subvetor que é menor que 4 (isto é, à direita de 3) produz um outro vetor ordenado.
Observe que este novo vetor é mais longo que o anterior por um elemento: A=[1,2,3,4,5,6,7,8]
Invariante do InsertionSort
Podemos aplicar essa ideia a cada passo:
[6∣,5,3,1,8,7,2,4]: o subvetor [6] está ordenado. Inserir o 5 na posição correta gera um subvetor ordenado [5,6]
[5,6∣,3,1,8,7,2,4]: o subvetor [5,6] está ordenado. Inserir o 3 na posição correta gera um subvetor ordenado [3,5,6]
[3,5,6,∣1,8,7,2,4]: o subvetor [3,5,6] está ordenado. Inserir o 1 na posição correta gera um subvetor ordenado [1,3,5,6]
⋯
[1,2,3,5,6,7,8∣4]: o subvetor [1,2,3,5,6,7,8] está ordenado. Inserir o 4 na posição correta gera um subvetor ordenado [1,2,3,4,5,6,7,8]
Corretude do InsertionSort
Existe um nome para a condição que é verdadeira antes e depois de cada iteração de um laço: invariante de laço
Para provar a corretudo do InsertionSort, vamos usar o invariante de laço e fazer uma prova por indução
Nesse caso, o invariante de laço é que, ao início da iteração i (a interação que queremos inserir o elemento A[i] no subvetor ordenado), o subvetor A[1..i−1] está ordenado
Corretude do InsertionSort
Em uma prova por indução, temos quatro componentes:
Hipótese de indução: o invariante de laço é verdadeiro depois da i-ésima iteração
Caso base: o invariante de laço é verdadeiro na primeira iteração.
Passo de indução: se o invariante de laço é verdadeira dapara a i-ésima interação, então ele também é verdadeiro para i+1-ésima iteração
Conclusão: se o invariante de laço é veradeira após a última iteração, então o algoritmo está correto!
Corretude do InsertionSort
Especificamente para o InsertionSort:
Hipótese de indução: na iteração i, A[1..i−1] está ordenado
Caso base: A[1] está ordenado.
Passo de indução: ao inserir o elemento A[i], A[1..i] continua ordenado
Conclusão: na n-ésima iteração (ao final do algortimo), A[1..n] (portanto o vetor inteiro) estará ordenado.
Corretude do InsertionSort
O caso base é verdade, pois um sub-vetor de 1 único elemento está ordenado.
Vamos supor que a hipótese de indução é válida para um i∈(2..n) (isto é, A[1..i−1] está ordenado).
Enquanto o laço "move" o elemento atual(A[i]) para a esquerda, os elementos maiores que atual são movidos uma posição para a direita, e continuam or ordem.
Corretude do InsertionSort
Quanto atual chega na posição "correta" no subvetor, todos os elementos à direita são maiores e todos os elementos a direta são menores que atual. Portanto, A[1..i] está ordenado e a invariante se mantém.
O laço termina quando i=n, portanto A[1..n] está ordenado e contendo todos os elementos do vetor A.
Podemos concluir que o algoritmo está correto.
Quantos recursos o algoritmo consome?
A quantidade de memória extra é desprezível: só precisamos de espaço extra para armazenar atual (ordenação in place)
Com relação ao tempo:
O laço da linha 1 é executado n vezes. Consequentemente, as linhas 2, 3 e 7 são executadas n−1 vezes cada.
Seja ri a quantidade de vezes que o laço da linha 4 é executado para a iteração i. As linhas 5 e 6 são executacas ri−1 vezes cada.
Quantos recursos o algoritmo consome?
Vamos escrever o tempo gasto pelo algoritmo com um polinômio T(n):
Para saber quantos recursos o algoritmo consome, precisamos saber o valor de ri.
Quantos recursos o algoritmo consome?
Melhor caso:
se o vetor já estiver odenado, o laço da linha 4 é executado 1 única vez (ri=1,∀i∈2..n) para cada iteração.
T(n)=2n+3i=2∑nri=5n−3
Quantos recursos o algoritmo consome?
Pior caso:
se o vetor estiver inversamente odenado, o laço da linha 4 é executado i vezes (ri=i,∀i∈2..n) para cada iteração. T(n)=2n+3i=2∑nri=n2+2n−6
(veja a somatório de Gauss para calcular o somatório)
Quantos recursos o algoritmo consome?
Caso médio:
- para fazer uma análise do caso médio, precisamos fazer alguma suposição sobre ri. Supunha que, em um caso típico, linha 4 é executada metade das vezes com relação ao pior caso (ri=2i,∀i∈2..n) para cada iteração.
T(n)=2n+3i=2∑nri=2n+4n(n−1)
Notação assintótica
O que significa medir o "tempo de execução" de um algoritmo?
Engenheiros provavelmente se importam com o "tempo de execução" (wall time): quanto tempo o algoritmo leva em segundos, minutos, horas, etc.
Isso depende de características do hardware, implementação, linguagem de programação, etc.
Apesar de importante, não será o nosso foco
Ao invés, queremos uma medida que é independente dessas considerações.
Notação assintótica
Ideia principal: focar como o tempo de execução escala em função de n (o tamanho da entrada).
Vantagens:
Abstrai o hardware, linguagem de programação, etc.
Mais genérica que a abordagem de implementar e testar
Desvantagens:
Só faz sentido se n é grande comparado com as constantes (9.999.999.999.999n é melhor que n2?)
A notação O
A notação O (big-O) é uma notação matemática para considerar o limite superior para o crescimento de uma função
Informalmente, ela pode ser obtida ignorando-se as constantes e termos que não dominam o crescimento da função
A notação O
Seja T(n) e g(n) funções sobre inteiros positivos.
Podemos pensar em T(n) como funções de tempo de execução: positiva e crescente em função de n.
Dizemos que "T(n) é O(g(n))" se g(n) cresce pelo menos tão rápido quanto T(n) conforme, n cresce.
Mais formalmente
T(n)=O(g(n)) se existem constantes positivas C e n0 tais que T(n)≤Cg(n) para todo n≥n0;
A notação O
Graficamente:
A notação O
Graficamente:
A notação O
Graficamente:
A notação O
Graficamente:
A notação O
Graficamente:
A notação O
Para provar que T(n)=O(g(n)), temos que mostrar que existe um c e n0 que satizfaz a definição.
Por exemplo, queremos provar que T(n)=10n2+5n+3 and g(n)=n2. Podemos mostrar que T(n)=O(g(n)) encontrando um c e n0 de acordo com a definição. Por exemplo: C=n210n2+5n+3=10+n5+n23
para n≥1 temos que 10+n5+n23≤10+5+3=18
Basta tomar n0=1 e C=18, e temos que T(n)=O(g(n))
A notação Ω
A notação Ω (big-Ω) é uma notação matemática para considerar o limite inferior para o crescimento de uma função
Informalmente, ela pode ser obtida ignorando-se as constantes e termos que não dominam o crescimento da função
A notação Ω
Seja T(n) e g(n) funções sobre inteiros positivos.
Dizemos que "T(n) é Ω(g(n))" se g(n) cresce no máximo tão rápido quanto T(n) conforme, n cresce.
Mais formalmente
T(n)=Ω(g(n)) se existem constantes positivas c e n0 tais que cg(n)≤T(n) para todo n≥n0;
A notação Ω
Graficamente:
A notação Ω
Similarmente, podemos provar que T(n)=Ω(g(n)), temos que mostrar que existe um c e n0 que satizfaz a definição.
Por exemplo, queremos provar que T(n)=10n2+5n+3 and g(n)=n2. Podemos mostrar que T(n)=Ω(g(n)) encontrando um c e n0 de acordo com a definição. Por exemplo:
c≤10+n5+n23
para n≥1 temos que
10+n5+n23≥10
Basta tomar n0=1 e c=10, que T(n)=Ω(g(n))
A notação Θ
Dizemos que T(n)=Θ(g(n)) se, e somente se:
T(n)=O(g(n)) e
T(n)=Ω(g(n))
T(n)=10n2+5n+3=Θ(n2) pois, como vimos anteriormente, T(n)=10n2+5n+3=O(n2) e T(n)=10n2+5n+3=Ω(n2)
Propriedades da notação assintótica
Sejam f(n), g(n) e h(n) funcoes positivas. Temos que:
f(n)=Θ(f(n));
f(n)=Θ(g(n)) se e somente se g(n)=Θ(f(n));
f(n)=O(g(n)) se e somente se g(n)=Ω(f(n));
Se f(n)=O(g(n)) e g(n)=Ω(h(n)), então f(n)=O(h(n)); O mesmo vale substituindo O por Ω;
Se f(n)=Θ(g(n)) e g(n)=O(h(n)), entao f(n)=O(h(n)); O mesmo vale substituindo O por Ω;
f(n)=O(g(n)+h(n)) se e somente se f(n)=O(g(n))+O(h(n)); O mesmo vale substituindo O por Ω ou por Θ;
Se f(n)=O(g(n)) e g(n)=O(h(n)), entao f(n)=O(h(n)); O mesmo vale substituindo O por Ω ou por Θ.
Complexidade do InsertionSort
A complexidade do InstertionSort é:
Melhor caso
Caso médio
Pior caso
Θ(n)
Θ(n2)
Θ(n2)
Complexidade do InsertionSort
Relembre que, no melhor caso T(n)=5n−3
De acordo com as propriedades 1 e 2, temos que:
T(n)=5n−3=Θ(n)
Complexidade do InsertionSort
Relembre que, no pior caso T(n)=n2+2n−6
Podemos provar como fizemos anteriormente (encontrar c e n0), mas obserse que, para qualquer T(n) na forma: T(n)=an2+bn+c
se tomarmos n0=1 e notar que para todo n≥n0 temos que an2≤an2+bn+c≤(a+b+c)n2
escolhendo c=a e C=a+b+c na definição de O e Ω (por consequencia Θ), temos que T(n)=n2+2n−6=Θ(n2)
Função assintótica de polinômios
Com uma análise similiar, podemos mostrar que para qualquer polinômio T(n)=i=1∑kaini
em que ai é uma constante para i∈1..k, e ak>0, temos que
T(n)=Θ(ak)
Complexidade do InsertionSort
Relembre que, no caso médio T(n)=2n+4n(n−1)=4n2+47n
que é similar ao caso anterior, portanto T(n)=2n+4n(n−1)=Θ(n2)
Complexidade do InsertionSort
Na prática, como estamos interessados no comportamento assintótico, não precisamos fazer uma análise tão cuidadosa como a que fizemos no início da aula.
Por exemplo, não precisamos contar as linas 2,3,7 e 5,6, pois "o que importa" é número de repetições do laço
Essa é uma das vantagens de se utilizar notação assintótica para estimar os recursos de um algoritmo
Complexidade
Em geral, quando falamos da complexidade de um algoritmo, estamos interessados no pior caso
nem sempre é fácil definir um "caso médio"
Qual é o limite superior do pior caso desse algoritmo?