¿Qué hace esa
gran O en
mis Estructuras
de Datos?

ADVERTENCIA

El código en esta presentación no tiene intenciones de ser usado
en un proyecto real, simplemente explica
un concepto

ADVERTENCIA

Esta presentación se enfoca en colecciones que no están enfocadas en concurrencia, que son las que usamos en la mayoría de nuestro día a día

Complejidad
Notación Gran O

¿Cómo medimos la
rapidez de un algoritmo
de modo que no
dependa del hardware?

Podemos tomar tiempos con muestras diferentes
y comparar el orden de magnitud en que
cambian los tiempos

Es útil no concentrarse
en los detalles y mirar el panorama general

Los algoritmos se ajustan a gráficas matemáticas

Los Mejores

O(1) < O(log n)

Los Razonables

O(n) < O(n log n)

Los Desafortunados

O(n^2) < O(n^3)

Usar con cuidado

O(a^n)

Existe un proceso matemático para determinar la complejidad, pero hay simples heurísticas que son útiles

Los Mejores

O(1) - constante

  • Retornar un atributo, operaciones
    como sumar 2 valores, etc
  • p.j. todos los métodos en Math

O(log n) - logarítmico

  • El proceso para llegar a un valor se puede
    modelar como un árbol n-ario
    (usualmente binario), en que el
    algoritmo recorre una sola rama
  • La altura de un árbol n-ario
    balanceado de n hojas es log n
  • p.j. búsqueda binaria
int indexOf(int[] a, int key) {
  int lo = 0;
  int hi = a.length - 1;
    while (lo <= hi) {
      int mid = lo + (hi - lo) / 2;
      if      (key < a[mid]) hi = mid - 1;
      else if (key > a[mid]) lo = mid + 1;
      else return mid;
    }
  return -1;
}

Los Razonables

O(n) - lineal

  • Algoritmo que tengan un ciclo
  • No importa, p.j. si el ciclo va hasta el
    final de la colección, o hasta la mitad,
    la complejidad es O(n) y no O(n/2)
  • En casos como los grafos, la complejidad
    incluye 2 magnitudes: O(|V|+|E|)
  • p.j. métodos como replace en String
String replace(char oldChar, char newChar) {
  if (oldChar != newChar) {
    int len = this.value.length;
    int i = -1;
    while (++i < len) {
      if (value[i] == oldChar) {
        break;
      }
    }
    if (i < len) {
      char buf[] = new char[len];
      for (int j = 0; j < i; j++) {
        buf[j] = value[j];
      }
      while (i < len) {
        char c = value[i];
        buf[i] = (c == oldChar) ? newChar : c;
        i++;
      }
      return new String(buf);
    }
  }
  return this;
}

O(n log n) lineal logarítmico

  • El problema se puede modelar como un
    árbol binario, en que el algoritmo tiene
    que recorrer todas las ramas de un árbol n-ario
  • De nuevo, en un árbol así, cada rama mide log n, recorrerlas todas toma n log n.
  • p.j. los algoritmos más usados en ordenamiento
void sort(int[] a, int[] aux, int lo, int hi) {
  if (hi <= lo) return;
  int mid = lo + (hi - lo) / 2;
  sort(a, aux, lo, mid);
  sort(a, aux, mid + 1, hi);
  merge(a, aux, lo, mid, hi);
}

void merge(int[] a, int[] aux, int lo, int mid, int hi) {
  for (int k = lo; k <= hi; k++) {
    aux[k] = a[k]; 
  }
  
  int i = lo, j = mid+1;
  for (int k = lo; k <= hi; k++) {
    if      (i > mid)         a[k] = aux[j++];
    else if (j > hi)          a[k] = aux[i++];
    else if (aux[j] < aux[i]) a[k] = aux[j++];
    else                      a[k] = aux[i++];
  }
}

Los Desafortunados

O(n^2) - cuadrático

  • Algoritmos con 2 ciclos anidados
  • "No importa" lo que se haga dentro de
    los ciclos, el algoritmos sigue siendo O(n^2)
  • No es una complejidad deseable,
    aunque puede ser inevitable
  • p.j. operaciones en una matriz
String pyramid(int size) {
  StringBuilder builder = new StringBuilder();
  int spaces = size - 1;
  int characters = 1;
  for (int row = 1; row <= size; ++row) {
     for (int s = 1; s <= spaces; ++s) {
        builder.append(" ");
     }
     for (int c = 1; c <= characters; ++c) {
        builder.append("*");
     }
     --spaces;
     characters += 2;
  }
  return builder.toString();
}

O(n^3) - cúbico

  • Algoritmos con 3 ciclos anidados
  • "No importa" lo que se haga dentro de
    los ciclos, el algoritmos sigue siendo O(n^3)
  • No es una complejidad deseable,
    aunque puede ser inevitable
  • p.j. Floyd Warshall
for (int v = 0; v < V; v++) {
  for (int w = 0; w < V; w++) {
    distTo[v][w] = Double.POSITIVE_INFINITY;
  }
}
for (int v = 0; v < G.V(); v++) {
  for (DirectedEdge e : G.adj(v)) {
    distTo[e.from()][e.to()] = e.weight();
    edgeTo[e.from()][e.to()] = e;
  }
  if (distTo[v][v] >= 0.0) {
    distTo[v][v] = 0.0;
    edgeTo[v][v] = null;
  }
}
for (int i = 0; i < V; i++) {
  for (int v = 0; v < V; v++) {
    if (edgeTo[v][i] == null) continue;  // optimization
    for (int w = 0; w < V; w++) {
      if (distTo[v][w] > distTo[v][i] + distTo[i][w]) {
        distTo[v][w] = distTo[v][i] + distTo[i][w];
        edgeTo[v][w] = edgeTo[i][w];
      }
    }
  }
}

Usar con cuidado

O(a^n) - exponencial

  • Algoritmos recursivos en los elementos
    a procesar no se reducen considerablemente 
  • Se vuelven increíblemente lentos para n >= 20
  • p.j. Torres de Hanoi
void move(int n, char source, char target, char auxiliary) {
  if (n < 0) {
    return;
  }
  
  move(n - 1, source, auxiliary, target)
  println(String.format("Move %i from %s to %s", n, source, target))
  move(n - 1, auxiliary, target, source)
}

Con estas heurísticas podemos tener un
análisis de complejidad bastante acertado, útil para la mayoría de los casos

Ten en cuenta siempre el orden de menor a mayor:
O(1) < O(log n) <
O(n) < O(n log n) <
O(n^2) < O(n^3) <
O(a^n)

Arreglos

Arreglo

Conjunto homogéneo
de elementos contiguos
en memoria

Los arreglos suelen
usarse como detalles
de implementación
de colecciones

Debido a su invariante, presentan acceso aleatorio; consultar cualquier posición toma O(1)

int[] data = new int[]{10, 5, 3};

| 1100 | 1104 | 1108 | -> dirección
+------+------+------+
|  10  |   5  |   3  | -> elementos
            
// Dado un arreglo, "data" de objecto T
// para saber la dirección en memoria
// del i-ésimo elemnto, siendo sizeof(T)
// una operación que devuelve el tamaño de T
// la fórmula es:

//              data + sizeof(int)   * i
// data[2] -> *(1100 +     4         * 2) == 1108

¿Qué complejidad tiene agregar un elemento más allá del tamaño inicial?

public int[] agrandar(int[] data, int longitud) {
  int[] copia = new int[longitud];
  for (int i = 0; i < data.length; ++i) {
    copia[i] = data[i];
  }
  return copia;
}

O(n)

public int[] set(int[] data, int índice, int e) {
  int[] copia = new int[data.length + 1];
  int i = 0;
  while (i < índice) {
   copia[i] = data[i];
   ++i;
  }
  copia[i] = e;
  ++i;
  while (i < copia.length) {
    copia[i] = data[i - 1];
    ++i;
  }
  return copia;
}

¿Qué complejidad tiene buscar un elemento?

Búsqueda

  • Ordenar - O(n log n)
  • Búsqueda Binaria - O(log n)

Arreglos

  • ¡Acceso aleatorio!
  • Otras operaciones: O(n), O(log n), O(n log n)
  • Úsense solo como detalles de implementación,
    no deben hacer parte del API de las clases
  • Hardware moderno carga en cache la memoria adyacente a la que un SO solicita, lo que hace que
    los arreglos sean muy eficientes para operaciones como recorrerlos, que es la base de otras operaciones

Comparable - Comparator

Muchas colecciones
utilizan una operación que permite establecer un orden entre elementos

Comparable

Esta interface hace explícito un ordenamiento
en los objetos de la clase que la implementa

/**
 * Establece el orden natural de T
 */
public interface Comparable<T> {
  /**
   * Retorna un entero negativo, cero, o un entero positivo 
   * si este objeto es menor, igual o mayor que otro.
   */
  int compareTo(T otro);
}

¿Por qué un valor negativo, cero o un valor positivo?

a - b

  • es negativo si a < b
  • es cero si        a == b
  • es positivo si  a > b
public class Niño 
    implements Comparable<Niño> {
  private String nombre;
  private String apellido;
  private int edad;

  // código

  @Override
  public int compareTo(Niño otro) {
    return this.edad - otro.edad;
  }
}

¡Cuidado!

La resta de 2 números muy
grandes puede causar overflow

public class Niño 
    implements Comparable<Niño> {
  private String nombre;
  private String apellido;
  private int edad;

  // código

  // Si Overflow es una preocupación mejor usar
  // código de este estilo:
  @Override
  public int compareTo(Niño otro) {
    if (this.edad < otro.edad)       return -1;
    else if (this.edad == otro.edad) return  0;
    else                             return  1;
  }
}
public class Niño 
    implements Comparable<Niño> {
  private String nombre;
  private String apellido;
  private int edad;

  // código

  // En general siempre es mejor 
  // apoyarse en el lenguaje
  @Override
  public int compareTo(Niño otro) {
    return Integer.compare(this.edad, otro.edad);
  }
}

¿Cómo se ordena por
más de un criterio?

// Horrible, antes de Java 8
public int compareTo(Niño otro) {
  int cmp1 = Integer.compare(this.edad, otro.edad);
  if (cmp1 != 0) {
    return cmp1;
  }
  int cmp2 = this.apellido.compareTo(otro.apellido);
  if (cmp2 != 0) {
    return cmp2;
  }
  return this.nombre.compareTo(otro.nombre);
}

Comparator

Permite establecer otros ordenamientos distintos
al de Orden Natural definido por Comparable

@FunctionalInterface
public interface Comparator<T> {
  /**
   * Mismo concepto que Comparable#compareTo
   */
  int compare(T o1, T o2);
}
List<String> data = Arrays.asList("ba", "b", "aa");
Collections.sort(data); // [aa, b, ba]
// Horrible, antes de Java 8
Collections.sort(data, new Comparator<String>() {
  @Override
  public int compare(String s1, String s2) {
    int cmp = Integer.compare(s1.length(), s2.length());
    if (cmp != 0) {
      return cmp;
    }
    return s2.compareTo(s1);
  }
}); // [b, ba, aa]

Java 8

List<String> data = Arrays.asList("ba", "b", "aa");

Collections.sort(data, 
    Comparator.comparingInt(String::length)); // [b, ba, aa]

Collections.sort(data, 
    Comparator.comparingInt(String::length)
              .reversed()
              .thenComparing(String::toString)); // [aa, ba, b]
@Override
public int compareTo(Niño otro) {
  return Comparator.comparingInt(Niño::getEdad)
		   .thenComparing(Niño::getApellido)
                   .thenComparing(Niño::getNombre)
                   .compare(this, otro); 
                   // ¿Cuáles son los objetos a comparar?
}

Comparable en Java 8

¡Esta no es una exposición exhaustiva de Comparator! 

Collection

boolean     add(E e)
boolean     addAll(Collection<E> c)
void        clear()
boolean     contains(Object o)
boolean     isEmpty()
Iterator<E> iterator()
boolean     remove(Object o)
boolean     removeAll(Collection<E> c)
boolean     removeIf(Predicate<E> filter)
boolean     retainAll(Collection<E> c)
int         size()
Stream<E>   stream()
Object[]    toArray()
T[]         toArray(T[] a)

List

List

Una colección de elementos que tiene un orden; hay un primer elemento, un segundo elemento, etc.

void	add(int index, E element)
E	get(int index)
int	indexOf(Object o)
E	remove(int index)
E	set(int index, E element)
List<E>	subList(int fromIndex, int toIndex)

ArrayList

Una implementación de List que utiliza un arreglo

public class ArrayList<E> 
      implements List<E> {
  private Object[] elementData;

   // más código
}

¿Cómo se agranda?

Agregar

  • Un ArrayList tiene 2 características: capacidad y tamaño, con la invariante que capacidad >= tamaño
  • La capacidad es el tamaño
    actual del arreglo subyacente
  • A medida que agrego elementos,
    los agrego al final del arreglo - O(1)
  • Cuando agrego un elemento y la capacidad,
    es igual al tamaño creo un nuevo arreglo de
    mayor tamaño, copio los elementos al nuevo
    arreglo y agrego el elemento al final - O(n)
  • Complejidad: O(1)*
public class ArrayList<E> implements List<E> {
   // código
  private int size = 0;

  public ArrayList() {
    this.elementData = new Object[10];
  }

  public boolean add(E e) {
    asegurarCapacidad(this.size + 1);
    elementData[size++] = e;
    return true;
  }

  private void asegurarCapacidad(int minCapacity) {
    if (minCapacity - elementData.length > 0) {
      int oldCapacity = elementData.length;
      int newCapacity = oldCapacity + (oldCapacity >> 1);
      elementData = Arrays.copyOf(elementData, newCapacity);
    }
  }
}

Otras operaciones

  • get(i) : O(1)
  • contains(E): O(n)
  • Collections.sort: O(n log n)

¡Que la capacidad inicial sea 10 y que crezca
en 1.5 son detalles
de implementación
y pueden cambiar en futuras versiones!

LinkedList

Una implementación
de List que usa
una lista
doblemente encadenada

public class LinkedList<E> implements List<E> {
  private int size = 0;
  private Node<E> first;
  private Node<E> last;

  public LinkedList() {
  }
  
  // más código
  private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
      this.item = element;
      this.next = next;
      this.prev = prev;
    }
  }
}
public class LinkedList<E> implements List<E> {
  // más código
  public E get(int index) {
    return node(index).item;
  }
  Node<E> node(int index) {
    if (index < (size >> 1)) {
      Node<E> x = first;
      for (int i = 0; i < index; i++)
        x = x.next;
      return x;
    } else {
      Node<E> x = last;
        for (int i = size - 1; i > index; i--)
          x = x.prev;
             return x;
         }
     }
  }
}
public class LinkedList<E> implements List<E> {
  // más código
  public boolean add(E e) {
    linkLast(e);
    return true;
  }
  void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
      first = newNode;
    else
      l.next = newNode;
    size++;
  }
}
public class LinkedList<E> implements List<E> {
  // más código
  public void add(int index, E element) {
    if (index == size)
      linkLast(element);
    else
      linkBefore(element, node(index));
  }
  void linkBefore(E e, Node<E> succ) {
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
      first = newNode;
    else
      pred.next = newNode;
    size++;
  }
}

Desempeño

  • add(E) : O(1)
  • add(i, E) : O(n)
  • get(i) : O(n)

¿Cuándo usarlas?

ArrayList y LinkedList son complementarios...

...pero, ArrayList aprovecha características de
hardware moderno

Para la gran mayoría de los casos en que usamos un List que no necesita ser usado en concurrencia es
mejor usar ArrayList

Map

Map

Una colección que
asocia objetos de tipo K llamados llaves con objetos de tipo V llamados valores

Para una llave K solo se puede asociar un valor V

TreeMap

Una implementación de
Map que usa un
árbol binario
de búsqueda (BST)

Árbol

Una estructura de
datos jerárquica:
cada nodo tiene hijos, existe un nodo que
no es hijo de otro nodo llamado raíz
y no hay ciclos

Árbol Binario

En un árbol binario,
los nodos tienen
a lo sumo 2 hijos

Árbol Binario de Búsqueda - BST

Un árbol binario con
la invariante de que el padre es "mayor" que el
hijo de la izquierda,
pero "menor" que
el de la derecha

BST

Un BST mantiene la colección ordenada, perfecto para una búsqueda binaria

public class TreeMap<K,V> {
  private final Comparator<K> comparator;
  private Node<K, V> root;
  private int size = 0;

  static final class Node<K,V> {
    K key;
    V value;
    Node left;
    Node right;

    Node(K key, V value) {
      this.key = key;
      this.value = value;
    }
  }
  // más código
}
public V get(K key) {
  return get(root, key);
}

private V get(Node x, K key) {
  if (x == null) return null;
  int cmp = this.comparator(key, x.key);
  if      (cmp < 0) return get(x.left, key);
  else if (cmp > 0) return get(x.right, key);
  else              return x.val;
}

public boolean contains(Key key) {
  return get(key) != null;
}
private Node put(Node x, K key, V val) {
  if (x == null) return new Node(key, val);
  int cmp = this.comparator(key, x.key);
  if      (cmp < 0) x.left  = put(x.left,  key, val);
  else if (cmp > 0) x.right = put(x.right, key, val);
  else              x.val   = val;
  return x;
}

Después de operaciones
de adición o de eliminación, se ejecuta un proceso
de balanceo, que toma O(1)

Si el árbol está balanceado

  • add(E) - O(log n)
  • contains(E) - O(log n)
  • remove(E) - O(log n)

TreeMap es también
un SortedMap
y un NavigableMap

SortedMap

Interface que añade métodos relacionados
que aprovechan que los elementos están ordenados

SortedMap

  • firstKey()
  • lastKey()
  • subMap(K fromKey, K toKey)
  • tailMap(K fromKey)

NavigableMap

Una interface que agrega métodos para hacer búsquedas aproximadas

Navigable Map

  • ceilingKey(K key)
  • floorEntry(K key)

HashMap

Una implementación
de Map que usa
un "hash table"

Si un arreglo tiene acceso aleatorio, sería genial si de alguna manera pudiera relacionar un objeto a un índice de manera rápida

Hashcode

Es una función que representa aleatóriamente el estado de un
objeto como un int

public int hashCode(String s) {
  int h = 0;
  for (int i = 0; i < s.length(); i++) {
    h = 31 * h + s.charAt(i);
  }
  return h;
}

Posible implementación para String

hashcode

  • ¡puede ser cualquier int!
  • ¡puede ser 0!
  • ¡puede ser negativo!
  • ¡2 objetos con estado distinto
    pueden tener el mismo hashcode!

¿Cómo podemos
escribir nuestra propia implementación
de hashcode?

Effective Java trae
una receta para eso,
pero ahora hay una
manera más sencilla

class Foo {
  private String a;
  private int b;
  private double c;
	
  @Override
  public int hashCode() {
    return Objects.hash(a, b, c);
  }
}

Volviendo al hash table, esta mantiene un arreglo, llamado cubetas,
y usa el hashcode para almacenar los elementos de la colección

¿Si podemos relacionar un objeto con un int, cómo lo transformamos en un índice de un arreglo?

¡Aplicamos una función
de compresión!

private int bucket(Object o) {
   return o.hashCode() % arreglo.length();
}

La solución más simple

¡Genial, ahora podemos hacer todas las operaciones de una colección de
forma muy eficiente!

add(E)

  • Calculamos el hashCode
  • aplicamos la función de compresión
  • almacenamos el elemento en esa cubeta
  • O(1) - siempre y cuando hashCode sea O(1)

contains, delete y similares se implementan igual
¡Todo es perfecto!

No todo es perfecto ¿qué pasa si hay una colisión?

Colisión: 2 objetos terminan en la misma cubeta, sea porque tienen el mismo hashcode o porque usan la misma cubeta después de la función de compresión

Existen varias estrategias: Java escogió chaining

Cada cubeta no tiene un elemento, tiene una lista encadenada de elementos

add(E) revisado

  • calcula el hashCode
  • aplica la función de
    compresión para saber la cubeta
  • recorre la lista encadena buscando
    si el elemento ya está presente.
    Si no lo está, lo agrega al final

Todas las demás operaciones son similares y nuevamente toman O(1)

¡No tan rápido
hay un gran PERO!

Si recorremos una
lista encadenada, ya
no es O(1) sino que es proporcional al tamaño
de la lista encadena

Pero si la lista es vacía o tiene un elemento y muy rara vez es mayor pero igual muy, muy pequeña, entonces el tiempo es O(1)*

hashtables tienen un Factor de Carga, cuando la relación entre elementos en la colección y número de cubetas sobrepasa ese número, se agranda el número de cubetas

agrandar cubetas

  • Factor de Carga 0.75
  • Tengo 16 cubetas y 13 elementos,
    la relación es 0.8125
  • Duplico el número de cubetas
    y para cada elemento en la colección,
    calculo el hashcode aplico la función
    de compresión del nuevo número
    de cubetas y almaceno ahí
  • Operación toma O(n)
  • 32 cubetas y 13 elementos - 0.40625
  • Operación solo volverá a suceder
    al llegar a 25 elementos

hashtable

  • add(E) - O(1)*
  • contains - O(1)
  • delete - O(1)

Pero no hay ninguna relación de orden
entre los elementos,
esa es la limitación

En Java para el correcto funcionamiento debe haber una relación entre equals() y hashCode()

Contrato entre
equals() y hashCode()

  • Dos objetos con el mismo estado,
    deben tener el mismo hashCode
  • Dos objetos con el mismo hashCode,
    no tienen que ser iguales
  • Dos objetos que son iguales,
    usando equals, tienen el mismo hashCode

Hay otro "pero" que pasamos por alto, las operaciones son O(1)* siempre y cuando hashCode() tome O(1)

¿El hashCode de String
si va a tener en cuenta todos los caracteres, implica que toma O(n), cómo minimizamos
esa complejidad?

String hace un cache
del valor y sólo lo evalúa
de manera lazy

public final class String {
  private final char value[];
  private int hash = 0; 

  // más código
  public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
      char val[] = value;

      for (int i = 0; i < value.length; i++) {
        h = 31 * h + val[i];
      }
      hash = h;
    }
    return h;
  }
}

¡Y por último: las llaves deben ser inmutables!

Los Strings son
las llaves ideales

TreeMap

  • Ofrece muchas operaciones relacionadas
    con el ordenamiento de los elementos
  • Ofrece garantías sobre la
    complejidad de sus operaciones

HashMap

  • Ofrece O(1) para todas sus operaciones...
  • ... aunque la adición es O(1)*
  • La noción de ordenamiento se pierde

¿Cuándo usarlas?

TreeMap, si la garantía
de tiempo es importante
o si las operaciones
de ordenamientos
son necesarias

HashMap, si el desempeño es lo más importante, aunque no sea garantizado

Set

Set

Una colección sin elementos repetidos

Para ser exactos: si trato
de agregar un elemento que ya está, esa
operación no modifica
el estado de la colección

La manera más simple
de implementar Set
es usando un Map

public class HashSet<E> {
  private HashMap<E,Object> map;
  private static final Object PRESENT = new Object();

  // más código
  public boolean add(E e) {
    return map.put(e, PRESENT)==null;
  }

  public boolean contains(Object o) {
    return map.containsKey(o);
  }
}       
public class TreeSet<E> {
  private NavigableMap<E,Object> m;
  private static final Object PRESENT = new Object();

  // más código

  public boolean add(E e) {
    return m.put(e, PRESENT)==null;
  }

  public boolean contains(Object o) {
    return m.containsKey(o);
  }
}

Las razones para escoger uno u
otro son las mismas que con Map

¿Cuándo usar
Set sobre List?

Set si es importante asegurar que no haya elementos repetidos o si necesitamos operaciones relacionadas con el ordenamiento

Queue

Queue

Una colección especializada en
retornar elementos en
un orden específico

Queue

Una colección
con comportamiento
"primero en entrar,
primero en salir"

Stack

Una colección con comportamiento
"último en entrar,
primero en salir"

Deque

Una interfaz que permite operaciones de Stack/Queue

public interface Deque<E> {
  void addFirst(E e);
  void addLast(E e);
  E pollFirst();
  E pollLast();
  boolean isEmpty();
}

ArrayDeque

Una implementación de Deque que utiliza un arreglo circular

ArrayDeque

  • Mantiene 2 índices: cabeza y cola
  • Si agrego / quito al final, muevo cola
  • Si agrego / quito en el principio, muevo cabeza
  • Si la cola excede el último índice y
    hay espacio en la cabeza, el índice va allá
  • Si la cabeza se corre después de la cabeza
    y hay espacio en la cola, el índice va allá
  • Cuando los índices se encuentran,
    la capacidad se duplica. La capacidad inicial es siempre una potencia de 2 y la por defecto es 8

ArrayDeque

  • Inserciones y eliminaciones toman O(1)*
  • Todas las demás operaciones toman O(n)

Hay algunos algorítmos
de uso general que dependen de Stack/Queue como BFS y DFS

PriorityQueue

Una implementación que retorna los elementos según su ordenamiento

Árbol perfectamente balanceado: no hay un nodo a distancia k + 1
hasta que existan todos los nodos a distancia k

Un Heap se puede implementar con un arreglo

Heap: un árbol perfectamente balanceado con la invariante que el padre es siempre "menor" que sus 2 hijos

Específicamente si la raíz
es el "menor" se llama un min-heap si es el "mayor" se llama un max-heap

public class PriorityQueue<E> {
  private E[] elements = (E[]) new Object[8];
  private int size = 0;

  public boolean add(E e) {
    elements[size++] = e;
    heapify();
  }

  private void heapify() {
    int index = size;
        
    while (hasParent(index) && less(index, parent(index))) {
      swap(index, parentIndex(index));
      index = parentIndex(index);
    }        
  }
}
public class PriorityQueue<E> {

  public E element() {
    E e = elements[0];
    swap(0, size);
    elements[size--] = null;
    makeHeap();
    return e;
  }
  // código
}
private void makeHeap() {
  int index = 0;
  while (hasLeftChild(index)) {
    int smallerChild = leftIndex(index);
    if (hasRightChild(index)
        && greater(leftIndex(index), rightIndex(index))) {
        smallerChild = rightIndex(index);
    } 
    if (greater(index, smallerChild)) {
      swap(index, smallerChild);
    } else {
      break;
    }
    index = smallerChild;
  }        
}

PriorityQueue

  • add(E) - O(log n)*
  • element() - O(log n)

Algoritmos que usan
una PriorityQueue

  • Simulación de eventos discretos
  • Algoritmo de Dijkstra
  • Huffman Coding
  • A*
  • etc.

Miscelania

Inmutabilidad

Es muy mala práctica mantener o dar una referencia a un
objeto inmutable

public class Foo {
  private Set<String> data;

  public Foo(final Set<String> data) {
     // Todas las colecciones 
     // tienen constructores de copia
     this.data = new HashSet<>(data);
  }
}
public class Foo {
  private Set<String> data;

  // más código

  public Set<String> getData() {
    // Collections.unmodifiableCollection()
    // Collections.unmodifiableList()
    // Collections.unmodifiableMap()
    // Collections.unmodifiableNavigableMap()
    // Collections.unmodifiableNavigableSet()
    // Collections.unmodifiableSet()
    // Collections.unmodifiableSortedMap()
    // Collections.unmodifiableSortedSet()
    return Collections.unmodifiableSet(data);
  }
}

NULL

¡Nunca agreguen null
a una colección!

!En general nunca usen null!

Streams

Reemplaza todas los
usos de iteración de colecciones con stream()

StreamUtils.toStream(page.listChildren())
           .filter(child -> Pages.isPublished(child))
           .map(child -> Link.of(child))
           .limit(MAX_LINKS)
           .collect(Collectors.toList());

Literales

Java no tiene literales
para colecciones, pero...

Arrays.asList("A", "B", "C");
new HashSet<>(Array.asList(1, 2, 3));

¡Java 9!

List.of("A", "B", "C");
Set.of(1, 2, 3);
Map.of("foo", 1, "bar", 2, "baz", 3);
Map.ofEntries(
    entry("foo", 1), 
    entry("bar", 2), 
    entry("baz", 3)
);

¡Lean el API de
Arrays y Collections!

Desempeño

Muchos de los problemas de desempeño
asociados con memoria, están relacionados
con colecciones

Desempeño

  • Colecciones vacías
  • Colecciones de un elemento
  • Usar siempre la misma colección

¿Si como parte de un método debemos retornar una colección vacía, qué código usamos?

List<Post> allPosts = 
    (!appId.isEmpty() && !appSecret.isEmpty())
        ? getPosts(screenName, appId, appSecret)
        : new ArrayList<>();

Una nueva colección,
no está vacía

  • ArrayList -> 16
  • HashMap / HashSet -> 16 
  • ArrayDeque -> 8
  • PriorityQueue -> 11

Y semánticamente no es lo mismo una lista vacía que una lista recién creada

List<Post> allPosts = 
    (!appId.isEmpty() && !appSecret.isEmpty())
        ? getPosts(screenName, appId, appSecret)
        : Collections.emptyList();
       // Collections.emptySet()
       // Collections.emptySet()
       // Collections.emptyMap()
       // Collections.emptyIterator()
       // ...

Cada uno de esos métodos retorna un Singleton

Para listas no vacías,
no olvides que existe
new ArrayList<>(int)

También Arrays.asList
y List.of

"La optimización
prematura es la raíz
de todos los males
"

La palabra clave de esa
cita es prematura

Crear colecciones
con el tamaño adecuado, no es prematuro

Para las demás colecciones, es mejor no usar esos constructores que modifican la capacidad inicial, el factor de carga, etc. a menos que
estés muy, muy, muy seguro de lo que haces

¿De dónde salen
esas colecciones de
un elemento?

Algunos API decidieron que no tenía sentido tener 2 versiones de métodos: una que devolvía un T y otra que tuviera List<T>

¿Por qué tener
una colección de 1
puede ser optimizado?

Colección Singleton

  • No hay un arreglo, simplemente un atributo
  • Métodos súper especializados, p.j: size(), sort(), get(i)

Colección Singleton

  • Collections.singleton()
  • Collections.singletonList()
  • Collections.singletonMap()
  • List.of(), Set.of(), Map.of()

Ser el programador
del ArrayList

Specification getSpecifications(String id, 
    List<Product> products, List<Specification> specs) {
  for (Product product: products) {
    if (product.getId().equals(id)) {
      for (Specification spec: specs) {
        if (spec.getCode().equals(product.getCode()) {
           return spec;
        }
      }
    }
  }
  return null;
}

Una historia de la vida real

Conozcan las estructuras de datos y úsenlas bien

Otras estructuras
de Datos

Boggle

  • Un juego de armar palabras
  • El usuario encuentra palabras y el
    juego tiene que validarlas como correctas
  • En español cada verbo tiene alrededor de 50 conjugaciones, correr, corro, corrés, corre, corréis, corren, corría, corrías, corríamos, corra, corriere, corrieres, corriéremos, corriera, etc.
  • ¿Qué tal arrastrarse? La mayoría de sus
    conjugaciones compartirían las primeras 7 letras

Trie es un árbol en que cada nodo tiene un hijo que representa una letra

Siempre vale la pena indagar si hay una mejor estructura de datos que las que vienen en un lenguaje 

Estructuras de
Datos Inmutables

Librerías

  • Guava
  • Eclipse Collections
  • Apache Common Collections

Referencias

Coursera

YouTube

Libros

Web

Q&A

¡Gracias!

@gaijinco

¿Qué hace esa gran O en mis Estructuras de Datos?

By Carlos Obregón

¿Qué hace esa gran O en mis Estructuras de Datos?

Una charla sobre Complejidad, Arreglos, Lis, Set, Map, Queue y desempeño

  • 2,612