Tabelas dinâmicas

Uma tabela é dinâmica se que incha e encolhe, conforme a necessidade, ao longo da execução de um algoritmo.  Esta página trata de algoritmos de manipulação de tabelas dinâmicas.

A página é inspirada na seção 17.4 de CLRS.  Veja também seção 3.2 de MS.  Veja ainda o verbete Dynamic array na Wikipedia.

Introdução

Uma tabela dinâmica (= dynamic table = unbounded array) é uma vetor sujeito as expansões e contrações.   Nossa tabela dinâmica reside num vetor A[1 .. m .. t].  Diremos que t é o tamanho e m é a carga da tabela.  As m primeiras posições estão ocupados com dados relevantes e as demais estão livres.  Se m = 0, diremos que a tabela está vazia. Se m = t, a tabela está cheia.

1 m t
11 22 33 44 55 66 77 88 99

Os parâmetros A, t e m serão tratados como variáveis globais.  A variável A é um ponteiro (que aponta para o primeiro elemento do vetor).

Inserção

Considere inicialmente o comportamento da tabela sob a operação de inserção de novos objetos.  A operação de inserção de um objeto a pode ser escrita assim:

  Insere-na-Tabela (a)
11 _ se t = 0
12 ____ então AAloca-Tabela (1)
13 ____ então t ← 1
14 _ se m = t
15 ____ então BAloca-Tabela (2t)
16 ____ então para k crescendo de 1 até m faça
17 _______ então B[k] ← A[k]
18 ____ então Libera-Tabela (A)
19 ____ então AB
10 ____ então t ← 2t
11 _ mm + 1
12 _ A[m] ← a

A linha 2 aloca uma nova tabela de tamanho 1.  A linha 5 aloca uma nova tabela de tamanho 2t.  As linhas 5 a 10 produzem uma expansão da tabela;  a linha 7, em particular, realoca o elemento A[k].  Na linha 8, o espaço ocupado por A é liberado.  Depois da linha 9, o ponteiro global A passa a apontar para a nova tabela que acabamos de alocar e preencher. 

Digamos que cada execução de uma instrução do tipo  A[∗] ← ∗  ou  B[∗] ← ∗  custa 1 moeda.  (Desta forma, o consumo de tempo será proporcional ao custo em moedas.)  Cada execução de Insere-na-Tabela consome 1 moeda no melhor casom+1  moedas no pior.

Considere agora uma sequência de n execuções de Insere-na-Tabela, começando com uma tabela vazia:

21 _ tm ← 0
22 _ para i crescendo de 1 até n faça
23 ____a
24 ____ Insere-na-Tabela (a)

Imediatamente antes de cada execução da linha 24, a tabela tem carga m = i−1.  Portanto, a execução da linha 24 custa i moedas no pior caso.  Assim, o custo total das n execuções não passa de 1+2+⋅⋅⋅+n = (n²+n)/2 moedas.  Mas essa cota superior é exagerada: o custo total não passa de 3n moedas, como veremos a seguir.

Consumo amortizado

Antes de cada execução de Insere-na-Tabela a tabela possui um certo estoque de moedas (acumulado ao longo das iterações anteriores).  Para dar início à execução Insere-na-Tabela, é preciso acrescentar três moedas ao estoque.  Durante a execução, as atribuição nas linhas 7 e 12 consomem parte do estoque deixando um saldo que, como veremos, nunca é negativo.  (A ideia é que a primeira das três moedas seja usada para custear a atribuição A[m] ← a na linha 12, a segunda moeda seja usada futuramente para custear a realocação de A[m] e a terceira custeie futuramente a realocação de um dos elementos de A[1..m−1].)   Graças a esse protocolo, temos o seguinte invariante:

antes de cada execução de Insere-na-Tabela,  a tabela possui pelo menos 2m − t moedas em estoque.

Prova do invariante:  A propriedade vale antes da primeira execução de Insere-na-Tabela pois nessa ocasião m e t são nulos.  Suponha agora que temos pelo menos 2m − t moedas no início de uma execução qualquer de Insere-na-Tabela.  Há três casos a considerar:

  1. Se t = 0 então m = 0 e 2m − t = 0.  No fim da execução do algoritmo temos m = t = 1, donde 2m − t = 1, e o estoque tem 2 moedas.  Assim, a propriedade continua valendo antes da próxima execução de Insere-na-Tabela.
  2. Suponha agora que t > 0 e m < t.  No fim da execução do algoritmo, m terá aumentado em 1 unidade e t terá permanecido constante, donde o valor da expressão 2m − t terá aumentado em 2 unidades.  O estoque de moedas também terá aumentado em 2 unidades.  Assim, continuaremos tendo pelo menos 2m − t moedas antes da próxima execução de Insere-na-Tabela.
  3. Suponha, finalmente, que t > 0 e m = t.  Nesse caso, o estoque tem pelo menos 2m − t = m moedas.  Acrescentamos três novas moedas ao estoque.  Depois das m repetições da linha 7, o estoque tem pelo menos três moedas.  A linha 12 consome uma moeda, deixando pelo menos duas em estoque.  Por outro lado, m aumenta em 1 e t, que valia m, dobra.  Assim, 2m − t passa a valer 2.  Com isso, continuaremos tendo pelo menos 2m − t moedas antes da próxima execução de Insere-na-Tabela.

Isso encerra a prova.   O invariante mostra que toda vez m = t o estoque tem pelo menos m moedas, o que é suficiente para custear a expansão da tabela (linhas 6 a 7).  Concluímos assim que a sequência de n execuções de Insere-na-Tabela apenas

3n  moedas.

Segue daí que as n execuções de Insere-na-Tabela consomem  Ο(n)  unidades de tempo.   Essa conclusão pode ser resumida dizendo que, no sentido amortizado, cada execução de Insere-na-Tabela na linha 24 consome  Ο(1)  unidades de tempo, ou seja, uma quantidade de tempo que não depende de n.

Exercícios

  1. Suponha que Insere-na-Tabela é invocado com t/2 ≤ m ≤ t.  Mostre que no fim da execução do algoritmo teremos  t/2 < m ≤ t.
  2. Mostre que no início de cada execução da linha 24 temos t = 0 ou t = 2p para algum número natural p.
  3. Troque "2t" por "t+2" nas linhas 5 e 10.  Qual o consumo de tempo do fragmento de código nas linhas 22 a 24?
  4. Troque "2t" por "3t" nas linhas 5 e 10.  Qual o consumo de tempo do fragmento de código nas linhas 22 a 24?

Remoção

Considere agora o comportamento da tabela sob a operação de remoção (= delete) de elementos.  Gostaríamos que a carga m da tabela nunca fosse menor que uma certa fração de t,  digamos t/4.  A operação de remoção de um elemento pode, então, ser descrita como segue:

  Remove-da-Tabela ( )
11 _ se mt/4
12 ____ então BAloca-Tabela (t/2)
13 ____ então para k crescendo de 1 até m faça
14 _______ então B[k] ← A[k]
15 ____ então Libera-Tabela (A)
16 ____ então AB
17 ____ então tt/2
18 _ aA[m]
19 _ mm − 1
10 _ se m = 0
11 ____ então Libera-Tabela (A)
12 ____ então t ← 0
13 _ devolva a

As linhas 2 a 7 produzem uma contração da tabela;  a linha 4, em particular, realoca o elemento A[k].  Na linha 8, o espaço ocupado por A é liberado.  Depois da linha 9, o ponteiro global A passa a apontar para a nova tabela que acabamos de alocar e preencher. 

Suporemos que em todas as invocações do algoritmo valem as seguintes condições:

Graças a estas condições, o algoritmo nunca será invocado com m = 0.  Além disso, o valor da expressão "t/2" nas linhas 2 e 7 será uma potência de 2 (em particular, será inteiro).

A ideia do "t/4" e "t/2" é que depois de uma contração a carga deve dobrar antes que uma expansão se faça necessária (e assim o número de elementos inseridos será suficiente para "pagar" pela expansão).  Por outro lado, depois de uma expansão, a carga precisa ser reduzida à metade antes que ocorra uma contração (assim, o número de elementos removidos será suficiente para "pagar" pela contração).

Exercícios

  1. Examine o funcionamento de Remove-da-Tabela no caso em que t = 1 (e portanto m = 1).  Examine o funcionamento de Remove-da-Tabela no caso em que t = 2 (e portanto 1 ≤ m ≤ 2).  Examine o funcionamento de Remove-da-Tabela no caso em que t = 3.  Examine o funcionamento de Remove-da-Tabela no caso em que t = 4. 
  2. Suponha que t > 0 e m ≥ t/4 no momento em que Remove-da-Tabela é invocado.  Mostre que m ≥ t/4 depois da execução de Remove-da-Tabela.
  3. Suponha que t é uma potência inteira de 2 antes da execução de Remove-da-Tabela.  Mostre que t continua sendo uma potência inteira de 2 depois da execução de Remove-da-Tabela.

Sequência de inserções e remoções

Imagine uma sequência arbitrária de n de inserções e remoções partindo de uma tabela vazia.  Isso pode ser descirto assim:

31 _ tm ← 0
32 _ para i crescendo de 1 até n faça
33 ____ se m > 0 e condição
34 _______ então aRemove-da-Tabela ( )
35 _______ então imprime a
36 _______ senão lê a
37 _______ senão Insere-na-Tabela (a)

A condição booleana na linha 33 é arbitrária e portanto, para todos os efeitos, a sequência de inserções e remoções é arbitrária (exceto pela restrição m > 0 que precede toda remoção).

No início de cada execução da linha 33, t é nulo ou uma potência inteira de 2.  Além disso, t/4 ≤ m ≤ t

Digamos que cada execução de uma instrução do tipo  A[∗] ← ∗  ou  B[∗] ← ∗  em Insere-na-Tabela e Remove-da-Tabela custa 1 moeda.  (Desta forma, o consumo de tempo será proporcional ao custo em moedas.)  Qual o custo total do processo iterativo nas linhas 32 a 37 no pior caso?

Para fazer a contabilidade, vamos estabelecer o seguinte protocolo.  No início de cada iteração, a tabela possui um certo estoque de moedas.  Para dar início a qualquer execução de Remove-da-Tabela, é preciso acrescentar duas moedas ao estoque.  Para dar início a qualquer execução de Insere-na-Tabela é preciso acrescentar três moedas ao estoque.  Durante a execução de qualquer desses dois algoritmos, as atribuição A[∗] ← ∗ e B[∗] ← ∗ consomem parte do estoque deixando um saldo que, como veremos, nunca é negativo.  Com esse protocolo, teremos o seguinte invariante:  no início de cada iteração, o número de moedas em estoque é

(Prove o invariante.)  Com isso, teremos pelo menos m moedas em estoque para custear a expansão da tabela sempre que isso se fizer necessário.

Fica claro agora que o custo total das n iterações não passa de

3n  moedas.

Segue daí que as n execuções de Insere-na-Tabela e Remove-da-Tabela consomem  Ο(n)  unidades de tempo.   Essa conclusão pode ser resumida dizendo que, no sentido amortizado, cada execução de Insere-na-Tabela ou Remove-da-Tabela consome  Ο(1)  unidades de tempo.

Exercícios

  1. Mostre que no início de cada execução da linha 33 o valor de t pertence ao conjunto {0,1,2,4,8,16,…,2p,… }.
  2. Mostre que  2m − t = t/2 − m  quando m = t/2.
  3. Defina f(m) assim: se m ≥ t/2 então f(m) = 2m − t e se m < t/2 então f(m) = t/2 − m.  Faça um gráfico da função f.
  4. [Importante]  Prove o invariante do processo iterativo nas linhas 32 a 37.
  5. [Importante]  Troque "t/4" por "t/2" na linha 1 de Remove-da-Tabela.  Qual o efeito sobre o consumo de tempo do fragmento de código nas linhas 32 a 37?

Valid HTML 4.01 Strict Valid CSS!