!wget -nc --no-cache -O init.py -q https://raw.githubusercontent.com/rramosp/introalgs.v1/main/content/init.py
import init; init.init(force_download=False); 

Tipos abstractos de datos

Objetivos del módulo

Conocer el concepto de tipos abstractos de datos y aprender a definir, interpretar e implementar estructuras de datos usando esta técnica.

Preguntas básicas

  1. ¿Qué es una tipo abstracto de datos (TAD)?

  2. ¿Qué significa ‘abstracto’?

  3. ¿Cómo se define un TAD?

  4. ¿Cómo se implementa un TAD?

  5. ¿Cómo se mide el rendimiento de una implementación?

Introducción

Uno de los temas de mayor importancia en el desarrollo de software es la definición de estructuras de datos en abstracto, es decir, independiente de su representación. Dicha definición conduce hacia la programación orientada a objetos (POO) y a una de sus principales características: el encapsulamiento.

  • TIPO (type): es un conjunto de valores. P.ej. [True, False], \(\mathbb{N}\), \(\mathbb{Z}\)

  • TIPO ABSTRACTO DE DATOS (TAD) (abstract data type, ADT): es un tipo de datos, MÁS una definición de conjunto de operaciones sobre este tipo de datos, MÁS un conjunto de axiomas que definen la semántica de las operaciones.

  • ESTRUCTURA DE DATOS (data structure): es una realización de un TAD en un lenguaje de programación concreto.

es preciso observar que:

  • Un TAD es una definición abstracta en algún idioma matemático o informal.

  • Un TAD especifica QUÉ hace una operación, pero no CÓMO.

  • Los axiomas suelen definir precondiciones y poscondiciones de las operaciones.

  • Si la definición de un TAD es totalmente matemática podríamos en teoría probar formalmente propiedades y algoritmos (i.e. verificación formal, derivación de programas, reparación de bugs, etc.).

  • Algunos tipos están ya implementados (built-in) en la base de Java, Python, Haskell, C, …

  • Un TAD suele imlpementarse como una clase en distintos lenguajes de programación.

referencias:

  • http://www.cs.fsu.edu/~lacher/lectures/Output/adts/index.html

  • http://btechsmartclass.com/DS/U1_T1.html

  • https://ece.uwaterloo.ca/~dwharder/aads/Abstract_data_types/Linear_ordering/Stack/

  • https://www.geeksforgeeks.org/abstract-data-types/

Ejemplo 1. Números naturales

TAD: \(NumeroNatural\)

\(\forall n,m \in NumeroNatural\)

  • signatures: $$

(1)\[\begin{align} cero() &\rightarrow NumeroNatural\\ sucesor(n) &\rightarrow NumeroNatural\\ esCero(n) &\rightarrow Bool\\ suma(n,m) &\rightarrow NumeroNatural\\ resta(n,m) &\rightarrow NumeroNatural\\ multiplica(n,m) &\rightarrow NumeroNatural\\ divide(n,m) &\rightarrow NumeroNatural\\ igual(n,m) &\rightarrow Bool\\ \end{align}\]

$$

  • axioms:

    \(esCero(cero()) := \)True
    \(esCero(sucesor(x)) := \)False

    \(suma(cero(), y) := y\)
    \(sum(sucesor(x),y) := sucesor(suma(x,y))\)

    \(resta(x, cero()) ::= x\)
    \(resta(cero(), y) ::= \) if \( esCero(y)\) then \(cero()\) else \(error\)
    \(resta(sucesor(x), sucesor(y)):=resta(x,y)\)

    \(multiplica(x, cero()) ::= cero()\)
    \(multiplica(x, sucesor(y)) ::= suma(x, multiplica(x, y))\)

    \(divide(cero(), y) ::= cero()\)
    \(divide(x, cero()) ::= error\)
    \(divide(x, y) ::=\) if \(menor(x, y) \) then \( cero()\) else \(sucesor(divide(resta(x, y), y))\)

    \(igual(Cero(), x) ::= esCero(x)\)
    \(igual(sucesor(x), cero()) ::= False\)
    \(igual(sucesor(x), sucesor(y)) ::= igual(x, y)\)

Observa que:

  1. En la parte de axiomas se utiliza el símbolo ::=, el cual se lee se define como y se conoce como la notación BNF (Backus Naur Form).

  2. En la parte correspondiente a las funciones se definen las operaciones que se pueden realizar, su sintaxis y el resultado que se obtiene al efectuar alguna de ellas.

  3. En la definición de funciones siempre se debe incluir:

    • Una función que cree la estructura vacía. En nuestro ejemplo es la función \(cero()\) la cual crea la estructura \(NumeroNatural\) vacía.

  • Una función que genere o que permita incluir los elementos o datos objeto que conformarán la estructura a definir. En nuestro ejemplo es la función \(sucesor()\)

  1. En la parte de axiomas se describe la forma como trabajan las funciones. Se omiten en esta parte las dos funciones básicas, (creación y generación) definidas anteriormente, ya que su forma de operación está implícita en la definición de la función.

La función cero() crea la estructura número natural vacía. sucesor(cero()) generará el primer número natural, el 1. sucesor(sucesor(cero())) representa el número natural 2 y así sucesivamente. En general, si sucesor(x) es un número natural i, entonces x representa el número natural i – 1.

Preguntas

Realiza las siguientes operaciones usando las funciones y axiomas definidos

  • 2+3

  • 5-2

  • 4*5

  • 12/4

  • 12/5

Ejemplo 2: Un diccionario

TAD: \(Diccionario\)

\(\forall\; A \in Diccionario \;\;\; i,j,v \in AnyType\)

  • signatures: $$

(2)\[\begin{align} crear() &\rightarrow Diccionario\\ A.almacena(v) &\rightarrow Diccionario\\ A.esVacio() &\rightarrow Bool\\ A.recupera(i) &\rightarrow AnyType\\ \end{align}\]

$$

  • axioms:

    \(crear().esVacio() := \) True
    \(A.almacena(i,v).esVacio() :=\) False
    \(crear().recupera(i) := error\)
    \(A.almacenar(i,v).recupera(j) := v\) if \(i==j\) else \(A.recupera(j)\)

Observa:

  • que siempre tenemos que tener alguna función que nos cree una instancia de un TAD

  • Python tiene una estructura dict con una semántica similar y con las funciones de almacenar y recuperar accesibles indirectamente a través de una sintaxis dedicada.

A = {"uno": "el primer elemento", "zapato": "para el pie", 3: "el numero 3", "dos": 2 }
for k,v in A.items():
    print("%10s"%str(k),"::", v)
       uno :: el primer elemento
    zapato :: para el pie
         3 :: el numero 3
       dos :: 2
print(A["uno"])
print(A[3])
print(A["dos"])
print(A["zapato"])
el primer elemento
el numero 3
2
para el pie
A["dos"] = "el numero dos"
A["comer"] = "verbo transitivo"

for k,v in A.items():
    print("%10s"%str(k),"::", v)
       uno :: el primer elemento
    zapato :: para el pie
         3 :: el numero 3
       dos :: el numero dos
     comer :: verbo transitivo
A["dos"]
'el numero dos'

Ejemplo 3. Lista Ordenada

Definición informal

  • tipo de datos:

    • los elementos de una lista ordenada son de un determinado tipo (p.ej. \(\mathbb{N}\), o string) que admite una función de comparación (1 < 2, “albert” < “arbol”).

  • funciones:

    • OrderedList(tipo): crea una lista ordenada vacía para elementos de tipo de datos tipo. Precondición: El tipo de datos ha de admitir comparación.

    • add(item): añade un nuevo ítem a la lista preservando el orden de la misma. Si el ítem ya existe en la lista la operación no modifica la lista. Precondición: El ítem ha de ser del tipo de la lista.

    • remove(item): elimina un ítem de la lista preservando el orden de la misma. Precondición: El ítem ha de existir en la lista.

    • contains(item): devuelve un booleano indicando si el ítem está en la lista.

    • len(): devuelve un entero con el número de elementos que tiene la lista.

    • first(): devuelve el primer item de la lista y establece un cursor en el primer item. Precondición: la lista no puede estar vacia.

    • next(): mueve el cursor al siguiente item de la lista y devuelve el item en el que inicialmente estaba. Precondición: el cursor no puede estar en el último item.

    • has_more(): devuelve un booleano True si el cursor está en el último item. Si no, devuelve False

Definición por axiomas

TAD: \(OrderedList\)

\(\forall \mathbb{T} \in Types, L \in OrderedList; a \in \mathbb{T}; k, n\in\mathbb{Z} \)

  • signatures: $$

(3)\[\begin{align} OrderedList (\mathbb{T}) &\rightarrow OrderedList\\ add(a) &\rightarrow OrderedList\\ remove(a) &\rightarrow OrderedList\\ contains(a) &\rightarrow Bool\\ len() &\rightarrow \mathbb{Z}\\ first() &\rightarrow \mathbb{T}\\ next() &\rightarrow \mathbb{T}\\ has\_more() &\rightarrow Bool\\ \end{align}\]

$$

  • definitions:

    • \(L^n ::= L.first(); \;\;\; L.next() \times (n-1) \;\;with\;\; n\in[1,len(L)]\)

    • \(k ::= n\) if \(L^n\)

    • \(L.has\_more() ::= k<len(L)\)

observa que la definición de \(k\) depende de cuantas veces haya llamado el usuario a la función \(next\) después de \(first\).

  • preconditions

operaciónprecondición
$OrderedList (\mathbb{T})$$\mathbb{T}$ admite comparación
$L.add(a)$$ a \in \mathbb{T}$
$L.remove(a)$$ L.contains(a)$
$L.first()$$len(L)>0$
$L^n$$n\in[1,len(L)]$
  • axioms (post conditions):

    • axiom 1: \(len(OrderedList(\mathbb{T}))=0\)

    • axiom 2: \(L^n < L^{n+1}\)

    • axiom 3: \(L.contains(a) \iff \exists n \;\;|\;\; a = L^n\)

    • axiom 4: \(len(L.add(a)) = len(L)+ 1 - L.contains(a)\)

    • axiom 5: \(len(L.remove(a)) = len(L) - L.contains(a)\)

    • axiom 6: \(L.add(a) \Rightarrow L.contains(a)\)

    • axiom 7: \(L.remove(a) \Rightarrow \neg L.contains(a)\)

Dada una implementación concreta, los axiomas nos permiten verificar si la implementación es correcta.

Implementación en Python

Observa:

  • el TAD se implementa como una clase de Python

  • las precondiciones las implementamos con assert

  • ¿qué tan eficiente es esta implementación?

class OrderedList:
    
    def __init__(self, item_type):
        self.item_type = item_type
        self.list = []
        self.cursor = 0
        
    def add(self, item):
        assert type(item)==self.item_type, "must comply with declared type"
        self.list = [i for i in self.list if i<item] + [item] + [i for i in self.list if i>item]
        return self
        
    def remove(self, item):
        assert self.contains(item), "item not in list"
        self.list = [i for i in self.list if item!=i]
        return self
    
    def contains(self, item):
        return item in self.list
    
    def first(self):
        self.cursor = 1
        return self.list[self.cursor-1]
    
    def __next__(self):
        assert self.has_more(), "no more items in list"
        self.cursor += 1
        return self.list[self.cursor-1]
    
    def has_more(self):
        return self.cursor<len(self.list)
    
    def len(self):
        return len(self.list)

comprobamos a mano la implementación

k = OrderedList(int)
k.add(10).add(1).add(2)
print(k.list)
k.remove(2)
print(k.list)
[1, 2, 10]
[1, 10]

comprobamos alguna precondición

print(k.first(), k.has_more())
print(next(k), k.has_more())
print(next(k))
1 True
10 False
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-7-ba9de29813c3> in <module>
      1 print(k.first(), k.has_more())
      2 print(next(k), k.has_more())
----> 3 print(next(k))

<ipython-input-5-5f8717398b95> in __next__(self)
     24 
     25     def __next__(self):
---> 26         assert self.has_more(), "no more items in list"
     27         self.cursor += 1
     28         return self.list[self.cursor-1]

AssertionError: no more items in list
k.add("ho")
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-8-f961c13bf47b> in <module>
----> 1 k.add("ho")

<ipython-input-5-5f8717398b95> in add(self, item)
      7 
      8     def add(self, item):
----> 9         assert type(item)==self.item_type, "must comply with declared type"
     10         self.list = [i for i in self.list if i<item] + [item] + [i for i in self.list if i>item]
     11         return self

AssertionError: must comply with declared type
k.remove(2)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-9-3774a04f67d2> in <module>
----> 1 k.remove(2)

<ipython-input-5-5f8717398b95> in remove(self, item)
     12 
     13     def remove(self, item):
---> 14         assert self.contains(item), "item not in list"
     15         self.list = [i for i in self.list if item!=i]
     16         return self

AssertionError: item not in list

SUGERENCIA ejecuta el código anterior en Python Tutor

tests unitarios con los axiomas

estos tests son independientes de la implementación

axiom 1: \(len(OrderedList(\mathbb{T}))=0\)

def test_axiom1(ordered_list_class):
    L = ordered_list_class(int)
    print("new list length:", L.len())
    assert L.len()==0
    
test_axiom1(OrderedList)
new list length: 0

axiom 2: \(L^n < L^{n+1}\)

def test_axiom2(ordered_list_class):
    import numpy as np
    
    L = ordered_list_class(int)
    print("inserting", end=' ') 
    for i in range(20):
        k = np.random.randint(1000)
        print(k, end=' ') 
        L.add(k)
    print("\n\ntesting with list", L.list, "\n")
    
    item = L.first()
    while L.has_more():
        k = next(L)
        print(item,"<",k, "  ", end=' ')
        assert item < k, "wrong list order: %d before %d"%(item,k)
        item = k
        
test_axiom2(OrderedList)
inserting 642 495 683 514 101 989 259 166 493 224 565 847 427 981 430 881 317 759 972 650 

testing with list [101, 166, 224, 259, 317, 427, 430, 493, 495, 514, 565, 642, 650, 683, 759, 847, 881, 972, 981, 989] 

101 < 166    166 < 224    224 < 259    259 < 317    317 < 427    427 < 430    430 < 493    493 < 495    495 < 514    514 < 565    565 < 642    642 < 650    650 < 683    683 < 759    759 < 847    847 < 881    881 < 972    972 < 981    981 < 989    

Otra implementación en Python

usamos la librería bisect. observa este ejemplo

import bisect        
l = [1,2,10,12,30]
print(bisect.bisect_left(l,11), bisect.bisect_right(l,11))
print(bisect.bisect_left(l,12), bisect.bisect_right(l,12))
bisect.insort_left(l,9)
print(l)
3 3
3 4
[1, 2, 9, 10, 12, 30]

Observa cómo usamos herencia para redefinir sólo los métodos que queremos

class OrderedList_bisect(OrderedList):
    
    def add(self, item):
        assert type(item)==self.item_type, "must comply with declared type"
        if not self.contains(item):
            bisect.insort_left(self.list, item)
        return self
        
    def remove(self, item):
        k = bisect.bisect_left(self.list, item)
        assert k<len(self.list) and self.list[k]==item,  "item not in list"        
        self.list = self.list[:k]+self.list[k+1:]
        return self
        
    def contains(self, item):
        k = bisect.bisect_left(self.list, item)
        return k<len(self.list) and self.list[k]==item

verificamos los axiomas que tenemos implementados

test_axiom1(OrderedList)
test_axiom1(OrderedList_bisect)
new list length: 0
new list length: 0
test_axiom2(OrderedList)
inserting 484 210 70 721 896 371 586 943 677 838 833 174 399 331 378 6 543 716 928 810 

testing with list [6, 70, 174, 210, 331, 371, 378, 399, 484, 543, 586, 677, 716, 721, 810, 833, 838, 896, 928, 943] 

6 < 70    70 < 174    174 < 210    210 < 331    331 < 371    371 < 378    378 < 399    399 < 484    484 < 543    543 < 586    586 < 677    677 < 716    716 < 721    721 < 810    810 < 833    833 < 838    838 < 896    896 < 928    928 < 943    
test_axiom2(OrderedList_bisect)
inserting 180 149 839 533 669 274 637 321 583 853 379 752 461 444 351 209 822 933 703 922 

testing with list [149, 180, 209, 274, 321, 351, 379, 444, 461, 533, 583, 637, 669, 703, 752, 822, 839, 853, 922, 933] 

149 < 180    180 < 209    209 < 274    274 < 321    321 < 351    351 < 379    379 < 444    444 < 461    461 < 533    533 < 583    583 < 637    637 < 669    669 < 703    703 < 752    752 < 822    822 < 839    839 < 853    853 < 922    922 < 933    

medimos rendimiento de ambas implementaciones

import numpy as np
l = np.random.randint(100000,size=1000)
print(len(l), len(np.unique(l)))
1000 997

para la operación de add

L1 = OrderedList(np.int64)
L2 = OrderedList_bisect(np.int64)
%%timeit
for i in l:
    L1.add(i)
146 ms ± 9.04 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit
for i in l:
    L2.add(i)
1.55 ms ± 37 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

para la operación contains

L1 = OrderedList(np.int64)
L2 = OrderedList_bisect(np.int64)
for i in l:
    L1.add(i)
    L2.add(i)
print(L1.len(), L2.len())
997 997
%%timeit
for i in l:
    L1.contains(i)
16.8 ms ± 146 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%%timeit
for i in l:
    L2.contains(i)
1.26 ms ± 30.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Ejercicios propuestos:

  • Complete la estructura númeroNatural incluyendo las operaciones módulo, potencia, factorial, máximo común divisor, mínimo común múltiplo, diferente, mayor, mayor o igual, menor y menor o igual.

  • Defina estructura númeroEntero incluyendo las mismas operaciones que en la estructura númeroNatural. Defina operaciones que considere necesarias para desarrollar los axiomas de las funciones definidas.

  • Defina una estructura Conjunto con operaciones Unión, Intersección, Pertenencia y Complemento.

  • Defina estructura String con operaciones esVacío, longitud, concatenación, subHilera y posición.

  • Defina estructura Lógico con sus correspondientes funciones y axiomas.