Considere o seguinte algoritmo que recebe um vetor A[p · · ·r] com p > 0, r > 0, e p ≤ r e de números inteiros:
SOMA(A, p,r)
1: if p < r then
2: q ← ⌊ p+r/ 2⌋
3: x ← SOMA(A, p, q)
4: y ← SOMA(A, q + 1,r)
5: return x + y
6: else
7: return A[p]
O algoritmo promete devolver a soma dos elementos do vetor, ele faz o que promete? Faça uma pequena simulação para mostrar o passo-a-passo de como ele funciona. Calcule em termos da notação O o tempo desse algoritmo, mas procure dar a resposta mais justa possível. Justifique a sua resposta.
Escreva um algoritmo que recebe um vetor decrescente A[1..n] de números inteiros e devolve a diferença A[j] − A[i] tal que i e j sejam escolhidos de tal forma a maximizar A[j] − A[i] para 1 ≤ i < j ≤ n. Descreva um algoritmo eficiente para esta tarefa. Qual a complexidade do teu algoritmo?
Indique com V (verdadeiro) ou F (falso) cada uma das afirmativas abaixo. É obrigatório justificar a resposta.
(a) O algoritmo de “quicksort” pode ser implementado para tentar na prática evitar o pior caso.
(b) O merge sort sempre consome tempo Θ(nlgn)
(c) A busca binária não precisa de um vetor ordenado para funcionar.
(d) O filho esquerdo de um nó k em um heap é dado por ⌊k/2⌋.
1) Sim, o algoritmo cumpre o que promete, somando todos os elementos do vetor. Ele faz isso dividindo o vetor sempre ao meio, calculando a soma de cada metade recursivamente, e depois somando os resultados.
Por exemplo, suponha o vetor A = [2, 4, 6, 1, 3, 5], sendo este 1-indexado (a primeira posição possui índice 1)
Para calcular a complexidade deste algoritmo, poderíamos utilziar o teorema mestre. Porém, isso não será necessário, pois a complexidade deste algoritmo é bastante intuitiva. Apesar de somar os elementos do vetor de uma forma mais "complicada" que o habitual, na prática o algoritmo sempre acessará todas as N posições do vetor, e cada uma delas apenas uma única vez. Sendo assim, a complexidade é O(N)
2) Como o vetor é dado na forma decrescente, pode-se encontrar a maior diferença A[j] - A[i] de forma gulosa. Para que essa diferença seja a maior possível, queremos que A[j] seja o maior possível e que A[i] seja o menor possível, e como o vetor está ordenado de forma decrescente, sabemos que o maior elemento está na primeira posição (A[1]) e o menor elemento está na última posição (A[n]). Ou seja, a maior diferença é A[1] - A[n]. Todas as operações envolvidas possuem custo constante (acesso a 2 posições do vetor e cálculo da diferença), por isso este algoritmo tem complexidade constante: O(1).
3)
(a) O algoritmo de “quicksort” pode ser implementado para tentar na prática evitar o pior caso.
Verdadeiro, mas cuidado! Existem formas de otimizar o algoritmo "quicksort" para evitar cair no pior caso (acredito que a técnica "mediana de três" seja a mais conhecida), porém isso não garante que o algoritmo nunca cairá no pior caso, ainda existem casos no qual a complexidade será O(n²).
(b) O merge sort sempre consome tempo ?(nlgn)
Verdadeiro. Essa é uma das grandes vantagens do merge sort. Na maioria dos casos ele é mais lento que o quicksort, mas diferente deste último, sua complexidade no pior caso ainda é O(nlgn).
(c) A busca binária não precisa de um vetor ordenado para funcionar.
Falso. O vetor estar ordenado é o que nos garante "eliminar" metade do intervalo de busca de uma vez. Dado um vetor A[1..n] em que estamos buscando o valor x e possui a posição central q = (n + 1)/2. Com o vetor ordenado, se A[q] = x, então já encontramos o valor desejado; se A[q] < x, estão x só pode estar entre as posições q+1 e n, pois todas as posições de 1 à q-1 possuem valores menores que A[q], e assim sendo, menores que x; de forma análoga, se A[q] > x, o valor x só pode estar entre as posições 1 e q-1. Agora, se o vetor não estivesse ordenado, não teriamos como garantir nada disso, e todas as posições precisariam ser varridas.
(d) O filho esquerdo de um nó k em um heap é dado por ?k/2?.
Falso. Pela definição de heap, com raiz iniciada em 0, um nó k possui o filho esquerdo 2*k + 1 e o filho direito 2*k + 2.