¿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