Nombre Password [ Regístrate ]

Conjuntos

 

* Introducción
* Representación mediante arrays de bits
* Representación mediante array
* Representación mediante lista enlazada

 

Introducción

Los conjuntos son una de las estructuras básicas de las matemáticas, y por tanto de la informática. No se va a entrar en la definición de conjuntos ni en sus propiedades. Se supondrá que el lector conoce algo de teoría de conjuntos. Con lo más básico es suficiente.

En realidad las estructuras de datos que se han implementado hasta ahora no son más que elementos diferentes entre sí (en general) en los que se ha definido una relación. Por ejemplo, en las listas ordenadas o los árboles binarios de búsqueda se tiene una serie de elementos que están ordenados entre sí. Obviando las propiedades de las estructuras, se ve que forman un conjunto, y su cardinal es el número de elementos que contenga la estructura. En los conjuntos no existen elementos repetidos, y esto se respeta en las implementaciones que se ofrecen a continuación.

Ahora bien, en esta sección se van definir unas implementaciones que permitan aplicar el álgebra de conjuntos, ya sea unión, intersección, pertenencia, etc. Se realizan tres implementaciones: array de bits, arrays y listas enlazadas.

 

Representación mediante arrays de bits

Ciertamente, un bit no da más que para representar dos estados diferentes. Por supuesto, pueden ser atributos muy variados, por ejemplo, ser hombre o mujer, adulto o niño, Windows o Linux, etc. También sirve para indicar si un elemento está o no dentro de un conjunto.

El array se utiliza para representar un conjunto de números naturales (u otro tipo de datos cuyos elementos se identifiquen por un número natural único mediante una correspondencia) entre 0 y N, siendo N la capacidad del array unidimensional (es decir, un vector); almacenará valores booleanos, esto es, 1 ó 0.

Por ejemplo, suponer el conjunto universal formado por los enteros entre 0 y 4: U = {0, 1, 2, 3, 4}, y el conjunto C = {1, 2}. Se representará de esta manera:

0 1 2 3 4
0 1 1 0 0

1 : indica que el elemento pertenece al conjunto.
0 : indica que el elemento no pertenece al conjunto.

Ahora bien, se ha dicho que se va a emplear un array de bits. ¿Qué se quiere decir con esto? Que no se va a emplear un array o vector como tal, sino un tipo de datos definido por el lenguaje de programación, que suele ocupar entre 8 y 64 bits, y por tanto podrá incluir hasta 64 elementos en el conjunto. Por ejemplo, en C o Pascal se define un tipo que ocupa 8 bits:

unsigned char conjunto8;
var conjunto8 : byte;

Si todos los bits de conjunto8 están a 1 entonces se tiene el conjunto: U = {0, 1, 2, 3, 4, 5, 6, 7}, y su cardinal es 8. Si todos los bits están a 0 se tiene el conjunto vacío. El bit más significativo señalará al elemento de mayor valor, el bit menos significativo al de menor valor. Ejemplos (bit más significativo a la izquierda):

11111111 -> U = {0, 1, 2, 3, 4, 5, 6, 7}
11110001 -> U = {0, 4, 5, 6, 7}
01010101 -> U = {0, 2, 4, 6}
00000000 -> U = vacío

La razón para emplear los arrays de bits es que las operaciones sobre los conjuntos se realizan de manera muy rápida y sencilla, al menos con los computadores actuales, que tienen un tamaño de palabra múltiplo de 8. Por supuesto, la ocupación en memoria está optimizada al máximo.
El inconveniente es que el rango de representación es muy limitado. Por eso su aplicación es muy restringida, y depende fuertemente del compilador y el computador sobre el que se implementan, pero es increíblemente rápida.

Nota: Pascal cuenta con un juego de instrucciones para manipular conjuntos definidos mediante arrays de bits, dando al usuario transparencia total sobre su manejo.

A continuación se implementa un TAD sobre conjuntos en C mediante array de bits.


- Tipo de datos empleado:

typedef unsigned long tconjunto;

El tipo long suele ocupar 32 bits, por tanto el rango será: [0..31].

Nota importante: en los ejemplos se muestran conjuntos que sólo tienen un máximo de 8 elementos (8 bits). Esto está puesto simplemente por aumentar la claridad, y cómo no, por ahorrar ceros.

 


- Definición de conjunto vacío y universal:

const tconjunto Universal = 0xFFFFFFFF;
const tconjunto vacio = 0;

Es decir, 32 bits puestos a 1 para el conjunto universal, 32 bits puestos a 0 para el conjunto vacío.


- Unión:

Se realiza mediante la operación de OR inclusivo. Ejemplo (con 8 bits en lugar de 32):

    11001100 -> A = {2,3,6,7}
Or 10010100 -> B = {2,4,7}
    ---------
    11011100 -> C = {2,3,4,6,7}

y se codifica así:

tconjunto unircjto(tconjunto A, tconjunto B)
{ return (A | B); }


- Intersección:

Se realiza mediante la operación AND. Ejemplo:

      11001100 -> A = {2,3,6,7}
And 10010100 -> B = {2,4,7}
      ---------
      10000100 -> C = {2,7}

y se codifica así:

tconjunto interseccion(tconjunto A, tconjunto B)
{ return (A & B); }

 

- Diferencia:

Para obtener C = A-B se invierten todos los bits de B y se hace un AND entre A y B negado. Ejemplo:

10011101 -> A = {0,2,3,4,7}
10110010 -> B = {1,4,5,7}
B negado: 01001101 -> B(negado) = {0,2,3,6}

      10011101
And 01001101
      ---------
      00001101 -> C = {0,2,3}

y se codifica así:

tconjunto diferencia2(tconjunto A, tconjunto B)
{ return (A & ~B); }

 

- Diferencia simétrica:

C = (A-B) Unión (B-A)
Se realiza mediante la operación de OR exclusivo (XOR) o aplicando las primitivas definidas anteriormente. Ejemplo:

     11110000 -> A = {4,5,6,7}
Xor 00011110 -> B = {1,2,3,4}
     ---------
     11101110 -> C = {1,2,3,5,6,7}

y se codifica así:

tconjunto difsim(tconjunto A, tconjunto B)
{ return (A ^ B); }

 

- Igualdad de conjuntos:

La implementación es directa, si todos los bits de A y B se corresponden entonces son iguales:

int iguales(tconjunto A, tconjunto B)
{ return (A == B); }

 

- Subconjuntos:

Si un conjunto A es subconjunto (considerando que un conjunto cualquiera es subconjunto de si mismo) de otro B entonces verifica esta relación: A intersección B = A. Notar que A es subconjunto de A, pues A intersección A = A.

Ejemplo:
A = {1,2,3,4}, B = {0,1,2,3}
C = A intersección B = {1,2,3}; C es distinto de A.

Se codifica así:

int subconjunto(tconjunto A, tconjunto B)
{ return (iguales(interseccion(A,B),A)); }

 

- Pertenencia:

Determinar si un elemento pertenece a un conjunto requiere efectuar una operación de desplazamiento a nivel de bits y una posterior comprobación del bit de signo resultante. Como siempre, un ejemplo o dos lo aclaran:
Sea x = 0 y A = {0,1,2,5}. Determinar si x pertecene a A.
00100111 -> A.
Primero se desplazan los bits de A tantas veces a la derecha como valga x, en el ejemplo no se desplazan; se obtiene A'. A continuación se aplica el test del bit de signo sobre A', que consiste en obtener el resto de la división entera entre dos. Si el resto es uno, entonces x pertenece a A. En caso contrario no pertenece a A. En el ejemplo: A' mod 2 = 1, luego x pertenece a A.

Otro ejemplo: x = 3, A = {0,1,2,5}.
Se desplazan los bits de A tres posiciones a la derecha:
00000100 -> A'.
Se hace el test de signo: A' mod 2 = 0. x no pertenece a A.

La codificación es la siguiente:

int pertenece(tconjunto A, int x)
{ return ((A >> x) % 2); }

 

- Inserción y borrado:

Para insertar un elemento x es necesario poner a 1 el bit correspondiente. Una manera sencilla de hacerlo es mediante una suma. Hay que sumar un valor que se corresponda con el bit que se quiere establecer a 1. Para hacerlo se volverá a aplicar una operación de desplazamiento, pero esta vez hacia la izquierda y aplicada sobre el número 1. Se desplazan x bits hacia la izquierda, suponiendo que el compilador llena con ceros por la derecha.

Por ejemplo, partir de A = conjunto vacío: { }.
Se quieren insertar los elementos 0,2,3 sobre A.
Insertar 0:
x = 0. p = 1, (00000001 en binario). Se desplaza p x (0) bits a la izquierda, p' = 1, y se suma a A. Queda: A <- A + p'. A = 1.

Insertar 2:
x = 2. p = 1. Se desplaza p x (2) bits a la izquierda, p' = 4 (000000100 en binario). A <- A (1) + p' (4), A = 5 (000000101).

Insertar 3:
x =3. p = 1. Se desplaza p x (3) bits a la izquierda, p' = 8. A <- A (5) + p' (8), A = 13 (00001101).

El borrado es exactamente lo mismo, pero hay que restar p en vez de sumar. Ejemplo: borrar 3 de A.
A <- A (13) - p' (8), A = 5 (00000101)


Antes de la codificación, hay que considerar otro detalle: es necesario comprobar previamente si el elemento ya está en el conjunto para evitar problemas inesperados. Por tanto, la codificación queda así para la inserción:

tconjunto insertar(tconjunto A, int x)
{
  if (pertenece(A,x)) return A;
  else return (A + ((tconjunto)1 << x));
}

y para el borrado:

tconjunto borrar(tconjunto A, int x)
{
  if (pertenece(A,x)) return A;
  else return (A - ((tconjunto)1 << x));
}

 

Conclusiones

Sin duda alguna, la gran ventaja de esta implementación es la rapidez de ejecución de todas las operaciones, que se ejecutan en tiempo constante: O(1). Además los elementos se encuentran empaquetados ocupando el menor espacio posible, esto es, un único bit.

La desventaja es que no admiten un rango muy amplio de representación. Aun así, para incrementar el rango basta con crear un array de tipo conjunto, por ejemplo: tconjunto superconjunto[10], y aplicar las operaciones sobre los bits en todos los elementos del array, excepto para la inserción y borrado, en cuyo caso hay que encontrar el bit exacto a manipular.

 

 

Representación mediante array

Los elementos del conjunto se guardan uno a continuación de otro empleando una lista densa representada mediante un array.

Ejemplo: Sea el conjunto C = {1, 2}. Se representará de esta manera:

0 1 2 3 4
1 2      

y su cardinal es 2.

Esta representación no limita el rango de representación más que al tipo de datos empleado. Por supuesto, ya no puede definirse explícitamente el conjunto universal.

Por razones de eficiencia a la hora de implementar las primitivas, las estructuras se pasan por referencia. Es un detalle importante, porque C garantiza que un array se pasa siempre por referencia, pero eso no es cierto si el array se pasa como parte de una estructura.

No se implementan rutinas de control de errores ni su detección. Se produce un error cuando se tratan de añadir elementos y estos desbordan la capacidad del array.

Nota importante: los elementos dentro del array no están ordenados entre sí.

 

- Tipo de datos empleado:

typedef int tTipo;

typedef struct
{
  tTipo elems[MAXELEM];
  int cardinal;
} tconjunto;

 

- Definición de conjunto vacío:

Un conjunto está vacío si su cardinal es cero. Para inicializar un conjunto a vacío basta con una instrucción:

A->cardinal = 0


- Pertenencia:

Para determinar si un elemento x pertenece al conjunto basta con recorrer el array hasta encontrarlo. Se devuelve True si se encuentra. Codificación:

int pertenece(tconjunto *A, tTipo x)
{
  int i;
  for (i = 0; i < A->cardinal; i++)
    if (A->elems[i] == x) return 1;
  return 0;
}

 

- Inserción y borrado:

Para insertar un elemento, primero debe comprobarse que no está, después se inserta en la última posición, esto es, la que señale el cardinal, que se incrementa en una unidad. Codificación:

void insertar(tconjunto *A, tTipo x)
{
  if (!pertenece(A, x))
    A->elems[A->cardinal++] = x;
}

Borrar es aparentemente más complicado. No se puede eliminar el elemento y dejar un hueco, puesto que en ese caso ya no se tiene una lista. Para eliminar este problema se sustituye el elemento borrado por el último de la lista. Codificación:

void borrar(tconjunto *A, tTipo x)
{
  int i;
  for (i = 0; i < A->cardinal; i++)
    if (A->elems[i] == x) {
      A->elems[i] = A->elems[--A->cardinal]; return;
    }
}

 

- Unión:

Para hacer C = A Unión B, se introducen en C todos los elementos de A y todos los elementos de B que no pertenezcan a A. Codificación:

void unircjto(tconjunto *A, tconjunto *B, tconjunto *C)
{
  int i;
  *C = *A;
  for (i = 0; i < B->cardinal; i++)
    if (!pertenece(A, B->elems[i]))
      insertar(C, B->elems[i]);
}

 

- Intersección:

Para hacer C = A intersección B, se hace un recorrido sobre A (o B) y se insertan en C los elementos que estén en B (o A).

El pseudocódigo es:

C = vacío
para cada x elemento de A
  si x pertenece a B entonces insertar x en C.

En C:

void interseccion(tconjunto *A, tconjunto *B, tconjunto *C)
{
  int i;
  C->cardinal = 0;
  for (i = 0; i < A->cardinal; i++)
    if (pertenece(B, A->elems[i]))
      insertar(C, A->elems[i]);
}

 

- Diferencia:

Para hacer C = A-B, se hace un recorrido sobre A (o B) y se insertan en C los elementos que no estén en B (o A).

El pseudocódigo es:

C = vacío
para cada x elemento de A
  si x no pertenece a B entonces insertar x en C.

En C:

void diferencia(tconjunto *A, tconjunto *B, tconjunto *C)
{
  int i;
  C->cardinal = 0;
  for (i = 0; i < A->cardinal; i++)
    if (!pertenece(B, A->elems[i]))
      insertar(C, A->elems[i]);
}

 

- Diferencia simétrica:

Sea C = (A-B) Unión (B-A). Para obtener este resultado se puede aprovechar el código estudiado anteriormente.

El pseudocódigo es:

C = vacío
para cada x elemento de A
  si x no pertenece a B entonces insertar x en C
para cada x elemento de B
  si x no pertenece a A entonces insertar x en C

En C:

void difsim(tconjunto *A, tconjunto *B, tconjunto *C)
{
  int i;
  C->cardinal = 0;
  for (i = 0; i < A->cardinal; i++)
    if (!pertenece(B, A->elems[i]))
      insertar(C, A->elems[i]);
  for (i = 0; i < B->cardinal; i++)
    if (!pertenece(A, B->elems[i]))
      insertar(C, B->elems[i]);  
}

 

- Subconjuntos:

Determinar si un conjunto A es subconjunto de B se reduce a comprobar si todo elemento de A es elemento de B. Se devuelve True si A es subconjunto de B. Codificación:

int subconjunto(tconjunto *A, tconjunto *B) /
{
  int i, esta;

  esta = 1;
  for (i = 0; i < A->cardinal; i++)
    if (!pertenece(B, A->elems[i])) return 0;
  return 1;
}

 

- Igualdad de conjuntos:

Un conjunto A es igual a otro B si A es subconjunto de B y ambos tienen los mismos elementos. Se devuelve True si A es igual a B. Codificación:

int iguales(tconjunto *A, tconjunto *B)
{
  return (subconjunto(A,B) && A->cardinal == B->cardinal);
}

 

Conclusiones

La ventaja de esta implementación es que no limita el rango de representación de los elementos del conjunto, y por supuesto tampoco limita el tipo de datos, siempre y cuando se pueda deducir cuando un elemento es igual a otro o no.

La desventaja de esta implementación con respecto a la de arrays de bits es su mala eficacia con respecto al tiempo de ejecución. El coste de la inserción y borrado es O(1). Siendo |A| el cardinal de un conjunto cualquiera A las operaciones de pertenencia se ejecuta en un tiempo O(|A|). En las restantes operaciones, que implican a dos conjuntos, la complejidad es O(|A|·|B|)

El espacio que ocupa un conjunto es de O(|MAXIMO|), siendo MAXIMO el tamaño del array.

 

Representación mediante lista enlazada

Esta representación es muy parecida a la implementación mediante un array, pero con alguna particularidad que la hace más interesante en algunos casos. Por supuesto, los tipos de datos de los elementos que se insertan son igualmente admisibles con listas como lo eran con arrays.

Suponer que entre los elementos del conjunto se puede definir una relación de orden, es decir, que se puede determinar si un elemento es mayor que otro. En este caso se pueden insertar y borrar elementos del conjunto de forma que la lista que los mantiene esté ordenada. Esto puede resultar interesante en algunas aplicaciones.

Sea |A| y |B| el cardinal de unos conjuntos cualesquiera A y B. Aplicando la suposición anterior las operaciónes de búsqueda, inserción y borrado se ejecutan en un tiempo O(|A|).
Pero hay una gran ventaja, y es que las restantes operaciones se ejecutan en un tiempo O(|A|+|B|). ¿Cómo se consigue ésto? Aprovechando las propiedades de tener listas ordenadas basta con hacer un único recorrido sobre cada lista. Esto es posible implementando un algoritmo basado en el algoritmo de fusión de dos listas ordenadas, que obtiene una lista ordenada a partir de dos o más listas ordenadas con un único recorrido de cada lista. (Es recomendable ver primero los Algoritmos de ordenación de listas y entender el proceso de intercalación o fusión, pero NO es necesario estudiar el proceso recursivo ya que no tiene interés aquí). Las operaciones de unión, intersección, etcétera, e incluiso el determinar si un conjunto es subconjunto de otro se efectúan haciendo pequeñas variaciones sobre el algoritmo de intercalación.

Nota sobre la implementación: Al estudiar la codificación se podrá notar que los conjuntos A y B sobre los que se hacen los recorridos no se modifican sino que quedan como están. Para ganar en eficiencia se puede hacer que el nuevo conjunto C no cree su propia lista de elementos sino que simplemente aproveche los enlaces de las listas que mantienen los conjuntos A y B, deshaciendo estos. Esto es algo opcional y no se ha implementado, pero es útil si dichos conjuntos no se van a emplear más. Los punteros c1 y c2 recorren las listas que representan a los conjuntos A y B respectivamente. El puntero c3 y aux sirven para crear el nuevo conjunto C.


Definición y tipo de datos empleado:

Se empleará una lista enlazada con cabecera ficticia y centinela. La razón es que se realizarán inserciones y búsquedas sobre la lista que contiene los elementos del conjunto. Como se ha comentado anteriormente, los elementos de la lista estarán ordenados. Por tanto, para emplear esta representación los elementos deben ser ordenables. En el código propuesto, se tratarán conjuntos de números enteros.

Los tipos de datos se declaran así:

typedef struct lista
{
  int elem;
  struct lista *sig;
} lista;


typedef struct tconjunto
{
  lista *cabecera,
	    *centinela;
  int cardinal;
} tconjunto;

 

- Creación del conjunto vacío:

La creación de un nuevo conjunto (vacío) se realiza estableciendo a cero el número de elementos y reservando memoria para los elementos de cabecera y centinela de la lista.

void crearcjto(struct tconjunto *cjto)
{
  cjto->cabecera = (lista *) malloc(sizeof(lista));
  cjto->centinela = (lista *) malloc(sizeof(lista));
  cjto->cabecera->sig = cjto->centinela;
  cjto->centinela->sig = cjto->centinela; /* opcional, por convenio */
  cjto->cardinal = 0;
}

- Pertenencia:

Para determinar si un elemento x pertenece al conjunto basta con recorrer la lista hasta encontrarlo o llegar al final de ésta. Se devuelve True si se encuentra antes del centinela.

int pertenece(tconjunto cjto, int x)
{
  lista *actual;

  actual = cjto.cabecera->sig;
  cjto.centinela->elem = x;
  while (actual->elem != x)
    actual = actual->sig;
  if (actual == cjto.centinela)
    return 0;
  else
    return 1;
}

- Inserción y borrado:

Para insertar un elemento primero debe comprobarse que no está, después se inserta ordenadamente en la lista, y se incrementa el cardinal en una unidad.

void insertar(tconjunto *cjto, int x)
{
  lista *anterior, *actual, *nuevo;

   /* 1.- busca */
  anterior = cjto->cabecera;
  actual = cjto->cabecera->sig;
  cjto->centinela->elem = x;
  while (actual->elem < x) {
    anterior = actual;
    actual = actual->sig;
  }

  if (actual->elem != x || actual == cjto->centinela) {
     /* 2.- crea */
    nuevo = (lista *) malloc(sizeof(lista));
    nuevo->elem = x;
     /* 3.- enlaza */
    nuevo->sig = actual;
    anterior->sig = nuevo;

    cjto->cardinal++;
  }
}

 

Para borrar un elemento basta con localizarlo dentro de la lista y eliminarlo.

void borrar(tconjunto *cjto, int x)
{
  lista *anterior, *actual;
    /* 1.- busca */
  anterior = cjto->cabecera;
  actual = cjto->cabecera->sig;
  cjto->centinela->elem = x;
  while (actual->elem < x) {
    anterior = actual;
    actual = actual->sig;
  }
  /* 2.- borra si existe */
  if (actual != cjto->centinela && actual->elem == x) {
    anterior->sig = actual->sig;
    free(actual);
  }
}

- Unión:

A partir de los conjuntos A y B se crea un nuevo conjunto C. Se supone que el conjunto C no ha sido inicializado antes. En cada paso se añade siempre un nuevo elemento. Por último se comprueba que no queden elementos sin copiar.

void unioncjto(tconjunto A, tconjunto B, tconjunto *C)
{
  lista *c1, *c2, *c3, *aux;

  crearcjto(C);
  c3 = C->cabecera;
  c1 = A.cabecera->sig; c2 = B.cabecera->sig;
  while (c1 != A.centinela && c2 != B.centinela) {
    aux = (lista *) malloc(sizeof(lista));
    if (c1->elem < c2->elem) {
      aux->elem = c1->elem;
      c1 = c1->sig;
    }
    else if (c1->elem > c2->elem) {
      aux->elem = c2->elem;
      c2 = c2->sig;
    }
    else {
      aux->elem = c1->elem; /* tambien vale: aux->elem = c2->elem */
      c1 = c1->sig;
      c2 = c2->sig;
    }
    aux->sig = C->centinela;
    c3->sig = aux; c3 = aux;
    C->cardinal++;
  }
  /* copia los elementos restantes si los hubiera */
  if (c1 != A.centinela) {
	while (c1 != A.centinela) {
	  aux = (lista *) malloc(sizeof(lista));
	  aux->elem = c1->elem;
	  aux->sig = C->centinela;
	  c3->sig = aux; c3 = aux;
	  C->cardinal++;
	  c1 = c1->sig;
	}
  }
  else if (c2 != B.centinela) {
	while (c2 != B.centinela) {
	  aux = (lista *) malloc(sizeof(lista));
	  aux->elem = c2->elem;
	  aux->sig = C->centinela;
	  c3->sig = aux; c3 = aux;
	  C->cardinal++;
	  c2 = c2->sig;
	}
  }
}

- Intersección:

C = A Intersección B, es el nuevo conjunto que se crea. Se añade un elemento cuando coincide en ambas listas a la vez (c1->elem == c2->elem).

void interseccion(tconjunto A, tconjunto B, tconjunto *C)
{
  lista *c1, *c2, *c3, *aux;

  crearcjto(C);
  c3 = C->cabecera;
  c1 = A.cabecera->sig; c2 = B.cabecera->sig;
  while (c1 != A.centinela && c2 != B.centinela) {
    if (c1->elem < c2->elem)
      c1 = c1->sig;
    else if (c1->elem > c2->elem)
      c2 = c2->sig;
    else {
      aux = (lista *) malloc(sizeof(lista));
      aux->elem = c1->elem; /* tambien vale: aux->elem = c2->elem */
      aux->sig = C->centinela;
      c3->sig = aux; c3 = aux;
      C->cardinal++;
      c1 = c1->sig;
      c2 = c2->sig;
	}
  }
}

- Diferencia:

C = A-B. Se añade un nuevo elemento sólo cuando (c1->elem < c2->elem).

void diferencia(tconjunto A, tconjunto B, tconjunto *C)
{
  lista *c1, *c2, *c3, *aux;

  crearcjto(C);
  c3 = C->cabecera;
  c1 = A.cabecera->sig; c2 = B.cabecera->sig;
  while (c1 != A.centinela && c2 != B.centinela) {
	if (c1->elem < c2->elem) {
      aux = (lista *) malloc(sizeof(lista));
      aux->elem = c1->elem;
      aux->sig = C->centinela;
      c3->sig = aux; c3 = aux;
      C->cardinal++;
      c1 = c1->sig;
	}
    else if (c1->elem > c2->elem)
      c2 = c2->sig;
    else 
      c1 = c1->sig, 
	  c2 = c2->sig;
  }
  /* aniade lo que quede de A */
  while (c1 != A.centinela) {
	aux = (lista *) malloc(sizeof(lista));
	aux->elem = c1->elem;
	aux->sig = C->centinela;
	c3->sig = aux; c3 = aux;
	C->cardinal++;
	c1 = c1->sig;
  }
}

- Diferencia simétrica:

C = (A-B) Unión (B-A). Es decir, todos los elementos no comunes de ambos conjuntos. Se añaden elementos si (c1->elem != c2->elem).

void difsim(tconjunto A, tconjunto B, tconjunto *C)
{
  lista *c1, *c2, *c3, *aux;

  crearcjto(C);
  c3 = C->cabecera;
  c1 = A.cabecera->sig; c2 = B.cabecera->sig;
  while (c1 != A.centinela && c2 != B.centinela) {
    if (c1->elem != c2->elem) {
      aux = (lista *) malloc(sizeof(lista));
	  if (c1->elem < c2->elem) { aux->elem = c1->elem; c1 = c1->sig; }
	  else { aux->elem = c2->elem; c2 = c2->sig; }
	  
	  aux->sig = C->centinela;
	  c3->sig = aux; c3 = aux;
	  C->cardinal++;
	}
    else {
      c1 = c1->sig;
      c2 = c2->sig;
    }
  }
  /* copia los elementos restantes si los hubiera */
  if (c1 != A.centinela) {
	while (c1 != A.centinela) {
	  aux = (lista *) malloc(sizeof(lista));
	  aux->elem = c1->elem;
	  aux->sig = C->centinela;
	  c3->sig = aux; c3 = aux;
	  C->cardinal++;
	  c1 = c1->sig;
	}
  }
  else if (c2 != B.centinela) {
	while (c2 != B.centinela) {
	  aux = (lista *) malloc(sizeof(lista));
	  aux->elem = c2->elem;
	  aux->sig = C->centinela;
	  c3->sig = aux; c3 = aux;
	  C->cardinal++;
	  c2 = c2->sig;
	}
  }
}

- Subconjuntos:

Determinar si un conjunto A es subconjunto de B se reduce a comprobar si todo elemento de A es elemento de B. Se devuelve True si A es subconjunto de B. Observar que si (c1->elem < c2->elem) entonces A ya no puede ser subconjunto de B, pues implica que dicho elemento no está en B, ya que c2 representa al menor de los elementos restantes del conjunto. Por último, observar la última condición: return (essub && c1 == A.centinela);. Es decir, quedan elementos de A que no han sido recorridos, pero B ya está totalmente recorrido, luego A no es subconjunto de B. Si se da el caso de que essub = true y c1 != A.centinela entonces se puede devolver un tercer valor que indique que B es subconjunto de A.

int subconjunto(tconjunto A, tconjunto B)
{
  int essub = 1;
  lista *c1, *c2;
  
  c1 = A.cabecera->sig; c2 = B.cabecera->sig;
  while (c1 != A.centinela && c2 != B.centinela && essub) {
	if (c1->elem < c2->elem)
	  essub = 0;
	else if (c1->elem > c2->elem)
	  c2 = c2->sig;	  
	else {
	  c1 = c1->sig;
	  c2 = c2->sig;
	}
  }
  return (essub && c1 == A.centinela);
}

- Igualdad de conjuntos:

Un conjunto A es igual a otro B si ambos tienen los mismos elementos. Se devuelve True si A es igual a B. Se comprueba primero el cardinal de ambos conjuntos.

int iguales(tconjunto A, tconjunto B)
{
  int igual;
  lista *c1, *c2;

  igual = A.cardinal == B.cardinal;

  c1 = A.cabecera->sig; c2 = B.cabecera->sig;
  while (c1 != A.centinela && c2 != B.centinela && igual) {
    if (c1->elem != c2->elem)
	  igual = 0;
    c1 = c1->sig;
	c2 = c2->sig;
  }
  return (igual);
}

 

Programa de prueba:

int main(void)
{
  tconjunto A, B, C, p1, p2, p3, p4, p5, p6;

  crearcjto(&A);
  crearcjto(&B);
  crearcjto(&C);

  /* A = {2,3,5}
     B = {1,2,3,4,5}
     C=  {3,4,6}
  */
  insertar(&A, 2); insertar(&A, 3); insertar(&A, 5);
  insertar(&B, 1); insertar(&B, 2); insertar(&B, 3); insertar(&B, 4); insertar(&B, 5);
  insertar(&C, 3); insertar(&C, 4); insertar(&C, 6);


  if (pertenece(A, 5)) printf("5 pertenece a A");
  if (!pertenece(B, 6)) printf("\n6 no pertenece a B");

  /* p1 = {2,3,4,5,6} */
  unioncjto(A,C,&p1);  
  
  /* p2 = {3} */
  interseccion(A,C,&p2);

  /* p3 = {1,4} */
  diferencia(B,A,&p3);

  /* p4 = p6 = {2,4,5,6}, p5 = vacío */
  difsim(C,A,&p4);
  difsim(A,C,&p6);
  difsim(B,B,&p5);

  if (iguales(p4,p6)) printf("\np4 = p6");
  if (subconjunto(B,B)) printf("\nB es subconjunto de B");
  if (!subconjunto(B,C)) printf("\nB no es subconjunto de C");
  if (subconjunto(A,B)) printf("\nA es subconjunto de B");
  if (subconjunto(p5, A)) printf("\np5 es subconjunto de A");


  return 0;
}

 

Conclusiones

Esta implementación tampoco limita el rango de representación de los elementos del conjunto, y por supuesto tampoco limita el tipo de datos, siempre y cuando se pueda deducir cuando un elemento es igual a otro o no.

Dado un conjunto A y B, las operaciones de inserción, borrado y pertenencia se ejecutan en un tiempo de O(|A|). Las operaciones de unión, intersección, diferencia, diferencia simétrica, subconjunto e igualdad se ejecutan en un tiempo de O(|A|+|B|).

El espacio que ocupa un conjunto es de O(|A|), siendo |A| el cardinal del conjunto A. Por supuesto es proporcional al tamaño del conjunto implementado mediante array, multiplicado por una constante debido al espacio ocupado por los punteros.

 


© (2001-2008) ALGORITMIA.NET - Política de privacidad