• No se han encontrado resultados

El TAD Dlink (enlace doble)

In document Tejiendo Algoritmos RUTA Critica (página 93-104)

1.4 Diseño de datos y abstracciones

2.4.7 El TAD Dlink (enlace doble)

En el mismo espíritu de diseño que el TAD Slink, el TAD Dlink dene un doble enlace contenido en un nodo perteneciente a una lista doblemente enlazada circular con nodo cabecera.

Dlink se especica e implanta en el archivo hdlink.H73ai que se estructura del siguiente modo:

73a hdlink.H 73ai≡

class Dlink {

hmiembros protegidos de Dlink 73bi hmiembros públicos de Dlink 73ci

};

hmacros conversión de Dlink a clase(never dened)i

A partir de este momento es muy importante puntualizar y distinguir los siguientes nes:

1. El n de un Dlink es fungir de nodo cabecera de una lista circular doblemente en- lazada. En este sentido, las operaciones de la clase Dlink reeren a listas doblemente enlazadas, circulares, cuyo nodo cabecera es this.

2. El n de un Dlink* es fungir de apuntador a un nodo perteneciente a una lista circular doblemente enlazada.

Dlink posee dos atributos protegidos:

73b hmiembros protegidos de Dlink 73bi≡ (73a)

mutable Dlink * prev; mutable Dlink * next;

next y prev son apuntadores al sucesor y predecesor de this.

Un nuevo Dlink siempre se crea con sus lazos apuntando a sí mismo; es decir, como un nodo cabecera cuya lista está vacía:

73c hmiembros públicos de Dlink 73ci≡ (73a) 73d .

Dlink() : prev(this), next(this) {}

Eventualmente, aunque delicado, es conveniente exportar interfaces para copias y asig- naciones:

73d hmiembros públicos de Dlink 73ci+ (73a) / 73c 73e .

Dlink(const Dlink &) { reset(); }

Dlink & operator = (const Dlink & l) {

reset(); return *this; }

En realidad, las dos operaciones no efectúan copia; se exportan para satisfacer operaciones con variables temporales usadas por el compilador. No es posible hacer copia a este nivel porque no se maneja ningún mecanismo de asignación de memoria ni se conoce el tipo de dato que albergan los nodos.

Un doble enlace puede reiniciarse mediante:

73e hmiembros públicos de Dlink 73ci+ (73a) / 73d 74 .

void reset() {

next = prev = this; }

reset() reinicia una lista a que apunte a sí mismo. Esto no es equivalente a eliminar los nodos atados a this.

Aunque no es posible asignar sobre una lista que contenga elementos, sí lo es inter- cambiar los contenidos de dos listas doblemente enlazadas en tiempo constante. Por esta razón se ofrece la primitiva swap() cuya implantación es como sigue:

74 hmiembros públicos de Dlink 73ci+ (73a) / 73e 75a .

void swap(Dlink * link) {

if (is_empty() and link->is_empty()) return; if (is_empty()) { link->next->prev = this; link->prev->next = this; next = link->next; prev = link->prev; link->reset(); return; } if (link->is_empty()) { next->prev = link; prev->next = link; link->next = next; link->prev = prev; reset(); return; } std::swap(prev->next, link->prev->next); std::swap(next->prev, link->next->prev); std::swap(prev, link->prev); std::swap(next, link->next); } link this

swap swap swap swap

swap() es una gran operación, pues aparte de que su tiempo de ejecución es espectac- ular, pues es constante, es general para todas las listas doblemente enlazadas con nodo cabecera, sin importar ni el tipo de dato que se maneje ni cómo se reserve la memoria. La gura 2.9 ilustra los nodos de las listas en los cuales se llevan a cabo los intercambios.

Asumiendo que this es el nodo cabecera de una lista podemos saber si la lista está vacía o no si contiene un solo elemento o si su cardinalidad es menor o igual a uno:

75a hmiembros públicos de Dlink 73ci+≡ (73a) / 74 75b .

bool is_empty() const { return this == next and this == prev; }

bool is_unitarian() const { return this != next and next == prev; }

bool is_unitarian_or_empty() const { return next == prev; }

Un punto a destacar de estas operaciones es que no requieren contabilizar la cantidad de nodos de la lista.

La gura 2.10 enumera los diferentes pasos involucrados en la inserción, los cuales se efectúan en la rutina insert(), la cual inserta a node como el sucesor de this:

75b hmiembros públicos de Dlink 73ci+ (73a) / 75a 75c .

void insert(Dlink * node) { node->prev = this; node->next = next; next->prev = node; next = node; } C D A B this X node

4: 3:next->prev = node; node->prev = this;

node->next = next; next = node;

1:

2:

Figura 2.10: Inserción en una lista doblemente enlazada implantada con Dlink La inserción como predecesor se denomina append() y se dene como sigue:

75c hmiembros públicos de Dlink 73ci+ (73a) / 75b 76a .

void append(Dlink * node) { node->next = this; node->prev = prev; prev->next = node; prev = node; }

Puesto que la lista es circular, insert() desde el nodo cabecera inserta un nodo al principio de la lista. Análogamente, append() los inserta al nal.

Dada la dirección de un nodo podemos acceder a su sucesor y predecesor mediante los siguientes métodos:

76a hmiembros públicos de Dlink 73ci+ (73a) / 75c 76b .

Dlink *& get_next() {

return next; }

Dlink *& get_prev() {

return prev; }

head

this

(a) Antes de ejecutar insert_list(head)

head

this

(b) Después de ejecutar insert_list(head) Figura 2.11: Inserción de lista dentro de una lista

Existen circunstancias en las cuales se requiere insertar una lista completa dentro de otra a partir de uno de sus nodos. Para ello utilizamos las primitivas insert_list() y append_list(). La gura 2.11 muestra el proceso de ejecución de insert_list(head). Después su ejecución, la lista cuyo nodo cabecera estaba apuntado por head deviene vacía, pues sus nodos fueron incluidos en this. Es muy importante notar que en este caso this no necesariamente es un nodo cabecera, sino que puede ser cualquier otro nodo. Las implementaciones son como sigue:

76b hmiembros públicos de Dlink 73ci+ (73a) / 76a 77a .

void insert_list(Dlink * head) { if (head->is_empty()) return; head->prev->next = next; head->next->prev = this; next->prev = head->prev;

next = head->next; head->reset();

}

void append_list(Dlink * head) { if (head->is_empty()) return; head->next->prev = prev; head->prev->next = this; prev->next = head->next; prev = head->prev; head->reset(); }

En algunos contextos, las operaciones insert_list() y append_list() se conocen como splice13.

Un caso particular, quizá mucho más común que el splice, es la operación de concatenar listas concat_list(head), la cual concatena la lista cuyo nodo cabecera es head con this. head deviene vacía después de la operación. En este caso, sí se asume que this es nodo cabecera. Al respecto, se plantea la siguiente implementación:

77a hmiembros públicos de Dlink 73ci+≡ (73a) / 76b 77b .

void concat_list(Dlink * head) { if (head->is_empty()) return; if (this->is_empty()) { swap(head); return; } prev->next = head->next; head->next->prev = prev; prev = head->prev; head->prev->next = this; head->reset(); }

Dada la dirección de un nodo hay varias maneras de invocar una eliminación. La más útil de todas, denominada autoeliminación, se efectúa mediante del(). La rutina es muy útil porque permite que otras estructuras de datos almacenen referencias eliminables a elementos de una lista enlazada. En otras palabras, un nodo cualquiera puede suprimirse a sí mismo de la lista. Su implementación es como sigue:

77b hmiembros públicos de Dlink 73ci+ (73a) / 77a 78a .

void del() {

prev->next = next;

13En inglés, este término se utiliza cuando se desea expresar que dos cosas se pegan, se juntan, por sus

extremos -una punta con la otra-. En la opinión de este redactor, el equivalente castellano más próximo es enlazar, cuyo uso plantea una ambigüedad en la jerga de listas enlazadas. Por esa razón, continuaremos utilizando el término en inglés.

next->prev = prev; reset();

}

Los pasos de del() se muestran en la gura 2.12.

C D A B this 1: 2: 3:

prev->next = next;

next->prev = prev; reset();

Figura 2.12: Autoeliminación en una lista doblemente enlazada implantada con Dlink Dado un nodo hay otras dos maneras de eliminar: su predecesor o su sucesor, las cuales se implantan mediante los siguientes métodos:

78a hmiembros públicos de Dlink 73ci+≡ (73a) / 77b 78b .

Dlink * remove_prev() {

Dlink* retValue = prev; retValue->del();

return retValue; }

Dlink * remove_next() {

Dlink* retValue = next; retValue->del();

return retValue; }

remove_prev() suprime el predecesor respecto a this; remove_next() suprime el suce- sor. Estas primitivas son particularmente útiles para el nodo cabecera de la lista. Puesto que la lista es circular, remove_prev(), invocada desde el nodo cabecera, suprime el último nodo de la lista; similarmente, remove_next() suprime el primero.

Una aplicación directa de algunos de los métodos explicados se ejemplica mediante una rutina de inversión de nodos de la lista:

78b hmiembros públicos de Dlink 73ci+ (73a) / 78a 79a .

size_t reverse_list() {

if (is_empty()) return 0;

Dlink tmp; // cabecera temporal donde se guarda lista invertida

// recorrer lista this, eliminar primero e insertar en tmp size_t counter = 0;

for (/* nada */; not is_empty(); counter++)

tmp.insert(remove_next()); // eliminar e insertar en tmp

return counter; }

Aparte de invertir la lista, reverse_list() aprovecha el recorrido para contar la can- tidad de nodos; cantidad que retorna la función.

Dada una lista, ¾cómo partirla por el centro en dos listas del mismo tamaño? Un truco consiste en avanzar dos apuntadores. Por cada iteración, un puntero avanza un paso,

mientras que el otro avanza dos14. Cuando el segundo puntero se encuentre al nal de la

lista, el primero se encontrará en el centro; este es el punto de partición. Este enfoque, es el más adecuado para particionar una lista simple, pero debe programarse cuidadosamente los casos extremos de cero, uno o dos elementos.

Para listas doblemente enlazadas existe un enfoque mucho más simple y, como todo lo simple, más conable: recorrer la lista por cada extremo. Por el lado izquierdo se recorre hacia la derecha; por el derecho hacia la izquierda. En cada iteración se elimina de cada extremo y se inserta en cada una de las listas resultado. Para ello diseñamos el método split_list(l, r), el cual recibe dos cabeceras de listas vacías l y r y particiona a this por el centro en dos partes, la izquierda en l y la derecha en r:

79a hmiembros públicos de Dlink 73ci+ (73a) / 78b 79b .

size_t split_list(Dlink & l, Dlink & r) {

size_t count = 0; while (not is_empty())

{ l.append(remove_next()); ++count; if (is_empty()) break; r.insert(remove_prev()); ++count; } return count; }

Dada una lista y uno de sus nodos, puede ser conveniente particionarla en un nodo dado. Para ello, se provee la función cut_list() cuya implantación es como sigue:

79b hmiembros públicos de Dlink 73ci+ (73a) / 79a 80a .

void cut_list(Dlink * link, Dlink * list) {

list->prev = prev; // enlazar list a link (punto de corte) list->next = link;

prev = link->prev; // quitar de this todo a partir de link link->prev->next = this;

link->prev = list; // colocar el corte en list list->prev->next = list;

}

El esquema de este algoritmo se ilustra en la gura 2.13. cut_list() particiona this en el nodo cuya dirección es link, el cual debe pertenecer a la lista this. Los nodos a la izquierda de link se preservan en this, mientras que los restantes, incluido link, se copian a list.

A B C D E link

list this

Figura 2.13: Corte de una lista en un nodo dado

Los usos de Dlink son casi los mismos que el de Slink desarrollado en la subsección Ÿ 2.4.2 (Pág. 65). Podemos hacer a una clase ser un Dlink por derivación pública, o hacerla parte de un nodo doble por declaración de un Dlink como atributo de la clase. La diferencia esencial respecto a Slink es su versatilidad, expresada por la riqueza de las operaciones que hemos estudiado, las cuales serían más difíciles de desarrollar que las operaciones de Slink.

Al igual que con Slink, para situaciones en que se usan registros que contengan Dlink se requieren macros que generan funciones de conversión de un Dlink hacia una clase que contenga un atributo Dlink. En este sentido hay dos posibilidades probables, aunque no generales: conversión hacia una clase simple o conversión hacia una clase parametrizada. Estas posibilidades se engloban en los macros DLINK_TO_TYPE(type_name, link_name) y LINKNAME_TO_TYPE(type_name, link_name), los cuales generan una función de conver- sión que convierte un puntero link de tipo Dlink en un puntero a una clase que contiene el Dlink.

El macro DLINK_TO_TYPE genera una función general con nombre dlink_to_type(). El macro LINKNAME_TO_TYPE recibe un parámetro llamado link_name, el cual debería corresponder exactamente con el nombre del campo dentro de la clase. La razón de ser de esta función es que permite al usuario distintos nombres de funciones para distintos nombres de campos; esto es indispensable en los casos en que la clase contenga dos o más enlaces dobles. Los macros DLINK_TO_TYPE y LINKNAME_TO_TYPE deben utilizarse dentro de la clase que use el tipo Dlink.

Iterador de Dlink

Dlink exporta un iterador (ver Ÿ 2.3 (Pág. 59)) cuyo esquema se presenta como sigue:

80a hmiembros públicos de Dlink 73ci+ (73a) / 79b 82c .

class Iterator {

hatributos Dlink::Iterator 80bi

hmiembros públicos iterador Dlink::Iterator 80ci

};

Para mantener el estado del iterador requerimos dos miembros:

80b hatributos Dlink::Iterator 80bi≡ (80a)

mutable Dlink * head; mutable Dlink * curr;

head es el nodo cabecera de la lista. curr es el nodo actual del iterador. Hay varias maneras de construir un iterador:

Iterator(Dlink * head_ptr) : head(head_ptr), curr(head->get_next()) {}

Iterator(const Iterator & itor) : head(itor.head), curr(itor.curr) {}

Un iterador puede asignarse:

81a hmiembros públicos iterador Dlink::Iterator 80ci+≡ (80a) / 80c 81b .

Iterator & operator = (const Iterator & itor) {

head = itor.head; curr = itor.curr; return *this; }

Un iterador puede reutilizarse, lo que requiere funciones de reiniciación: 81b hmiembros públicos iterador Dlink::Iterator 80ci+ (80a) / 81a 81c .

void reset_first() { curr = head->get_next(); } void reset_last() { curr = head->get_prev(); }

reset_first() posiciona el iterador sobre el primer elemento. reset_last() posiciona el iterador sobre el último elemento.

Existen situaciones especiales en las cuales se desea inicializar un iterador a partir de un elemento actual ya conocido:

81c hmiembros públicos iterador Dlink::Iterator 80ci+≡ (80a) / 81b 81d .

void set(Dlink * new_curr) {

curr = new_curr; }

void reset(Dlink * new_head) {

head = new_head;

curr = head->get_next();; }

set() sitúa el iterador a un nuevo nodo actual, mientras reset() sitúa el iterador a una nueva lista.

El elemento actual del iterador se accede y se verica mediante:

81d hmiembros públicos iterador Dlink::Iterator 80ci+ (80a) / 81c 82a .

bool has_current() const {

return curr != head; }

Dlink * get_current() {

return curr; }

bool is_in_first() const { return curr == head->next; }

Para avanzar el iterador utilizamos las siguientes primitivas:

82a hmiembros públicos iterador Dlink::Iterator 80ci+≡ (80a) / 81d 82b .

void prev() { curr = curr->get_prev(); } void next() { curr = curr->get_next(); }

A menudo pueden combinarse iteradores en un for que semejan la iteración sobre un arreglo. Como una lista no tiene acceso directo, la condición de iteración no debe ser una comparación por posición del tipo i < n. En una lista y, en general, para secuencias que se manipulen con iteradores, la comparación debe ser entre iteradores. Puesto que no se puede conocer la posición dentro de la secuencia para todas las situaciones, no es posible emplear los comparadores relacionales <, <=, >, >=, pero sí se pueden sobrecargar los operadores == y != tal como en efecto lo están en la biblioteca.

Un estilo tradicional de uso de la comparación entre iteradores se ilustra en este ejem- plo:

for (Dlink::Iterator curr(list); curr != end; curr.next())

Donde end es un iterador sobre la lista list apuntando al nal. Este es el estilo de la biblioteca estándar stdc++. end es equivalente a:

Dlink::Iterator end(list); list.reset_last();

list.next();

Es posible insertar respecto al elemento actual del iterador. Para ello basta con invocar sobre el elemento actual cualquiera de las primitivas de inserción insert() o append().

Hay situaciones en las que se requiere eliminar el elemento actual del iterador. En este caso no es posible efectuar del() sobre el nodo actual, pues podría perderse el estado del iterador. Para solventar esta situación, la clase Dlink::Iterator exporta una función de eliminación sobre el nodo actual que deja al iterador en el nodo sucesor del eliminado: 82b hmiembros públicos iterador Dlink::Iterator 80ci+ (80a) / 82a

Dlink * del() {

Dlink * current = get_current(); // obtener nodo actual next(); // avanzar al siguiente nodo

current->del(); // eliminar de la lista antiguo nodo actual return current;

}

del() retorna el enlace eliminado de manera tal que el cliente pueda disponer de él en la forma que preera.

Como ejemplo de uso de Dlink::Iterator consideremos el siguiente método:

82c hmiembros públicos de Dlink 73ci+≡ (73a) / 80a

void remove_all_and_delete() {

for (Iterator itor(this); itor.has_current(); delete itor.del()) ; }

Este método elimina todos los nodos y asume que la memoria de cada nodo fue asignada mediante new.

In document Tejiendo Algoritmos RUTA Critica (página 93-104)