Шпаргалка по структурам данных в Go

в 20:31, , рубрики: Go, golang, stl контейнеры, Алгоритмы, собеседования

Шпаргалка по структурам данных в Go - 1


Некоторые компании проводят собеседования с online написанием кода. Требуется решить олимпиадную задачку на скорость. В таких условиях нет времени посмотреть подробности реализации структур данных — нужно сразу реализовать идею. Но курсы по алгоритмам и структурам данных дают примеры или на псевдокоде, или на С++. Ещё эталонные решения задач написаны зачастую на С++. Готовясь к собеседованию, составил шпаргалку библиотек — аналогов контейнеров STL, что бы не тратить драгоценное время на поиск.

Начнём с очевидного.

Динамический непрерывный массив

Аналог std::vector.
Поддерживает обращение к элементу по индексу за константное время в несколько тактов процессора. Можно увеличить или уменьшить вместительность. Это встроеный slice:

vector := []int{}

Удобно, что базовые концепции встроены в язык.

Cтек


Аналог std::stack.
Упорядоченный набор, в которой добавление новых элементов и удаление существующих производится с одного конца. Первым из стека удаляется элемент, который был помещен туда последним (last-in, first-out — LIFO). Это опять встоенный slice. Из проекта в проект копируются сниппеты:

// Push
stack = append(stack, value)

// Pop
// Проверка, что len(stack) > 0
stack, value = a[:len(stack)-1], a[len(stack)-1]

Oперация среза не аллоцирует новую память.

Очередь


Аналог std::queue и std::deque.
Очереди реализуют операции извлечения и добавления для начала и конца за константное время. Первым из очереди удаляется элемент, который был первым помещен (first-in, first-out — FIFO). Буферизованный канал является очередью на кольцевом буфере, можно использовать его, когда читатель и писатель — разные горутины. Но отдельной реализации очереди в стандартной библиотеке нет. Список awesome-go советует библиотеку https://github.com/gammazero/deque.

import "github.com/gammazero/deque"

Реализуемые операции:

func (q *Deque) PushBack(elem interface{})
func (q *Deque) PushFront(elem interface{})
func (q *Deque) PopBack() interface{}
func (q *Deque) PopFront() interface{}
func (q *Deque) Back() interface{}
func (q *Deque) Front() interface{}
func (q *Deque) At(i int) interface{}

Двусвязный список

Аналог std::list.
Состоит из элементов, содержащих помимо собственных данных ссылки на следующий и предыдущий элемент списка. Он есть в стандартной библиотеке:

import "container/list"

Как и ожидается, поддерживает операции вставки (в начало, в конец, до и после элемента, указатель на который передан) и удаления.

func (l *List) PushBack(v interface{}) *Element
func (l *List) PushFront(v interface{}) *Element
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
func (l *List) Remove(e *Element) interface{}

Gо не предоставляет специфического синтаксиса для итераторов. Потому следующий/предыдущий элемент можно получить из указателя на любой узел. Эти методы не протухают после добавления/удаления элемента в список, без неожиданностей.

func (e *Element) Next() *Element
func (e *Element) Prev() *Element

Очередь с приоритетом

Аналог std::priority_queue
Позволяет складывать элементы в любом порядке, а доставать в любой момент времени самый приоритетный из оставшихся. Применяется, например, в алгоритме построения минимального покрывающего дерева, когда на очередном шаге алгоритм выбирает самое короткое ребро из всех, одним концом начинающихся в уже покрытых вершинах.

В стандартной библиотеке есть адаптер, превращающий любой сортируемый контейнер (реализующий sort.Interface) в очередь с приоритетом.

import "container/heap"

Это классическая Двоичная куча. Реализует вставку и удаление за O(log n).

func Pop(h Interface) interface{}
func Push(h Interface, x interface{})
func Remove(h Interface, i int) interface{}

Хеш таблица


Она же словарь и ассоциативный массив.
Aналог std::unordered_map.
Позволяет добавлять ключ-значение, удалять значение по ключу и проверять наличие элемента за O(1) в среднем. Очевидно, map встроена в язык:

unorderedMap := make(map[string]int)

Результат make(map) является указателем, и способ работы немного отличается от стандартных контейнеров:

// Проверка вхождения:
_, ok := unorderedMap["route"]
// Удаление элемента:
delete(unorderedMap, "route")
// Нахождение длины:
n := len(unorderedMap)

«runtime/map», в отличии от std::unordered_map заботится о программисте — удалять значения во время итерации по ним безопасно.

Множество

Аналог std::unordered_set.
Почти то же самое, что и хеш-таблица, но без сохранения значения.
Если нам нужно только быстрая проверка вхождения, то можно снова использовать встроенный map. Нужно лишь указать пустое значение, что бы указать, что важен только ключ.

var m = make(map[string]struct{})
m["!"] = struct{}{}
_, ok := m["!"] // true

Но эта реализация не поддерживает сложных операторов. Для объединения, пересечения, разности из коробки, понадобятся сторонние библиотеки. Самая используемая, судя по количеству звёзд: https://github.com/deckarep/golang-set

import "github.com/deckarep/golang-set"

Самая нужная часть API:

Add(i interface{}) bool
Remove(i interface{})
Cardinality() int // len()
Contains(i ...interface{}) bool
IsSubset(other Set) bool
Intersect(other Set) Set
Union(other Set) Set
Difference(other Set) Set
SymmetricDifference(other Set) Set

Множество int

В экспериментальной части стандарной библиотеки есть оптимизированное множесво int, экономящее каждый бит.

import "golang.org/x/tools/container/intsets"

Оно также поддерживает объединение, пересечение, разность множеств.

Двоичные деревья поиска


Aналоги std::set и std::map.
Могут показаться новичку плохими аналогами хеш-таблиц:
также поддерживают добавление, удаление и проверку вхождения, но за O(log n).
Но деревья хранят узлы отсортированными по ключу.
В стандартной библиотеке go деревьев нет, широко используется репозиторий, содержащий AVL, Красно-Чёрные и B-деревья.

import "github.com/emirpasic/gods/trees/avltree"

Наиболее употребимые методы API:

func (tree *Tree) Get(key interface{}) (value interface{}, found bool)
func (tree *Tree) Put(key interface{}, value interface{})
func (tree *Tree) Remove(key interface{})
func (tree *Tree) Size() int
func (tree *Tree) Keys() []interface{}
func (tree *Tree) Values() []interface{}
func (tree *Tree) Left() *Node
func (tree *Tree) Right() *Node

Есть два особо важных метода деревев:

func (tree *Tree) Ceiling(key interface{}) (ceiling *Node, found bool)

возвращает наименьший существующий элемент больще ключа.

func (tree *Tree) Floor(key interface{}) (floor *Node, found bool)

возвращает наибольший существующий элемент меньше ключа.

Задачи на это относительно часто попадаются в собеседованиях. В реальной жизни используется в индексах баз данных:

select x from table where x <= $1 limit 1;

При наличии индекса отработает за O(log n), за 1 поиск границы в B-дереве.

Фильтр Блума


А вот этой структуры данных в stl нет.
Как и хеш-таблица, позволяет проверять принадлежность элемента к множеству. Но фильтр не хранит ключи при добавлении, и занимает константное количество памяти. Есть возможность получить ложноположительное срабатывание (элемента в множестве нет, но структура данных сообщает, что он есть), но не ложноотрицательное. Используется как фильтр, что бы быстро отсекать почти все не существующие ключи, экономя более дорогую проверку, например читающую с диска или делающую запрос в базу данных.
Есть сторонняя библиотека: https://github.com/willf/bloom

import "github.com/willf/bloom"

Не так часто используется, API можно и подсмотреть.

HyperLogLog


В стандартной библиотеке С++ такого нет.
Вероятностная структура данных. С небольшой ошибкой ( ≈ 0.4% ) считает количество уникальных элементов, не храня сами ключи. Даёт огромную экономию памяти. Если стоит задача быстро посчитать количество посетителей или запросов — HyperLogLog подходит идеально.
Самая популярная библиотека для этого сейчас.
https://github.com/axiomhq/hyperloglog

import "github.com/axiomhq/hyperloglog"

Сортировки

Аналоги std::sort и std::stable_sort.
С потребительской точки зрения есть только 2 принципиально разных типа:
Стабильные (не меняют порядок равных элементов [[4, 0], [1, 2], [1, 1], [5, 6]] --> [[1, 2], [1, 1], [4, 0],[5, 6]])
и не стабильные, не дающие гарантии на последовательность остальных полей.
И то и другое есть в стандартной библиотеке:

func Sort(data Interface)

Это реализация быстрой сортировки Хоара, нестабильная. Но для участков длины < 12 используется сортировка кучей, в качестве оптимизации.

func Stable(data Interface)

Внутри это сортирова слиянием, но, в целях эффективности, при достижении рекурсивным алгоритмом блоков меньше 20 элементов используется сортировка вставками.
Это классические алгоритмы, работающие за O(n log n).

Если вы дочитали — поздравляю. Знание конкретных API помгает при решении тестовых задач. (Eсли вы работали с чем-то и знаете лучшие альтернативы — пишите в комментариях.)

Автор: Олег Шванн

Источник

* - обязательные к заполнению поля