• Nenhum resultado encontrado

Umalista encadeada ´e uma estrutura de dados linear onde cada elemento ´e armazenado em um n´o, que armazena tamb´emendere¸cos para outros n´os da lista. Por isso, cada n´o de uma lista pode estar em uma posi¸c˜ao diferente da mem´oria, sendo diferente de um vetor, onde os elementos s˜ao armazenados de forma cont´ınua. Na forma mais simples, tˆem-se acesso apenas ao primeiro n´o da lista. Em qualquer varia¸c˜ao, listas n˜ao permitem acesso direto a um elemento: para acessar o k-´esimo elemento da lista, deve-se acessar o primeiro, que d´a acesso ao segundo, que d´a acesso ao terceiro, e assim sucessivamente, at´e que o (k−1)-´esimo elemento d´a acesso ao k-´esimo.

Em uma lista duplamente encadeada L, cada n´o cont´em um atributo chave e dois ponteiros, anterior epr´oximo. Obviamente, cada elemento da lista pode conter outros atributos contendo mais dados. Aqui vamos sempre inserir, remover ou modificar elementos de uma lista baseados nos atributos chave, que sempre contˆem inteiros n˜ao negativos.

Dado um n´o xde uma lista duplamente encadeada, x.anterioraponta para o n´o que est´a imediatamente antes de x na lista e x.proximo aponta para o n´o que est´a imediatamente ap´osx na lista. Se x.anterior=null, ent˜ao xn˜ao tem predecessor, de modo que ´e o primeiro n´o da lista, a cabe¸ca da lista. Se x.proximo=null, ent˜ao

x n˜ao tem sucessor e ´e chamado decauda da lista, sendo o ´ultimo n´o da mesma. O atributo L.cabeca aponta para o primeiro n´o da lista L, sendo que L.cabeca =null

Figura 4.1: Lista duplamente encadeada circular. quando a lista est´a vazia.

Existem diversas varia¸c˜oes de listas al´em de listas duplamente encadeadas. Em umalista encadeada simples n˜ao existe o ponteiro anterior. Em uma lista circular, o ponteiro proximo da cauda aponta para a cabe¸ca da lista, enquanto o ponteiro

anteriorda cabe¸ca aponta para a cauda. A Figura 4.1mostra um exemplo de uma lista duplamente encadeada circular.

A seguir vamos descrever os procedimentos de busca, inser¸c˜ao e remo¸c˜ao em uma lista duplamente encadeada, n˜ao ordenada e n˜ao-circular.

O procedimento BuscaNaLista mostrado no Algoritmo 10 realiza uma busca pelo primeiro n´o que possui chave k na lista L. Primeiramente, a cabe¸ca da lista L´e analisada e em seguida os elementos da lista s˜ao analisados, um a um, at´e que k seja encontrado ou at´e que a lista seja completamente verificada. No pior caso, toda a lista deve ser verificada, de modo que o tempo de execu¸c˜ao de BuscaNaLista´eO(n) para uma lista com n elementos.

Algoritmo 10: BuscaNaLista(L, k)

1 x=L.cabeca

2 enquanto x6=null e x.chave 6=k fa¸ca

3 x=x.proximo

4 retorna x

A inser¸c˜ao ´e realizada sempre no come¸co da lista. No Algoritmo 11inserimos um n´ox na lista L. Portanto, caso L n˜ao seja vazia, o ponteiro x.proximodeve apontar para a atual cabe¸ca de L e L.cabeca.anterior deve apontar para x. Caso L seja vazia, ent˜ao x.proximo aponta para null. Como x ser´a a cabe¸ca de L, o ponteiro

x.anteriordeve apontar para null.

Algoritmo 11: InsereNaLista(L, x)

1 x.proximo=L.cabeca

2 se L.cabeca6=null ent˜ao

3 L.cabeca.anterior=x

4 L.cabeca=x

5 x.anterior=null

InsereNaLista´e executado em tempo Θ(1) para uma lista com n elementos. Note que o procedimento de inser¸c˜ao em uma lista encadeada ordenada levaria tempo O(n), pois precisar´ıamos inserir x na posi¸c˜ao correta dentro da lista, tendo que percorrer toda a lista no pior caso.

O Algoritmo 12mostra o procedimento RemoveDaLista, que remove um n´o x

de uma lista L. Note que o parˆametro passado para o procedimento n˜ao ´e um valor chave k, mas sim um ponteiro para um n´ox. Esse ponteiro pode ser encontrado, por exemplo, com uma chamada `aBuscaNaLista. A remo¸c˜ao ´e simples, sendo necess´ario somente atualizar os ponteirosx.anterior.proximoe x.proximo.anterior, e tendo cuidado com os casos onde x´e a cabe¸ca ou a cauda deL.

Algoritmo 12: RemoveDaLista(L, x)

1 se x.anterior6=null ent˜ao

2 x.anterior.proximo=x.proximo

3 sen˜ao

4 L.cabeca=x.proximo

5 se x.proximo6=null ent˜ao

6 x.proximo.anterior=x.anterior

Como somente uma quantidade constante de opera¸c˜oes ´e efetuada, a remo¸c˜ao leva tempo Θ(1) para ser executada. Por´em, se quisermos remover um elemento que cont´em uma dada chave k, precisamos primeiramente efetuar uma chamada ao algoritmo

BuscaNaLista(L, k) e ent˜ao remover o elemento retornado pela busca, gastando tempo Θ(n) no pior caso.

Observe que o fato do procedimento RemoveDaListater sido feito em uma lista

duplamente encadeada ´e essencial para que seu tempo de execu¸c˜ao seja Θ(1). Se L

na posi¸c˜ao anterior ax, dado que n˜ao existe x.anterior. Portanto, seria necess´ario uma busca por esse elemento, para podermos efetuar a remo¸c˜ao de x. Desse modo, um procedimento de remo¸c˜ao em uma lista encadeada simples leva tempo Θ(n) no pior caso.

Cap´ıtulo 5

´

Arvores

´

Arvores s˜ao, de certa forma, um conceito estendido de listas ligadas. S˜ao estruturas n˜ao lineares constitu´ıdas de n´os, onde cada n´o x cont´em um elemento armazenado emx.chave e pode ter um ou mais ponteiros para outros n´os. Mais especificamente, ´

arvores s˜ao estruturas hier´arquicas nas quais um n´o aponta para os n´os abaixo dele na hierarquia, chamados seus n´os filhos. Um n´o especial ´e a raiz, que ´e o topo da hierarquia e est´a presente no n´ıvel 0 da ´arvore. N´os filhos da raiz est˜ao no n´ıvel 1, os n´os filhos destes est˜ao no n´ıvel 2, e assim por diante. O n´ıvel de um n´o ´e definido formalmente como a menor quantidade de n´os que existem entre o n´o e a raiz. Um n´o sem filhos ´e chamado de folha da ´arvore. Veja na Figura 5.1 um exemplo de ´arvore e as devidas nomenclaturas. x y a b d z w c n´ıvel 3 n´ıvel 2 n´ıvel 1 n´ıvel 0

Figura 5.1: Exemplo de estrutura ´arvore com 4 n´ıveis e altura 3, onde: (i) x´e o n´o raiz (n´ıvel 0), (ii)y, z e ws˜ao filhos de x, (iii) y ´e pai de a e b, (iv)a, d, z ec s˜ao folhas.

Figura 5.2: ´Arvore bin´aria quase completa.

Em uma ´arvore, s´o temos acesso direto ao n´o raiz e qualquer manipula¸c˜ao, portanto, deve percorrer os ponteiros entre os n´os. Note ainda que existe um ´unico caminho entre a raiz e uma folha. A distˆancia do caminho raiz-folha mais longo, considerando todas as folhas, define aaltura da ´arvore. Equivalentemente, a altura de uma ´arvore ´e igual ao maior n´ıvel. Aaltura de um n´o xda ´arvore ´e definida como a menor quantidade de n´os existentes entre x e uma folha. De outra forma, a altura de x´e a altura da sub´arvore com raiz em x.

Considerando apenas essas informa¸c˜oes, vemos que qualquer busca deve ser feita percorrendo a ´arvore toda. Inser¸c˜oes e remo¸c˜oes n˜ao est˜ao bem definidas tamb´em. Assim, essencialmente, n˜ao ganhamos muita coisa com rela¸c˜ao a uma lista ligada.

O tipo mais comum de ´arvore, e que define melhor as opera¸c˜oes mencionadas, ´e a

´

arvore bin´aria. ´Arvores bin´arias s˜ao aquelas cujo maior n´umero de filhos de qualquer n´o ´e dois e, portanto, podemos distinguir os filhos entre direito e esquerdo. Elas tamb´em podem ser definidas recursivamente: ela ´e vazia ou ´e um n´o raiz que ´e pai de uma ´arvore bin´aria `a direita e de outra ´arvore bin´aria `a esquerda. Assim, tamb´em dizemos que o filho direito (resp. esquerdo) do n´o raiz ´e raiz da sub´arvore direita

(resp. esquerda). Formalmente, sex ´e um n´o, ent˜ao x cont´em os atributos x.chave,

x.direitae x.esquerda.

Uma ´arvore bin´aria ´e dita completa se todos os seus n´ıveis est˜ao completamente preenchidos. Note que ´arvores bin´arias completas com altura h possuem 2h+1−1 n´os. Uma ´arvore bin´aria com altura h ´e dita quase completa se os n´ıveis 0,1, . . . , h−1 tˆem todos os n´os poss´ıveis. Na Figura 5.2 temos um exemplo de uma ´arvore bin´aria quase completa.

5.1 Arvores bin´´ arias de busca

´

Arvores bin´arias de busca s˜ao ´arvores bin´arias especiais nas quais, para cada n´o x, todos os n´os da sub´arvore esquerda possuem chaves menores do que x.chave e todos os n´os da sub´arvore direita possuem chaves maiores do quex.chave. Essa propriedade ´e usada justamente para guiar a opera¸c˜ao de busca. Assim, se quisermos procurar um elemento k na ´arvore, primeiro o comparamos com a raiz: (i)k ´e igual `a chave da raiz e a busca termina, (ii)k ´e menor do que a chave da raiz e o problema se reduz a procurar k na sub´arvore esquerda, ou (iii) k ´e maior do que a chave da raiz e o problema se reduz a procurar k na sub´arvore direita. Note que o pior caso de uma busca ser´a percorrer um caminho raiz-folha inteiro, de forma que a busca pode levar tempo O(h), onde h´e a altura da ´arvore. Agora temos uma potencial melhora com rela¸c˜ao a listas ligadas: pode ser que a ´arvore tenha altura menor do que o n´umero n

de elementos armazenados nela.

Outras opera¸c˜oes poss´ıveis em ´arvores de busca que n˜ao alteram sua estrutura s˜ao:

Encontrar o menor elemento: basta seguir os filhos esquerdos a partir da raiz at´e chegar em um n´o que n˜ao tem filho esquerdo – este cont´em o menor elemento da ´

arvore. Tempo necess´ario: O(h).

Encontrar o maior elemento: basta seguir os filhos direitos a partir da raiz at´e chegar em um n´o que n˜ao tem filho direito – este cont´em o maior elemento da ´

arvore. Tempo necess´ario: O(h).

O sucessor de um elementok: ´e o menor elemento que ´e maior do que k. Seja x

o n´o tal que x.chave=k. Pela estrutura da ´arvore, sex tem um filho direito, ent˜ao o sucessor de k ´e o menor elemento armazenado nessa sub´arvore direita. Caso x n˜ao tenha filho direito, ent˜ao o primeiro n´o que cont´em um elemento maior do que k deve estar em um ancestral de x: ´e o n´o de menor chave cujo filho esquerdo tamb´em ´e ancestral de x. Tempo necess´ario: O(h)

O predecessor de um elementok: se x´e o n´o que cont´em k, o predecessor dek

´e o maior elemento da sub´arvore enraizada no filho esquerdo de x ou ent˜ao ´e o maior ancestral cujo filho direito tamb´em ´e ancestral de x. Tempo necess´ario:

30 17 4 20 18 90 60 45 37 97

Figura 5.3: Exemplo de ´arvore bin´aria de busca onde o sucessor de 30 ´e o 37 (menor n´o da sub´arvore enraizada em 90) e o sucessor de 20 ´e o 30 (menor ancestral do 20 cujo filho esquerdo, o 17, tamb´em ´e ancestral do 20).

Veja a Figura 5.3 para exemplos de elementos sucessores.

O Algoritmo13mostra o procedimentoInsereNaABB, que recebe a raizRde uma ´

arvore bin´aria de busca (ABB) e um novo n´o x e tenta inseri-lo na ´arvore, retornando o n´o raiz da ´arvore “nova”. Se a ´arvore est´a inicialmente vazia, ent˜ao o n´o x ser´a a nova raiz. Caso contr´ario, o primeiro passo do algoritmo ´e buscar por x.chave na ´

arvore. Se x.chave n˜ao estiver na ´arvore, ent˜ao a busca terminou em um n´o y que dever´a ser o pai dex: sex.chave< y.chave, ent˜ao inserimosx `a esquerda deye caso contr´ario o inserimos `a direita. Note que qualquer busca posterior por x.chave vai percorrer exatamente o mesmo caminho e chegar corretamente a x. Portanto, essa inser¸c˜ao mant´em a propriedade da ´arvore bin´aria de busca. N˜ao ´e dif´ıcil perceber que o tempo de execu¸c˜ao desse algoritmo tamb´em ´eO(h).

Algoritmo 13: InsereNaABB(R,x)

1 se R ==null ent˜ao

2 retorna x

3 se x.chave< R.chave ent˜ao

4 R.esquerda= InsereNaABB(R.esquerda, x)

5 se x.chave> R.chave ent˜ao

6 R.direita= InsereNaABB(R.direita, x)

7 retorna R

No caso de remo¸c˜oes, precisamos tomar alguns cuidados extras para garantir que a ´

90 60 45 37 97 60 45 37 90 97 60 45 37 90 97 45 37 97 60 90 37 45 60 90 97

Figura 5.4: Cinco exemplos de ´arvores formadas pela inser¸c˜ao dos elementos 37, 45, 60, 90 e 97 em diferentes ordens.

e basta removˆe-lo. Se o n´o a ser removido tem um ´unico filho, ent˜ao temos um caso simples tamb´em e basta substitu´ı-lo por esse filho. Agora, se o n´oxa ser removido tem dois filhos, precisamos substitu´ı-lo por algum outro n´o que tenha no m´aximo um filho e v´a manter a propriedade de busca. Um bom candidato para substituirx´e seu sucessor: todos os n´os `a esquerda dex tˆem elementos menores do que o sucessor de x e todos os n´os `a direita tˆem elementos maiores. Como o sucessor de x´e o n´o de menor chave da sub´arvore direita de x (pois x tem dois filhos) e o menor n´o de uma ´arvore tem no m´aximo um filho (`a direita), podemos de fato trocar o n´o sucessor com xe prosseguir removendo x, que passa a ter um ´unico filho. Note que o tempo de execu¸c˜ao dessa opera¸c˜ao depende basicamente da opera¸c˜ao que encontra o sucessor de um n´o (pois nos outros casos temos simples atualiza¸c˜oes de ponteiros), de forma que ela tamb´em leva tempo O(h).

Assim, buscar por um elemento, inserir um novo n´o, remover algum n´o, encontrar o k-´esimo menor elemento e encontrar o predecessor ou sucessor de um elemento s˜ao opera¸c˜oes que podem ser feitas em tempo O(h) em uma ´arvore bin´aria de busca, onde

h´e a altura da ´arvore.

Note agora que a inser¸c˜ao ´e feita “de qualquer forma”, apenas respeitando a propriedade de busca. Assim, a ´arvore gerada ap´os um certo n´umero n de inser¸c˜oes pode ter qualquer formato. Um mesmo conjunto de elementos, dependendo da ordem na qual s˜ao inseridos, pode dar origem a v´arias ´arvores diferentes, veja a Figura 5.4

Todas as opera¸c˜oes que mencionamos tˆem tempoO(h) e, como vimos na Figura5.4, uma ´arvore bin´aria de busca com n n´os pode ter altura h=n e, portanto, ser t˜ao ruim quanto uma lista ligada. Uma forma de melhorar os tempos de execu¸c˜ao das opera¸c˜oes, portanto, ´e garantir que a altura da ´arvore n˜ao seja t˜ao grande.