Home RecentChanges

Lesson21

Ainda que em situações ideais uma árvore binária possa garantir tempo $O(\log n)$ para as operações usuais sobre mapas associativos, existem casos em que tal não acontece com tempos de acesso $O(n)$ no pior caso. Estes casos surgem em geral quando as árvores binárias não estão balanceadas ou equilibradas, com o pior caso a ocorrer quando estão degeneradas. Para percebermos o que é uma árvore binária degenerada basta pensar no que acontece se inserirmos elementos com chaves ordenadas por ordem crescente (ou decrescente). Qual é o tempo de inserção e de pesquisa nesse caso?

NOTA: No que se segue, dada uma árvore binária, pressupõe-se que:

Por forma a garantir o tempo $O(\log n)$ queremos por norma manter as árvores equilibradas, em particular com recurso a árvores auto-equilibradas, em que se procura minimizar a altura das mesmas enquanto sujeitas a operações de inserção e remoção.

Uma árvore diz-se perfeitamente equilibrada se, para qualquer nó da mesma, a diferença entre o número de nós das sub-árvores é no máximo 1. Neste caso a altura da árvore é mínima em relação ao número de nós da mesma.

No entanto, manter uma árvore perfeitamente equilibrada implica um custo elevado nas operações, pelo que vamos discutir algumas estratégias que procuram minimizar a altura e garantir o tempo $O(\log n)$, sem no entanto manter as árvores perfeitamente equilibradas. Estas estratégias admitem relaxações à noção de árvore equilibrada. Em particular, no que se segue, vamos dizer que uma árvore está equilibrada quando a diferença de altura das sub-árvores de qualquer nó é no máximo 1.


Operações sobre árvores binárias

No que segue vamos assumir que cada nó numa árvore binária é representado da seguinte forma:

struct STnode {
  Item item;
  link l, r;
  int N
};

O novo campo N indica o número de nós na sub-árvore cuja raiz é o nó actual.

Considere-se ainda a seguinte interface para o ADT:

void STinit(int);
int  STcount();
Item STsearch(Key v);
void STinsert(Item item);
Item STselect(int);
void STdelete(Item);
void STsort(void (*visit)());

E em que temos as seguintes operações básicas implementadas:

#include <stdlib.h>
#include "Item.h"

typedef struct STnode* link;

struct STnode {
  Item item;
  link l, r;
  int N };
static link head, z;

link NEW(Item item, link l, link r, int N) {
  link x = malloc(sizeof(struct STnode));
  x->item = item;
  x->l = l;
  x->r = r;
  x->N = N;
  return x;
}

void STinit() {
  head = (z = NEW(NULLitem, NULL, NULL, 0));
}

int STcount() {
   return head->N;
}

Item searchR(link h, Key v) {
  Key t = key(h->item);
  if (h == z)
    return NULLitem;
  if eq(v, t)
    return h->item;
  if less(v, t)
    return searchR(h->l, v);
  else
    return searchR(h->r, v);
}

Item STsearch(Key v) {
  return searchR(head, v);
}

link insertR(link h, Item item) {
  Key v = key(item), t = key(h->item);
  if (h == z) return NEW(item, z, z, 1);
  if less(v, t)
    h->l = insertR(h->l, item);
  else
    h->r = insertR(h->r, item);
  (h->N)++;
  return h;
}

void STinsert(Item item) {
  head = insertR(head, item);
}

Operações de rotação

Rotação à esquerda:

Lesson21rotL.png

link rotL(link h) {
  link x = h->r;
  h->r = x->l;
  x->l = h;
  x->l->N = x->l->r->N + x->l->l->N + 1;
  x->N = x->l->N + x->r->N + 1;
  return x;
}

Rotação à direita:

Lesson21rotR.png

link rotR(link h) {
  link x = h->l;
  h->l = x->r;
  x->r = h;
  x->r->N = x->r->r->N + x->r->l->N + 1;
  x->N = x->l->N + x->r->N + 1;
  return x;
}

Inserção na raiz

Insere novo nó numa folha, mas utiliza rotações para colocar o novo nó na raiz da árvore. Esta abordagem é inspirada nas splay trees.

link insertT(link h, Item item) {
  Key v = key(item);
  if (h == z)
    return NEW(item, z, z, 1);
  if (less(v, key(h->item))) {
    h->l = insertT(h->l, item);
    h = rotR(h);
  } else {
    h->r = insertT(h->r, item);
    h = rotL(h);
  }
  return h;
}

Selecção

Neste caso queremos seleccionar a $k$-ésima chave. Por exemplo, obter a quinta chave:

Lesson21select.png

Item selectR(link h, int k) {
  int t = h->l->N;
  if (h == z)
    return NULLitem;
  if (t > k)
    return selectR(h->l, k);
  if (t < k)
    return selectR(h->r, k-t-1);
  return h->item;
}

Item STselect(int k) {
  return selectR(head, k);
}

Partição

Neste caso queremos seleccionar a $k$-ésima chave e colocá-la na raiz. Exemplo:

Passo 1:

Lesson21part1.png

Passo 2:

Lesson21part2.png

Passo 3:

Lesson21part3.png

link partR(link h, int k)
{
  int t = h->l->N;
  if (t > k) {
    h->l = partR(h->l, k);
    h = rotR(h);
  }
  if (t < k) {
    h->r = partR(h->r, k-t-1);
    h = rotL(h);
  }
  return h;
}

Remoção

Passo 1:

Lesson21delete1.png

Passo 2:

Lesson21delete2.png

Passo 3:

Lesson21delete3.png

link deleteR(link h, Key v)
{
  link x;
  Key t = key(h->item);
  if (h == z)
    return z;
  if (less(v, t))
    h->l = deleteR(h->l, v);
  if (less(t, v))
    h->r = deleteR(h->r, v);
  if (eq(v, t)) {
    x = h;
    h = joinLR(h->l, h->r);
    free(x);
  }
  return h;
}

link joinLR(link a, link b)
{
  if (b == z)
    return a;
  b = partR(b, 0);
  b->l = a;
  return b;
}

void STdelete(Key v)
{
  head = deleteR(head, v);
}

Ordenação

Neste caso utilizamos a travessia in-order para obter uma ordenação pelas chaves.

void sortR(link h, void (*visit)(Item))
{
  if (h == z)
    return;
  sortR(h->l, visit);
  visit(h->item);
  sortR(h->r, visit);
}

void STsort(link h, void (*visit)(Item))
{
  sortR(head, visit);
}

Equilibrar árvores

Vamos utilizar a operação de partição para colocar a mediana na raiz e equilibrar a árvore recursivamente.

link balanceR(link h)
{
  if (h->N < 2)
    return h;
  h = partR(h, h->N/2);
  h->l = balanceR(h->l);
  h->r = balanceR(h->r);
  return h;
}

Inserção randomizada

Se as chaves forem uniformemente distribuídas, um novo nó fica na raíz com probabilidade $1/ (1 + N )$. Portanto, neste caso, quando inserimos um nó, vamos colocá-lo na raiz com probabilidade $1/ (1 + N )$.

link insertR(link h, Item item)
{
  Key v = key(item)
  Key t = key(h->item);
  if (h == z)
    return NEW(item, z, z, 1);
  if (rand()< RAND_MAX/(h-N+1))
    return insertT(h, item);
  if less(v, t)
    h->l = insertR(h->l, item);
  else
    h->r = insertR(h->r, item);
  (h->N)++;
  return h;
}

void STinsert(Item item)
{
  head = insertR(head, item);
}