Mediana e i-ésimo menor elemento

Este página trata do problema da encontrar a mediana de um conjunto. (Não confunda mediana com média. Por exemplo, a média de  {1, 2, 99} é 51, enquanto a mediana é 2.) Também trata do problema mais geral de encontrar o i-ésimo menor elemento do conjunto. É fácil resolver o problema em tempo linearítmico; mas gostaríamos de resolver o problema em tempo linear. Mostraremos como fazer isso em média.

A página é inspirada no capítulo 9 de CLRS.

Mediana

Dado um conjunto S de números, o posto (= rank) de um elemento x de S é número de elementos de S que são menores ou iguais a x, ou seja, a cardinalidade do conjunto { sS : sx }. Se os elementos de S são  s1, s2, … , sn  em ordem crescente então o posto de si é  i. (Convém não esquecer que, como em qualquer conjunto, os elementos de S são distintos dois a dois.)

O i-ésimo menor elemento de um conjunto S é o elemento de posto i. Por exemplo, o 1-ésimo menor elemento de S é o mínimo de S e o S⎮-ésimo menor elemento é o máximo de S.

A mediana (= median) de um conjunto S com n elementos é o  ⌊(n+1)/2⌋-ésimo menor elemento de S. (Eu poderia ter adotado como mediana o ⌈(n+1)/2⌉-ésimo menor elemento; isso só faria diferença quando n é par.)

Nossos conjuntos serão representados por vetores com elementos distintos dois a dois, ou seja, sem elementos repetidos. (É bem verdade que quase tudo se aplica igualmente bem a vetores com elementos repetidos, mas a discussão fica mais simples se os elementos forem distintos dois a dois.)

Problema da mediana:  Encontrar a mediana de um vetor A[p .. r] sem elementos repetidos.

Problema da mediana generalizada:  Dado um vetor A[p .. r] sem elementos repetidos e um número natural i, encontrar o i-ésimo menor elemento do vetor.

É fácil resolver o problema em Ο(nlg n) unidades de tempo, sendo n o número de elementos do vetor: basta rearranjar A[p .. r] em ordem crescente e pinçar o elemento A[p + i − 1]. O desafio é obter um algoritmo mais rápido.

Exemplo A.  No exemplo a seguir, o quarto menor elemento do vetor é 41, o sétimo menor elemento é 66, e a mediana é 55.

33 44 55 77 95 99 22 25 41 66 88 89

Exercícios 1

  1. Suponha que o vetor A[1 .. n] é estritamente crescente. É correto dizer que a mediana de A[1 .. n] é A[n/2]? É correto que a mediana de A[1 .. n] é n/2⌉?
  2. Critique a seguinte proposta de definição: mediana de um conjunto de n números é o n/2⌋-ésimo menor elemento do conjunto.
  3. Suponha que os elementos do vetor A[1 .. n] são distintos dois a dois. Critique a seguinte proposta de definição de mediana:  mediana de A[1 .. n] é A[⌈n/2⌉].
  4. Prove que ⌊(n+1)/2⌋ = ⌈n/2⌉. Prove que ⌈(n+1)/2⌉ = ⌊n/2⌋ + 1.
  5. ★ Escreva um algoritmo que consuma tempo Ο(n) para determinar o segundo menor elemento de um vetor com n ≥ 2 elementos distintos dois a dois. Escreva um algoritmo que consuma tempo Ο(n) para determinar o n−1-ésimo menor elemento de um vetor com n ≥ 2 elementos distintos dois a dois.
  6. Escreva um algoritmo que encontre o menor e o segundo menor elemento de um vetor A[1 .. n] sem elementos repetidos fazendo apenas n + ⌈lg n⌉ − 2 comparações entre elementos do vetor.
  7. Como as definições de posto e i-ésimo menor elemento devem ser alteradas para que se apliquem a um vetor com elementos repetidos?
  8. ★ Suponha que caiu do céu um algoritmo linear que calcula a mediana de um vetor com n elementos distintos dois a dois. Como esse algoritmo poderia ser usado para melhorar a qualidade do subalgoritmo Divide usado pelo Quicksort?
  9. Suponha dado um algoritmo Mediana que rearranja os elementos de um vetor A[p .. r] de números inteiros distintos dois a dois e devolve um índice q tal que A[q] é a mediana do vetor e A[p .. q−1] < A[q] < A[q+1 .. r]. Suponha ainda que o consumo de tempo do algoritmo é Θ(n), sendo n = rp+1. Escreva um algoritmo que use Mediana para resolver o problema da mediana generalizada. O seu algoritmo deve consumir Ο(n) unidades de tempo.

Algoritmo de seleção

Começamos por estudar um algoritmo que, no pior caso, é pior que ordenar-e-pinçar. Embora ruim, ele servirá de base para um algoritmo melhor, a ser estudado na próxima seção.

O algoritmo Select recebe um vetor A[p .. r] sem elementos repetidos e um número natural i e devolve o i-ésimo menor elemento do vetor. Vamos supor que 1 ≤ irp+1 (e portanto p ≤ r), pois do contrário a instância do problema não tem solução.

Select (A, p, r, i)   ▷ 1 ≤ irp+1
1 . se p = r
2 .ooo devolva A[p] e pare
3 . q := Divide (A, p, r)   pqr
4 . k := qp + 1
5 . se i = k
6 .ooo devolva A[q] e pare
7 . se i < k
8 .ooo devolva Select (A, p, q − 1, i) e pare
9 . devolva Select (A, q + 1, r, i − k)

Na linha 3, o algoritmo invoca a rotina Divide da página Ordenação: Quicksort, que rearranja o vetor e devolve um índice q tal que A[p .. q−1] < A[q] < A[q+1 .. r] (as duas desigualdades são estritas pois o vetor não tem elementos repetidos são distintos). Observe que nas linhas 8 e 9 as condições para a invocação de Select estão satisfeitas. Observe também que as instâncias de que essas linhas tratam são menores que a instância original.

Prova da correção do algoritmo.  Se p = r então i = 1 e portanto o algoritmo devolve a resposta correta na linha 2. Agora suponha que p < r. Depois da linha 3, temos A[p .. q−1] < A[q] < A[q+1 .. r]. Depois da linha 4, k é o número de elementos de A[p .. q].

O algoritmo Select usa o método da divisão e conquista. A fase da divisão — executada por Divide — é a que consome mais tempo. A fase da conquista consiste nas duas chamadas recursivas, linhas 8 e 9. A fase da combinação é vazia.

Exemplo B.  A figura faz o rastreamento da execução do algoritmo Select ao procurar pelo 7-o menor elemento de um vetor.

[33 44 55 77 95 99 22 25 41 66 88 89] [33 44 55 77 22 25 41 66 88] 89 [95 99] [33 44 55 77 22 25 41 66] 88 89 [95 99] [33 44 55 22 25 41] 66 [77] 88 89 [95 99]

O 7-o menor elemento do vetor é 66. (Isso foi detectado na linha 6 do algoritmo.)

Exercícios 2

  1. Diga o que o algoritmo Select faz.
  2. Por que Select não precisa tratar de instâncias em que p = r+1, como faz o Quicksort?
  3. ★ Mostre que na linha 8 as condições para a invocação de Select estão satisfeitas. Mostre que na linha 9 as condições para a invocação de Select estão satisfeitas. Mostre que na linha 9 o i − k-ésimo menor elemento de A[q+1 .. r] é o i-ésimo menor elemento de A[p .. q−1].
  4. ★ Analise uma execução de Select em que q é igual a r no fim da linha 3. Analise uma execução de Select em que q é igual a p no fim da linha 3.
  5. ★ Complete os detalhes da prova da correção do algoritmo Select.
  6. Suponha dado um algoritmo Divide2 que receba um vetor A[p .. r] e devolva um índice q no intervalo p .. r−1 depois de rearranjar o vetor de tal modo que A[p .. q] ≤ A[q+1 .. r]. Escreva uma versão do Select que use o algoritmo Divide2 no lugar de Divide.
  7. Versão iterativa. Escreva uma versão iterativa do algoritmo Select.

Desempenho

Convém medir o consumo de tempo de Select indiretamente, contando o número de comparações entre elementos do vetor como já fizemos ao estudar o Quicksort. Para simplificar, diremos simplesmente comparações, deixando a expressão entre elementos do vetor subentendida. Essas comparações ocorrem todas na linha 4 da rotina Divide, que faz exatamente m − 1 comparações quando recebe um vetor com m elementos.

Desempenho no pior caso.  Seja n = rp+1 o número de elementos do vetor e denote por P(n) o número de comparações no pior caso. A intuição sugere que o pior caso acontece quando toda execução de Divide faz uma divisão desequilibrada do vetor. Mais precisamente, o pior caso acontece quando (1) a linha 6 nunca é executada, (2) depois de cada execução da linha 3, q vale p (e portanto a linha 9 é executada) ou vale r (e portanto a linha 8 é executada). Nesse caso, P(n) satisfaz a recorrência

  P(n) = P(n−1) + n − 1 .  (A)

O termo P(n−1) representa o número de comparações implícito nas linhas 8 e 9 e o termo n − 1 é o número de comparações em Divide. A recorrência pode ser facilmente desenrolada:

P(n) = P(n−1) + n − 1
  = P(n−2) + (n − 2) + (n − 1)
  = P(n−3) + (n − 3) + (n − 2) + (n − 1)
 
  = P(1) + 1 + 2 + … + (n − 2) + (n − 1)
  = n(n−1)/2

pois P(1) = 0. (Verifique por indução!) Logo, P(n) = Θ(n²).

Como o consumo de tempo é proporcional ao número de comparações, concluímos que o algoritmo Select é quadrático no pior caso. Esse desempenho é pior que o trivial ordenar-e-pinçar.

Desempenho no melhor caso.  No melhor caso, Select executa a linha 3 uma única vez e termina na linha 6. Assim, o algoritmo faz apenas n − 1 comparações.

Uma forma menos radical de melhor caso é aquela em que a linha 6 nunca é executada e todas as execuções da linha 3 produzem uma divisão equilibrada do vetor, deixando metade dos elementos de um lado e metade do outro. Nesse caso, o número M(n) de comparações satisfaz a recorrência

  M(n) = M(⌊(n−1)/2⌋) + n − 1 .  (B)

Para tentar obter uma pista da solução dessa recorrência, vamos resolver a recorrência mais simples M(n) = M(n/2) + n − 1:

M(n) = M(n/2) + n − 1
  = M(n/4) + n/2 − 1  +  n − 1
  = M(n/4) + n/2 + n − 2
  = M(n/8) + n/4 + n/2 + n − 3
  = M(n/2i) + n/2i-1 + n/2i-2 + … + n/2i-ii
  = M(n/2i) + 21n/2i + 22n/2i + … + 2in/2ii
  = M(n/2i) + n (21 + 22 + … + 2i) / 2ii
  = M(n/2i) + n (2i+1 − 2) / 2ii

(veja o Apêndice). Vamos imaginar que os valores de n são potências de 2. Então, quando i atingir lg n, teremos M(n) = 2 (n − 1) − lg n, pois M(1) = 0. Resumindo: M(n) = Θ(n).

É possível verificar diretamente que a solução M(n) da recorrência original (B) também está em Θ(n). O Teorema Mestre (veja a página Recorrências) também confirma que M(n) = Θ(n). Logo o algoritmo Select é linear no melhor caso.

Desempenho em um caso típico.  Mesmo que a linha 3 não divida A[p .. r] de maneira perfeitamente equilibrada, o algoritmo Select continua linear se todas as divisões deixarem uma porcentagem dos elementos do vetor de um lado e a porcentagem complementar do outro, como acontece com o algoritmo Quicksort. Isso sugere que, em média, o algoritmo Select é linear.

Exercícios 3

  1. ★ Confirme, por indução, a solução da recorrência (A).
  2. Mostre por indução, diretamente a partir da recorrência (A), que n²/4 ≤ P(n) ≤ n²/2 para todo n ≥ 1.
  3. Justificando a intuição. Usamos a intuição para dizer que P(n) satisfaz a recorrência (A). Se ignorarmos a intuição, devemos reescrever (A) assim:

    P(n)  =  maxhP(h)  +  n − 1 ,

    com max tomado sobre todos os valores de h no intervalo 1 .. n−1. (Os casos h = 0 e h = n não acontecem porque nem o vetor A[p .. q−1] na linha 8 nem o vetor A[q+1 .. r] na linha 9 são vazios.) (Compare o P(h) com o P(i−1) + P(ni) na recorrência correspondente da página Ordenação: Quicksort.) Mostre que P(n) = Ο(n²). Mostre que P(n) = Ω(n²).

  4. ★ Confirme, por indução, que a solução da recorrência (B) está em Θ(n).
  5. ★ Use o Teorema Mestre para confirmar que a solução da recorrência (B) está em Θ(n).

Seleção aleatorizada

A seção anterior mostrou que o algoritmo Select tem mau desempenho quando a linha 3 divide o vetor de maneira sistematicamente muito desequilibrada. Isso acontece quando o pivô na rotina Divide é sistematicamente muito pequeno ou muito grande. Para evitar essa situação extrema, é uma boa ideia escolher o pivô aleatoriamente, como já fizemos na versão aleatorizada do Quicksort. O algoritmo Rand-Select faz isso. Ele recebe um vetor A[p .. r] sem elementos repetidos e um número natural i no intervalo 1 .. rp+1 e devolve o i-ésimo menor elemento do vetor.

Rand-Select (A, p, r, i)   ▷ 1 ≤ irp+1
1 . se p = r
2 .ooo devolva A[p] e pare
3 . q := Rand-Divide (A, p, r)   pqr
4 . k := qp + 1
5 . se i = k
6 .ooo devolva A[q] e pare
7 . se i < k
8 .ooo devolva Rand-Select (A, p, q − 1, i) e pare
9 . devolva Rand-Select (A, q + 1, r, i − k)

Não faz sentido discutir o desempenho de pior caso nem o de melhor caso de Rand-Quicksort. Devemos tratar do desempenho esperado (no sentido que essa palavra tem na teoria das probabilidades). Como já fizemos na seção anterior, vamos medir o consumo de tempo pelo número de comparações (linha 4 de Divide).

O ponto de partida para o cálculo do número esperado de comparações é a seguinte observação: o número de comparações que o algoritmo executa não depende dos valores dos elementos do vetor A[p .. r] mas apenas da posição de seu menor elemento, da posição do segundo menor elemento, da posição do terceiro menor elemento, etc. Assim, por exemplo, o vetor [88 55 99 22] é equivalente ao vetor [3 2 4 1]. Podemos, portanto, restringir nossa atenção aos vetores que são permutações de 1 .. n, sendo n = rp+1.

Exemplo C: A tabela mostra o número esperado de comparações para todas as n! permutações de 1 .. n supondo que a linha 6 nunca é executada, que a linha 8 é executada se q − p > r − q e que a linha 9 é executada q − p < r − q.

n comparações
1 0 / 1
2 2 / 2
3 16 / 6
4 116 / 24
5 864 / 120
6 7128 / 720
7 63744 / 5040
8 630816 / 40320

Suponha que A[p .. r] é uma permutação aleatória de 1 .. n e seja  E(n)  o número esperado de comparações que Rand-Select executa supondo que todas as n! permutações são igualmente prováveis. (Em outras palavras, E(n) é o número médio de comparações para as n! permutações de 1 .. n. Veja a página Análise probabilística e algoritmos aleatorizados.) Um cálculo probabilístico mostra que E(n) satisfaz a recorrência

 
E(n)  ≤   2  n/2⌋ ≤ i < nE(i)  +  n − 1 .
n
 (C)

(Veja a dedução da recorrência na seção 9.2 de CLRS e compare com a recorrência correspondente do Rand-Quicksort.)  De acordo com a recorrência (C), E(2) ≤ (2/2) E(1) + 1, E(3) ≤ (2/3) (E(1) + E(2)) + 2, E(4) ≤ (2/4) (E(2) + E(3)) + 3, e assim por diante. Como E(1) = 0, temos

E(n)  ≤  8n

para todo n ≥ 1. Isso não surpreende em face da discussão do desempenho típico de Select (sem a aleatorização).

Exemplo D: A tabela mostra a cota superior (C) para alguns valores de n. Verifique esses números! Escreva os números em notação ponto-flutuante.

n   (C)
1 0 / 1
2 2 / 2
3 16 / 6
4 116 / 24
5 888 / 120
6 7176 / 720
7 66048 / 5040
8 638112 / 40320

Como o consumo de tempo de Rand-Select é proporcional ao número de comparações, concluímos que o consumo de tempo esperado do algoritmo é

Ο(n) .

Exercícios 4

  1. Números de comparações para todas as permutações.  Seja A[1 .. n] uma permutação de 1 .. n. Submeta A[1 .. n] ao Select sem especificar o parâmetro i e conte o número total de comparações. Suponha que a linha 6 de Select nunca é executada; nas linhas 8 e 9, escolha sempre o lado maior. Faça isso para n = 1, 2, 3, 4 e para cada permutação de 1 .. n. Construa uma tabela para apresentar os resultados. (Para construir a tabela de n, use as tabelas de n−1, n−2, etc. A título de amostra, veja abaixo duas linhas da tabela de 3.)
        antes         depois        comparações
        [2  1  3]     [2  1] 3      3
        [1  3  2]     [1] 2 [3]     2
    
  2. Calcule a cota superior de E(2), E(3), E(4), E(5), E(6) na recorrência para E(n) dada acima. (Por definição, E(1) = 0.) Escreva cada E(n) na forma m/n! com m um número natural. Compare o resultado com o exercício Número de comparações para permutações proposto logo acima. (Observação: Como era de se esperar, E(1) a E(5) coincidem com os correspondentes números esperados de comparações do algoritmo Rand-Quicksort.)
  3. Mostre que a solução da recorrência (C) satisfaz E(n) ≤ 8n para todo n ≥ 1. Mostre também que E(n) ≥ 2n para todo n ≥ 9. [Solução]
  4. É um boa ideia trocar a invocação de Divide por uma invocação de Select (ou se Rand-Select) no código do algoritmo Quicksort?

Seleção em tempo linear

Blum, Floyd, Pratt, Rivest, Tarjan encontraram (1973) um algoritmo linear para o problema da mediana generalizada. O algoritmo consome tempo Ο(n), mesmo no pior caso, mas é um tanto complexo e a constante escondida sob a expressão Ο(n) é grande.

Exercícios 5

  1. Dado um vetor A[1 .. n] de números naturais sem elementos repetidos e um número natural k ≤ n, determinar os k elementos do vetor que estão mais próximos da mediana do vetor. Escreva um algoritmo que resolva o problema em tempo Ο(n).