Cheatsheet de Algoritmos en Golang (Go)

  ·   13 minutos

Algoritmos en Golang

Arrays

Estructura de datos que organiza datos secuencialmente en memoria contigua.

  • O(1) lectura indexada
  • O(n) inserción/eliminación aleatoria
  • O(n) búsqueda
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
mylist := []int{1, 2, 3}
size := len(mylist)

for _, val := range mylist {
    // pasar
}

for index, val := range mylist {
    // pasar
}

mylist = append(mylist, 4)        // -> [1, 2, 3, 4]
mylist = append(mylist, 5, 6)     // -> [1, 2, 3, 4, 5, 6]

Ordenamiento

El ordenamiento es el proceso de reorganizar los datos en un array, de modo que se almacenen en orden ascendente o descendente. Existen muchos algoritmos para ordenar, pero la mejor complejidad es O(nlogn).

1
2
3
4
5
6
7
8
// Implementación incorporada de ordenamiento
import "sort"

sort.Ints(mylist)

sort.Slice(mylist, func(i, j int) bool {
    return mylist[i][0] < mylist[j][0]
})

Quicksort

El algoritmo de ordenamiento más común, es la implementación incorporada en muchos lenguajes. Este algoritmo es muy adecuado para ordenar arrays en el lugar.

Esta técnica utiliza divide y vencerás para dividir el problema en subproblemas más pequeños seleccionando un pivote en el array y comparando el resto con el pivote.

  • O(nlogn) - complejidad
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func quickSort(arr []int) []int {
    if len(arr) <= 1 {
        return arr
    }

    pivot := arr[len(arr)-1]
    lessThanPivot := []int{}
    equalToPivot := []int{}
    greaterThanPivot := []int{}

    for _, element := range arr {
        if element < pivot {
            lessThanPivot = append(lessThanPivot, element)
        } else if element > pivot {
            greaterThanPivot = append(greaterThanPivot, element)
        } else {
            equalToPivot = append(equalToPivot, element)
        }
    }

    left := quickSort(lessThanPivot)
    right := quickSort(greaterThanPivot)

    result := append(left, equalToPivot...)
    result = append(result, right...)

    return result
}

Merge Sort

Es una técnica recursiva para ordenar un array, es muy común para el ordenamiento paralelo porque puede particionar los datos entre muchos procesos.

Este algoritmo utiliza una técnica de divide y vencerás y funciona muy bien porque fusionar arrays ya ordenados es una operación O(n).

  • O(nlogn) - complejidad
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func merge(arr1, arr2 []int) []int {
    result := []int{}
    i, j := 0, 0
    for i < len(arr1) && j < len(arr2) {
        if arr1[i] < arr2[j] {
            result = append(result, arr1[i])
            i++
        } else {
            result = append(result, arr2[j])
            j++
        }
    }
    result = append(result, arr1[i:]...)
    result = append(result, arr2[j:]...)
    return result
}

func mergeSort(arr []int) []int {
    if len(arr) <= 1 {
        return arr
    }
    mid := len(arr) / 2
    left := mergeSort(arr[:mid])
    right := mergeSort(arr[mid:])
    return merge(left, right)
}

Búsqueda Binaria

Este algoritmo mejora la búsqueda en listas que están ordenadas. Generalmente, buscar un valor en una lista tiene complejidad O(n), pero si la lista ya está ordenada, esta técnica puede hacerlo en O(logn), lo cual es mucho mejor.

Esta técnica utiliza un enfoque de divide y vencerás para reducir el espacio de búsqueda saltando valores irrelevantes.

  • O(logn) - complejidad

Si el elemento no se encuentra, el elemento debería estar en la posición anotada con left.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func binarySearch(arr []int, value int) int {
    if len(arr) == 0 {
        return -1
    }
    left, right := 0, len(arr)-1
    for left <= right {
        pivot := (left + right) / 2
        if arr[pivot] < value {
            left = pivot + 1
        } else if arr[pivot] > value {
            right = pivot - 1
        } else {
            return pivot
        }
    }
    return -1
}

Dos Punteros

Técnica para recorrer un array y encontrar un subarray, es útil en problemas donde la solución ingenua es un doble bucle. Problemas comunes están relacionados con subarrays o intervalos, aprovechando un array ordenado.

1
2
3
4
5
6
7
// Encontrar subarray con dos punteros
left, right := 0, len(mylist)-1
for left < right {
    // hacer algo
    left++
    right--
}

Intervalos

Los intervalos representan rangos definidos por un punto de inicio y fin, comúnmente usados en programación de horarios y problemas de rango. Un enfoque común es ordenar los intervalos por tiempos de inicio o fin para simplificar las comparaciones y reducir la complejidad temporal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import "sort"

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func mergeIntervals(intervals [][]int) [][]int {
    if len(intervals) == 0 {
        return intervals
    }

    // Ordenar intervalos por inicio
    sort.Slice(intervals, func(i, j int) bool {
        return intervals[i][0] < intervals[j][0]
    })

    merged := [][]int{intervals[0]}
    for _, interval := range intervals[1:] {
        last := merged[len(merged)-1]
        if interval[0] <= last[1] {
            last[1] = max(last[1], interval[1])
        } else {
            merged = append(merged, interval)
        }
    }
    return merged
}

Lista Enlazada

Una Lista Enlazada es una estructura de datos que almacena elementos secuencialmente, pero con nodos dispersos aleatoriamente en memoria. Cada nodo contiene un valor y un puntero al siguiente nodo.

Las Listas Enlazadas permiten inserción/eliminación eficiente en ambos extremos pero no soportan acceso indexado.

1
2
3
4
5
6
7
8
type ListNode struct {
    value int
    next  *ListNode
}

head := &ListNode{value: 0}
head.next = &ListNode{value: 1}
head.next.next = &ListNode{value: 3}

La lista enlazada puede ser doblemente enlazada y almacenar un puntero al elemento anterior también.

Para iterar a través de una lista, puede verificar los siguientes nodos hasta que no pueda.

1
2
3
4
5
curr := head
for curr != nil {
    // Procesar valor
    curr = curr.next
}

Pila y Cola

Estas son estructuras de datos enfocadas en acceder a un extremo. Una Pila es una colección de tipo Último en Entrar Primero en Salir (LIFO), mientras que una Cola es una colección de tipo Primero en Entrar Primero en Salir (FIFO). Pueden implementarse fácilmente, y Go proporciona las estructuras de datos necesarias.

  • O(1) - agregar o eliminar de los extremos
  • O(n) - búsqueda y acceso indexado
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Cola
var queue []int

queue = append(queue, 1)
queue = append(queue, 2)

var element int
element, queue = queue[0], queue[1:] // element = 1

// Pila
var stack []int

stack = append(stack, 1)
stack = append(stack, 2)

element = stack[len(stack)-1]
stack = stack[:len(stack)-1] // element = 2

Hashmap

Un Hashmap es una estructura de datos que almacena pares clave/valor. Los valores pueden ser leídos eficientemente usando la clave. Funciona haciendo hash de la clave para almacenar valores en una tabla.

  • O(1) - lectura y escritura si se proporciona la clave
  • O(n) - si no se proporciona la clave
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
hashmap := make(map[string]string)
hashmap["key"] = "value"

_, exists := hashmap["key"]       // exists == true
_, exists = hashmap["other key"]  // exists == false

value := hashmap["key"] // value == "value"

// Formatos de iteración
for key, value := range hashmap {
    // pasar
}
for _, value := range hashmap {
    // pasar
}
for key := range hashmap {
    // pasar
}

Hashset

Es un caso especial de hashmap donde los valores son structs vacíos. Esta estructura de datos se utiliza para representar conjuntos y generalmente viene con operaciones de álgebra de conjuntos eficientes.

  • O(1) - leer y escribir un valor particular
  • O(n) - la mayoría de las operaciones de conjunto
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Inicializar conjuntos
A := make(map[int]struct{})
A[1] = struct{}{}
A[2] = struct{}{}
A[3] = struct{}{}

_, exists := A[1] // exists == true
_, exists = A[4]  // exists == false

B := make(map[int]struct{})
B[3] = struct{}{}
B[4] = struct{}{}
B[5] = struct{}{}

AuB := make(map[int]struct{})
for k := range A {
    AuB[k] = struct{}{}
}
for k := range B {
    AuB[k] = struct{}{}
}
// AuB contiene 1, 2, 3, 4, 5

AnB := make(map[int]struct{})
for k := range A {
    if _, exists := B[k]; exists {
        AnB[k] = struct{}{}
    }
}
// AnB contiene 3

Cola de Prioridad

La Cola de Prioridad, también conocida como PQ, es una estructura de datos cuyo objetivo es dar acceso a elementos basándose en su prioridad asignada. La cola mantiene un orden de prioridad tal que los elementos con mayor prioridad (o menor, dependiendo de la implementación) siempre son accedidos primero.

Esta estructura de datos se implementa comúnmente con montículos (heaps), un tipo especial de árbol binario que asegura que el elemento de mayor prioridad sea accesible eficientemente después de operaciones de agregar o eliminar.

  • O(1) - leer el elemento de mayor prioridad
  • O(logn) - insertar o eliminar un elemento

La biblioteca estándar de Go proporciona un paquete container/heap que puede usarse para implementar una cola de prioridad.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import (
    "container/heap"
)

type Item struct {
    value    string // El valor del elemento; arbitrario.
    priority int    // La prioridad del elemento.
    index    int    // El índice del elemento en el heap.
}

// Una PriorityQueue implementa heap.Interface y mantiene Items.
type PriorityQueue []*Item

func (pq PriorityQueue) Len() int { return len(pq) }

func (pq PriorityQueue) Less(i, j int) bool {
    // Queremos que Pop nos dé el de menor prioridad, así que usamos menor que aquí.
    return pq[i].priority < pq[j].priority
}

func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index = i
    pq[j].index = j
}

func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}

func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil  // evitar fuga de memoria
    item.index = -1 // por seguridad
    *pq = old[0 : n-1]
    return item
}

// Usando la PriorityQueue
pq := &PriorityQueue{}
heap.Init(pq)

heap.Push(pq, &Item{
    value:    "Charly",
    priority: 1,
})

heap.Push(pq, &Item{
    value:    "David",
    priority: 0,
})

heap.Push(pq, &Item{
    value:    "John",
    priority: 3,
})

item := heap.Pop(pq).(*Item)
// item.value == "David"

item = heap.Pop(pq).(*Item)
// item.value == "Charly"

Bitmaps

Estructura de datos que almacena eficientemente valores de bits en un array; los bits pueden representar valores booleanos, y el array en sí puede representar conjuntos. Esta estructura de datos es menos flexible que un conjunto pero puede ser muy eficiente en memoria y tiempo.

  • O(1) - escribir y leer un valor binario
  • O(1) - álgebra de conjuntos
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// También puede inicializarse como entero.
bitmap := 0b0000

// Establecer bit a 1
i := 2
bitmap |= (1 << i)
// -> 0b100

// Establecer bit a 0
bitmap &= ^(1 << i)
// -> 0b000

// Alternar bit
bitmap ^= (1 << i)
// -> 0b100

// Comprobar bit
(bit >> i) & 1

// Operaciones de conjunto
A := 0b1010
B := 0b1101

// Intersección
A & B // -> 0b1000

// Unión
A | B // -> 0b1111

// Diferencia
A & ^B

// Comprobar todos los bits
numBits := 4
A & ((1 << numBits) - 1)

Grafos

Estructura de datos que organiza datos en términos de sus relaciones.

Árbol

Estructura de datos de grafo que organiza datos en formato jerárquico y no tiene ciclos. A menudo se representa como una estructura con punteros recursivos a sus hijos.

1
2
3
4
5
6
7
8
type Node struct {
    val      int
    children []*Node
}

root := &Node{val: 0}
child := &Node{val: 1}
root.children = append(root.children, child)

Árbol Binario

Es un caso especial de un árbol donde cada nodo tiene como máximo 2 hijos. Esta estructura de datos usualmente se representa con hijos izquierdo y derecho.

1
2
3
4
5
6
7
8
type Node struct {
    val   int
    left  *Node
    right *Node
}

root := &Node{val: 0}
root.left = &Node{val: 1}

Búsqueda en Profundidad

También conocida como DFS, es una técnica algorítmica para recorrer un grafo o árbol visitando cada camino en profundidad antes de visitar otros caminos. Es útil cuando el problema requiere una comparación de escenarios finales de cada camino.

Al recorrer un árbol, el procesamiento del valor puede ocurrir antes de visitar el resto del camino o después; esto se llama recorrido preorden o postorden.

  • O(n) - complejidad temporal
  • O(h) - complejidad de memoria, donde h es la altura del árbol
    • O(n) - complejidad de memoria en el peor de los casos si el árbol está sesgado
    • O(logn) - complejidad de memoria si el árbol está equilibrado
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Implementación iterativa de DFS
stack := []*Node{}
stack = append(stack, root)

for len(stack) > 0 {
    // Sacar el elemento superior
    curr := stack[len(stack)-1]
    stack = stack[:len(stack)-1]

    // Procesar aquí si es recorrido preorden

    // Agregar hijos a la pila
    if curr.right != nil {
        stack = append(stack, curr.right)
    }
    if curr.left != nil {
        stack = append(stack, curr.left)
    }

    // Procesar aquí si es recorrido postorden
}

// Implementación recursiva
func DFS(node *Node) {
    if node == nil {
        return
    }
    // Procesar aquí si es recorrido preorden

    // Visitar recursivamente los hijos
    DFS(node.left)
    DFS(node.right)

    // Procesar aquí si es recorrido postorden
}

// Comenzar recorriendo la raíz
DFS(root)

Búsqueda en Anchura

También llamada BFS, es una técnica para recorrer un grafo o árbol en niveles. Puede ser útil para encontrar el camino más corto desde la raíz pero puede ser menos eficiente en memoria para caminos largos.

  • O(n) - complejidad temporal
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Implementación iterativa de BFS
queue := []*Node{}
queue = append(queue, root)

for len(queue) > 0 {
    // Desencolar el elemento frontal
    curr := queue[0]
    queue = queue[1:]

    // Procesar aquí

    // Agregar hijos a la cola
    if curr.left != nil {
        queue = append(queue, curr.left)
    }
    if curr.right != nil {
        queue = append(queue, curr.right)
    }
}

Backtracking

Una técnica algorítmica para recorrer un array y procesar no solo el valor sino el camino tomado hasta el valor. Esto es especialmente útil al modelar decisiones como árbol y comparar resultados, por ejemplo en juegos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type StackItem struct {
    path []int
    node *Node
}

stack := []StackItem{}

// Agregar un array vacío como camino y el nodo raíz
stack = append(stack, StackItem{path: []int{}, node: root})

for len(stack) > 0 {
    // Sacar el elemento superior
    curr := stack[len(stack)-1]
    stack = stack[:len(stack)-1]

    path, node := curr.path, curr.node

    // Procesar camino y nodo actual

    // Poner el siguiente camino y nodo en la parte superior de la pila
    if node.right != nil {
        stack = append(stack, StackItem{path: append(path, node.val), node: node.right})
    }
    if node.left != nil {
        stack = append(stack, StackItem{path: append(path, node.val), node: node.left})
    }
}

El backtracking también puede implementarse de manera recursiva, y a veces puede ser un poco más fácil pensar en este algoritmo de esta manera.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func BacktrackingWithDFS(path []int, node *Node) {
    // Procesar fin del grafo
    if node == nil {
        return
    }
    // Opcionalmente procesar nodos hoja
    if node.left == nil && node.right == nil {
        // Procesar nodo hoja
    }
    // Procesar camino y nodo actual
    BacktrackingWithDFS(append(path, node.val), node.left)
    BacktrackingWithDFS(append(path, node.val), node.right)
}

BacktrackingWithDFS([]int{}, root)