Next Up Previous Hi Index

Chapter 17

Listas enlazadas

17.1 Referencias incrustadas

Hemos visto ejemplos de atributos que hacen referencia a otros objetos, a los que llamamos referencias incrustadas (v�ase la Secci�n 12.8). Una estrutura de datos com�n, la lista enlazada, saca partido de esta caracter�stica.

Las listas enlazadas se componen de nodos, donde cada nodo contiene una referencia al pr�ximo nodo de la lista. Adem�s, cada nodo contiene una unidad de datos llamada carga

Podemos considerar una lista enlazada como una estructura de datos recursiva porque tiene una definici�n recursiva.

Una lista enlazada puede ser:

Las estructuras recursivas de datos nos llevan a m�todos recursivos.

17.2 La clase Nodo

Como es habitual cuando se escribe una clase, comenzaremos con los m�todos de inicializaci�n y __str__, para poder comprobar el mecanismo b�sico de crear y mostrar el nuevo tipo:

class Nodo:
  def __init__(self, carga=None, siguiente=None):
    self.carga = carga
    self.siguiente = siguiente

  def __str__(self):
    return str(self.carga)

Como es habitual, los par�metros para el m�todo de inicializaci�n son opcionales. Por defecto, la carga y el enlace, siguiente, se ponen a None.

La representaci�n alfanum�rica de un nodo es �nicamente la de la carga. Como se puede pasar cualquier valor a la funci�n str, podemos guardar cualquier valor en una lista.

Para comprobar la implementaci�n en este punto, podemos crear un Nodoe imprimirlo:

>>> nodo = Nodo("prueba")
>>> print nodo
prueba

Para hacerlo m�s interesante, necesitaremos una lista que contenga m�s de un nodo:

>>> nodo1 = Nodo(1)
>>> nodo2 = Nodo(2)
>>> nodo3 = Nodo(3)

Este c�digo crea tres nodos, pero a�n no tenemos una lista porque los nodos todav�a no est�n enlazados. El diagrama de estados tiene el siguiente aspecto:

Para enlazar los nodos, debemos hacer que el primer nodo haga referencia al segundo, y que �ste haga referencia al tercero:

>>> nodo1.siguiente = nodo2
>>> nodo2.siguiente = nodo3

La referencia del tercer nodo ser� None, que indica que es el final de la lista. Ahora el diagrama de estados tendr� el siguiente aspecto:

Ahora ya sabe c�mo crear nodos y enlazarlos en listas. Lo que podr�a estar menos claro es por qu�.

17.3 Listas como colecciones

Las listas son utiles porque aportan un modo de ensamblar m�ltiples objetos dentro de una �nica entidad, a veces llamada colecci�n. En el ejemplo, el primer nodo de la lista sirve como referencia a la lista completa.

Para pasar la lista como par�metro, s�lo tenemos que hacer referencia al primer nodo. Por ejemplo, la funci�n imprimeLista toma como argumento un nodo simple. Empezando con la cabeza de la lista, imprime cada nodo hasta que llega al final.

def imprimeLista(nodo):
  while nodo:
    print nodo,
    nodo = nodo.siguiente
  print

Para llamar a este m�todo, pasamos una referencia al primer nodo:

>>> imprimeLista(nodo1)
1 2 3

Dentro de imprimeLista tenemos una referencia al primer nodo de la lista, pero no hay una variable que haga referencia a los otros nodos. Tendremos que usar el valor de siguiente de cada nodo para acceder al siguiente nodo.

Para recorrer una lista enlazada es habitual usar la variable de un bucle como nodo para hacer referencia sucesivamente a cada uno de los nodos.

Este diagrama muestra el valor de lista y los valores que va tomando nodo:

Por convenio, las listas suelen imprimirse entre corchetes con comas entre los elementos, como [1, 2, 3]. Como ejercicio, modifique imprimeLista para que genere una salida con este formato.

17.4 Listas y recursividad

Es natural expresar muchas operaciones con listas por medio de m�todos recursivos. El siguiente ejemplo es un algoritmo recursivo para imprimir una lista hacia atr�s:

  1. Separar la lista en dos partes: el primer nodo (llamado cabeza) y el resto (llamado cola).
  2. Imprimir la cola hacia atr�s.
  3. Imprimir la cabeza.

Por supuesto, el paso 2, la llamada recursiva, supone que tenemos una manera de imprimir una lista del rev�s. Pero si suponemos que la llamada recursiva funciona     el acto de fe     entonces podemos estar convencidos de que este algoritmo funciona.

Todo lo que necesitamos es un caso b�sico y una forma de demostrar que para cualquier lista podemos obtener el caso b�sico. Dada la definici�n recursiva de una lista, un caso b�sico natural es la lista vac�a, representada por None:

def imprimeAlReves(lista):
  if lista == None: return
  cabeza = lista
  cola = lista.siguiente
  imprimeAlReves(cola)
  print cabeza,

La primera l�nea maneja el caso inicial no haciendo nada. Las dos siguientes l�neas dividen la lista en cabeza y cola. Las dos �ltimas l�neas imprimen la lista. La coma que hay al final de la �ltima l�nea evita que Python salte de l�nea despu�s de imprimir un nodo.

Llamamos este m�todo tal como antes invocamos a imprimeLista:

>>> imprimeAlReves(nodo1)
3 2 1

El resultado es una lista invertida.

Es posible que se pregunte por qu� imprimeLista e imprimeAlRevesson funciones y no m�todos de la clase Nodo. La raz�n es que queremos usar None para representar la lista vac�a y no es legal llamar un m�todo sobre None. Esta limitaci�n resulta algo inc�moda a la hora de escribir c�digo para manipular listas con un estilo orientado a objetos puro.

�Podemos demostrar que imprimeAlReves siempre acabar�? En otras palabras, �siempre alcanzaremos el caso b�sico? De hecho, la respuesta es no. Algunas listas har�n que el m�todo no funcione.

17.5 Listas infinitas

No hay nada que impida a un nodo hacer referencia a un nodo anterior de la lista, incluido �l mismo. Por ejemplo, esta figura muestra una lista con dos nodos, uno de los cuales apunta a s� mismo:

Si llamamos a imprimeLista sobre esta lista, entrar� en un bucle infinito. Si llamamos a imprimeAlReves, lo har� de forma infinitamente recursiva. Eeste tipo de comportamiento da lugar a que sea muy dif�cil trabajar con listas infinitas.

Sin embargo, en ocasiones resultan �tiles. Por ejemplo, podr�amos representar un n�mero como una lista de d�gitos y usar una lista infinita para representar una fracci�n repetida.

A pesar de todo, es un problema que no podamos probar que imprimeListae imprimeAlReves puedan terminar correctamente. Lo mejor que podemos hacer es afirmar que "si la lista no contiene bucles, estos m�todos podr�n terminar". Este tipo de afirmaciones se conocen como condiciones previas. Imponen una restricci�n sobre uno de los par�metros y describen el comportamiento del m�todo si la restricci�n se satisface. Veremos m�s ejemplos m�s adelante.

17.6 Teorema fundamental de la ambig�edad

Una parte de imprimeAlReves podr�a habernos sorprendido:

    cabeza = lista
    cola = lista.siguiente

Despu�s de la primera asignaci�n, la cabeza y la cola tienen el mismo tipo y el mismo valor. Asi que, �para qu� hemos creado un nueva variable?

La raz�n es que las dos variables desempe�an papeles diferentes. Pensamos en la cabeza como una referencia al primer nodo de la lista. Estos "papeles" no forman parte del programa, sino que est�n en la mente del programador.

En general no podemos decir con s�lo mirar un programa qu� papel desempe�ar� un variable. Esta ambig�edad puede ser �til, pero tambi�n puede dificultar la lectura del programa. A menudo usaremos nombres para las variables como nodo y lista para explicar c�mo queremos usar una variable y a veces creamos variables adicionales para eliminar ambig�edades.

Podr�amos haber escrito imprimeAlReves sin cabeza ni cola, que lo har�a mas conciso, pero posiblemente menos claro:

def imprimeAlReves(lista) :
  if lista == None : return
  imprimeAlReves(lista.siguiente)
  print lista,

Mirando esas dos llamadas, hemos de recordar que imprimeAlRevestrata sus argumentos como una colecci�n y print los trata como a un s�lo objeto.

El teorema fundamental de la ambig�edad indica que la ambig�edad que es inherente a una referencia a un nodo:

Una variable que hace apunta a nodo puede tratar a este nodo como un objeto o como el primero de una lista de nodos.

17.7 Modificar listas

Hay dos formas de modificar una lista enlazada. Obviamente, podemos cambiar la carga de uno de los nodos, pero las operaciones m�s interesantes son las que a�aden, quitan o reordenan los nodos.

Como ejemplo, escribamos un m�todo que quite el segundo nodo de la lista y devuelva una referencia al nodo quitado:

def eliminaSegundo(lista):
  if lista == None: return
  primero = lista
  segundo = lista.siguiente
  # hacer que el primer noda apunte al tercero
  primero.siguiente = segundo.siguiente
  # separar el segundo nodo del resto de la lista
  segundo.siguiente = None
  return segundo

De nuevo, estamos usando variables temporales para hacer m�s legible el c�digo. As� es como usamos este m�todo:

>>> imprimeLista(nodo1)
1 2 3
>>> eliminado = elimnaSegundo(nodo1)
>>> imprimeLista(eliminado)
2
>>> imprimeLista(nodo1)
1 3

El diagrama de estado nos muestra el efecto de la operaci�n:

�Qu� ocurrir�a si llam�ramos a este m�todo y pas�ramos una lista de un �nico elemento (un singleton)? �Qu� suceder�a si pas�ramos una lista vac�a como argumento? �Hay una condici�n previa para este m�todo? Si es as�, ser�a algo razonable establecer un m�todo para manejar una violaci�n de la condici�n previa.

17.8 Envoltorios y ayudantes

A menudo es �til dividir una operaci�n de listas en dos m�todos. Por ejemplo, para imprimir una lista invertida en el formato convencional [3, 2, 1] podemos usar el m�todo imprimeAlReves para imprimir 3, 2, pero necesitaremos un m�todo aparte para imprimir los corchetes y el primer nodo. Llam�moslo imprimeAlRevesBonito:

def imprimeAlRevesBonito(lista) :
  print "[",
  if lista != None :
    cabeza = lista
    cola = lista.siguiente
    imprimeAlReves(cola)
    print cabeza,
  print "]",

De nuevo, vemos que es buena idea comprobar m�todos como �ste para ver si funcionan con casos especiales como una lista vac�a o un singleton.

Cuando usamos este m�todo en alg�n otro lugar del programa, llamamos directamente a imprimeAlRevesBonito, y �ste llama a imprimeAlReves en nuestro lugar. En cierto modo, imprimeAlRevesBonito act�a como un envoltorio, y utiliza a imprimeAlReves como su ayudante.

17.9 La clase ListaEnlazada

Existen ciertos problemas delicados con la forma en que se implementaron las listas. Inviertiendo el orden de causa y efecto, propondremos primero una implementaci�n alternativa y explicaremos luego los problemas que �sta resuelve.

En primer lugar crearemos un nueva clase llamada ListaEnlazada. Sus atributos son un entero que contiene la longitud de la lista y una referencia al primer nodo. Los objetos ListaEnlazada sirven de asas para la manipulaci�n de las listas de los objetos de tipo Nodo:

class ListaEnlazada :
  def __init__(self) :
    self.longitud = 0
    self.cabeza   = None

Una ventaja de la clase ListaEnlazada es que suministra un marco natural para poner funciones-envoltorio como imprimeAlRevesBonito, que podemos convertir en un m�todo de la clase ListaEnlazada:

class ListaEnlazada:
  ...
  def imprimeAlReves(self):
    print "[",
    if self.cabeza != None:
      self.cabeza.imprimeAlReves()
    print "]",

class Nodo:
  ...
  def imprimeAlReves(self):
    if self.siguiente != None:
      cola = self.siguiente
      cola.imprimeAlReves()
    print self.carga,

Para complicar un poco m�s las cosas, renombramos imprimeAlRevesBonito. Ahora hay dos m�todos llamados imprimeAlReves: uno en la clase Nodo (el ayudante), y otro en la clase ListaEnlazada (el envoltorio). Cuando el envoltorio llama a self.cabeza.imprimeAlReves, est� llamando al ayudante, ya que self.cabeza es un objeto de tipo Nodo.

Otra ventaja de la clase ListaEnlazada es que facilita la forma de a�adir o quitar el primer elemento de una lista. Por ejemplo, agregaPrimero es un m�todo para ListaEnlazada; toma un elemento de la carga como argumento y lo coloca en el principio de la lista:

class ListaEnlazada:
  ...
  def agregaPrimero(self, carga):
    nodo = Nodo(carga)
    nodo.siguiente = self.cabeza
    self.cabeza = nodo
    self.longitud = self.longitud + 1

Como suele ser usual, deber�amos comprobar �ste c�digo para ver si maneja casos especiales. Por ejemplo, �qu� ocurrir�a si la lista est� unicialmente vac�a?

17.10 Invariantes

Algunas listas est�n "bien construidas"; otras no. Por ejemplo, si una lista contiene un bucle, provocar� que nuestros m�todos se cuelguen, as� que podr�amos exigir que las listas no contengan bucles. Otro requisito es que el valor de el valor de longitud en el objeto de tipo ListaEnlazada deber�a ser igual al n�mero verdadero de nodos de la lista.

A este tipo de requisitos los llamaremos invariantes porque, idealmente deber�an cumplirse en todos los objetos en todo momento. Especificar invariantes para objetos es una pr�ctica �til de la programaci�n porque hace m�s f�cil demostrar la idoneidad del c�digo, comprobar la integridad de las estructuras de datos y la detecci�n de errores.

Una cosa que a veces confunde respecto a los invariantes es que en ocasiones son violados. Por ejemplo, en medio de agregaPrimero, despu�s de a�adir el nodo paro antes de incrementar la longitud, se viola el invariante. Se acepta este tipo de violaci�n; de hecho, a menudo es imposible modificar un objeto sin violar un invariante durante al menos un corto espacio de tiempo. Normalmente, se exige que todo m�todo que viole un invariante debe restablecerlo.

Si hay alg�n tramo significativo de c�digo en el que el invariante se ve violado, es importante que los comentarios lo dejen claro, de modo que no se realicen operaciones que dependan del invariante.

17.11 Glosario

referencia incrustada
Es una referencia almacenada en un atributo de un objeto.
lista enlazada
Estructura de datos que implementa una colecci�n por medio de una secuencia de nodos enlazados.
nodo
Elemento de una lista, normalmente implementado como un objeto que contiene una referencia a otro objeto del mismo tipo.
carga
Datos contenidos en un nodo.
enlace
Referencia incrustada usada para enlazar un objeto con otro.
condici�n previa
Afirmaci�n que debe ser cierta para que un m�todo funcione correctamente.
teorema fundamental de la ambig�edad
Una referencia a un nodo de una lista puede tratarse como un objeto individual o como el primero de una lista de nodos.
singleton
Lista enlazada con un solo nodo.
envoltorio
M�todo que act�a como mediador entre un m�todo invocador y m�todo ayudante, haciendo a menudo su invocaci�n m�s f�cil o menos proclive a errores.
ayudante
M�todo al que no se invoca directamente por un m�todo llamante sino por otro m�todo para formar parte de una operaci�n.
invariante
Afirmaci�n que deber�a ser cierta para un objeto en todo momento (excepto tal vez cuando se est� modificando el objeto).


Next Up Previous Hi Index

" + str + "

Close window

Hosted by www.Geocities.ws

"); } //-->
Hosted by www.Geocities.ws

1