Введение

Язык программирования Go предоставляет базовые контейнеры, но часто разработчикам необходимы более специализированные структуры данных. Пакет go-collections предлагает реализации распространенных структур данных с поддержкой дженериков, что делает код более выразительным и удобным.

В этой статье мы подробно рассмотрим возможности пакета go-collections, его установку и примеры использования различных структур данных.

!В комментариях написали, что нужно упомянуть, что это моя библиотека, иначе я каким-то образом ввожу людей в заблуждение!


1. Установка

Для установки пакета используйте систему модулей Go:

go get github.com/idsulik/go-collections

После установки вы можете импортировать необходимые структуры данных в своем коде.


2. Обзор структур данных

Пакет go-collections предоставляет следующие структуры данных:

Deque (Двухсторонняя очередь)

Описание:

Deque (Double-ended queue) — это структура данных, которая позволяет добавлять и удалять элементы как с начала, так и с конца. Реализована на основе циклического буфера, что позволяет эффективно использовать память и поддерживать амортизированную константную сложность операций.

Конструктор:

func New[T any](initialCapacity int) *Deque[T]

Методы:

  • PushFront(item T): Добавляет элемент в начало очереди.

  • PushBack(item T): Добавляет элемент в конец очереди.

  • PopFront() (T, bool): Удаляет и возвращает элемент из начала очереди.

  • PopBack() (T, bool): Удаляет и возвращает элемент из конца очереди.

  • PeekFront() (T, bool): Возвращает элемент из начала очереди без удаления.

  • PeekBack() (T, bool): Возвращает элемент из конца очереди без удаления.

  • Len() int: Возвращает количество элементов в очереди.

  • IsEmpty() bool: Проверяет, пуста ли очередь.

  • Clear(): Очищает очередь.

Сложность:

  • Амортизированная сложность: O(1) для всех методов.

  • Наихудший случай: O(n) при необходимости расширения буфера.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/deque"
)

func main() {
    d := deque.New[int](0)
    d.PushBack(1)
    d.PushFront(2)

    front, _ := d.PopFront()
    back, _ := d.PopBack()

    fmt.Println(front) // Вывод: 2
    fmt.Println(back)  // Вывод: 1
}

Set (Множество)

Описание:

Set — это неупорядоченная структура данных, которая хранит только уникальные значения. Множества обеспечивают быстрый доступ, добавление и удаление элементов, используя хеш-таблицы для управления уникальностью.

Конструктор:

func New[T comparable]() *Set[T]

Методы:

  • Add(item T): Добавляет элемент в множество.

  • Remove(item T): Удаляет элемент из множества.

  • Has(item T) bool: Проверяет наличие элемента.

  • Clear(): Очищает множество.

  • Len() int: Возвращает количество элементов.

  • IsEmpty() bool: Проверяет, пусто ли множество.

  • Elements() []T: Возвращает срез всех элементов.

  • AddAll(items ...T): Добавляет несколько элементов.

  • RemoveAll(items ...T): Удаляет несколько элементов.

  • Diff(other *Set[T]) *Set[T]: Разница множеств.

  • Intersect(other *Set[T]) *Set[T]: Пересечение множеств.

  • Union(other *Set[T]) *Set[T]: Объединение множеств.

  • IsSubset(other *Set[T]) bool: Проверяет, является ли подмножеством.

  • IsSuperset(other *Set[T]) bool: Проверяет, является ли надмножеством.

  • Equal(other *Set[T]) bool: Проверяет равенство множеств.

Сложность:

  • Сложность для Add, Remove, Has, Len, IsEmpty, AddAll, RemoveAll: O(1) в среднем случае, благодаря хеш-таблицам.

  • Сложность для Elements: O(n), где n — количество элементов в множестве.

  • Сложность для операций Diff, Intersect, Union, IsSubset, IsSuperset, Equal: O(n), где n — количество элементов в наибольшем множестве.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/set"
)

func main() {
    s := set.New[int]()
    s.Add(1)
    s.Add(2)
    s.Add(2) // Дубликат не добавится

    fmt.Println(s.Elements()) // Вывод: [1 2]
}

LinkedList (Односвязный список)

Описание:

Односвязный список — это линейная структура данных, в которой элементы связаны последовательно, где каждый элемент (узел) содержит значение и ссылку на следующий элемент. Односвязные списки позволяют добавлять и удалять элементы с начала и конца списка.

Конструктор:

func New[T any]() *LinkedList[T]

Методы:

  • AddFront(value T): Добавляет элемент в начало списка.

  • AddBack(value T): Добавляет элемент в конец списка.

  • RemoveFront() (T, bool): Удаляет и возвращает элемент из начала списка.

  • RemoveBack() (T, bool): Удаляет и возвращает элемент из конца списка.

  • Iterate(fn func(T) bool): Итерация по элементам списка.

  • Size() int: Возвращает размер списка.

Сложность:

  • Сложность для AddFront, AddBack, RemoveFront, Size: O(1) — операции выполняются за постоянное время, так как не требуют обхода списка.

  • Сложность для RemoveBack: O(n), где n — количество элементов в списке, поскольку требуется пройти весь список до предпоследнего элемента.

  • Сложность для Iterate: O(n), где n — количество элементов в списке, так как выполняется обход всех элементов.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/linkedlist"
)

func main() {
    list := linkedlist.New[int]()
    list.AddFront(1)
    list.AddBack(2)

    list.Iterate(func(value int) bool {
        fmt.Println(value)
        return true
    })
    // Вывод:
    // 1
    // 2
}

Queue (Очередь)

Описание:

Очередь — это структура данных типа FIFO (First-In, First-Out), в которой элементы добавляются в конец и удаляются из начала. Очередь обеспечивает базовые операции для управления элементами в порядке их поступления.

Конструктор:

func New[T any](initialCapacity int) *Queue[T]

Методы:

  • Enqueue(item T): Добавляет элемент в конец очереди.

  • Dequeue() (T, bool): Удаляет и возвращает элемент из начала очереди.

  • Peek() (T, bool): Возвращает первый элемент без удаления.

  • Len() int: Возвращает количество элементов.

  • IsEmpty() bool: Проверяет, пуста ли очередь.

  • Clear(): Очищает очередь.

Сложность:

  • Сложность всех методов: O(1) амортизированная — благодаря использованию двусторонней очереди (Deque), операции выполняются за постоянное время.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/queue"
)

func main() {
    q := queue.New[int](0)
    q.Enqueue(1)
    q.Enqueue(2)

    item, _ := q.Dequeue()
    fmt.Println(item) // Вывод: 1
}

Stack (Стек)

Описание:

Стек — это структура данных типа LIFO (Last-In, First-Out), в которой последний добавленный элемент является первым, который будет удален. Стек поддерживает стандартные операции добавления и удаления элементов с верхушки.

Конструктор:

func New[T any](initialCapacity int) *Stack[T]

Методы:

  • Push(item T): Добавляет элемент на вершину стека.

  • Pop() (T, bool): Удаляет и возвращает элемент с вершины стека.

  • Peek() (T, bool): Возвращает элемент с вершины без удаления.

  • Len() int: Возвращает количество элементов.

  • IsEmpty() bool: Проверяет, пуст ли стек.

  • Clear(): Очищает стек.

Сложность:

  • Сложность для Push, Pop, Peek, Len, IsEmpty: O(1) — операции выполняются за постоянное время.

  • Сложность для Clear: O(n), где n — количество элементов в стеке, из-за необходимости обнуления всех ссылок в срезе.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/stack"
)

func main() {
    s := stack.New[int](0)
    s.Push(1)
    s.Push(2)

    item, _ := s.Pop()
    fmt.Println(item) // Вывод: 2
}

Trie (Префиксное дерево)

Описание:

Trie — это префиксное дерево, структура данных, которая поддерживает эффективную вставку и поиск слов и префиксов. Каждый узел дерева представляет символ, и пути в дереве образуют слова.

Конструктор:

func New() *Trie

Методы:

  • Insert(word string): Добавляет слово в Trie.

  • Search(word string) bool: Проверяет наличие слова.

  • StartsWith(prefix string) bool: Проверяет наличие слов с заданным префиксом.

Сложность:

  • Сложность всех методов: O(m), где m — длина слова или префикса. Операции зависят от длины входной строки, так как необходимо пройти по символам строки.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/trie"
)

func main() {
    t := trie.New()
    t.Insert("hello")
    t.Insert("helium")

    fmt.Println(t.Search("hello"))    // Вывод: true
    fmt.Println(t.StartsWith("he"))   // Вывод: true
    fmt.Println(t.Search("helix"))    // Вывод: false
}

Priority Queue (Приоритетная очередь)

Описание:

Приоритетная очередь — это структура данных, которая позволяет эффективно извлекать и удалять элементы с наивысшим (или наинизшим) приоритетом. Она поддерживает порядок элементов с использованием кучи (heap), что обеспечивает быстрый доступ к элементу с высшим приоритетом.

Конструктор:

func New[T any](less func(a, b T) bool) *PriorityQueue[T]

Параметры:

  • less: Функция сравнения, определяющая приоритет элементов.

Методы:

  • Push(item T): Добавляет элемент в очередь.

  • Pop() (T, bool): Удаляет и возвращает элемент с наивысшим приоритетом.

  • Peek() (T, bool): Возвращает элемент с наивысшим приоритетом без удаления.

  • Len() int: Возвращает количество элементов.

  • IsEmpty() bool: Проверяет, пуста ли очередь.

  • Clear(): Очищает очередь.

Сложность:

  • Сложность для Push, Pop: O(log n) — операции выполняются за логарифмическое время благодаря поддержанию свойств кучи при добавлении и удалении элементов.

  • Сложность для Peek, Len, IsEmpty: O(1) — доступ к верхушке кучи и проверки выполняются за постоянное время.

  • Сложность для Clear: O(n) — удаление всех элементов требует линейного времени, так как освобождается вся память.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/priorityqueue"
)

func main() {
    pq := priorityqueue.New[int](func(a, b int) bool {
        return a < b // Минимальный элемент имеет наивысший приоритет
    })

    pq.Push(3)
    pq.Push(1)
    pq.Push(2)

    item, _ := pq.Pop()
    fmt.Println(item) // Вывод: 1
}

Binary Search Tree (Двоичное дерево поиска)

Описание:

Двоичное дерево поиска (BST) — это структура данных, поддерживающая элементы в отсортированном порядке, что позволяет эффективно выполнять операции вставки, удаления и поиска. Каждая нода дерева имеет максимум два потомка: левый — для значений меньше текущего, и правый — для значений больше текущего.

Конструктор:

func New[T constraints.Ordered]() *BST[T]

Параметры:

  • T constraints.Ordered: Тип данных, поддерживающий операции сравнения < и >.

Методы:

  • Insert(value T): Вставляет значение в дерево.

  • Remove(value T): Удаляет значение из дерева.

  • Contains(value T) bool: Проверяет наличие значения.

  • InOrderTraversal(fn func(T)): Обходит дерево в порядке возрастания.

  • Len() int: Возвращает количество узлов.

  • IsEmpty() bool: Проверяет, пусто ли дерево.

  • Clear(): Очищает дерево.

Сложность:

  • Сложность для Insert, Contains, Remove: O(h), где h — высота дерева. В худшем случае (несбалансированное дерево) сложность может быть O(n), где n — количество узлов, но в среднем случае для сбалансированных деревьев — O(log n).

  • Сложность для InOrderTraversal: O(n) — необходимо обойти все узлы.

  • Сложность для Len, IsEmpty: O(1) — получение текущего размера или проверки выполняются за постоянное время.

  • Сложность для Clear: O(1) — ссылки на корень и размер просто обнуляются.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/bst"
)

func main() {
    tree := bst.New[int]()
    tree.Insert(2)
    tree.Insert(1)
    tree.Insert(3)

    tree.InOrderTraversal(func(value int) {
        fmt.Println(value)
    })
    // Вывод:
    // 1
    // 2
    // 3
}

Skip List (Список с пропусками)

Описание:

Skip List — это вероятностная структура данных, которая обеспечивает быстрый поиск, вставку и удаление элементов за счет использования нескольких уровней связей, что позволяет пропускать части списка. Skip List является альтернативой сбалансированным деревьям поиска и поддерживает элементы в отсортированном порядке.

Конструктор:

func New[T constraints.Ordered](maxLevel int, p float64) *SkipList[T]

Параметры:

  • maxLevel: Максимальный уровень списка.

  • p: Вероятность, определяющая уровень новых узлов (обычно 0.5).

Методы:

  • Insert(value T): Вставляет значение.

  • Delete(value T): Удаляет значение.

  • Search(value T) bool: Ищет значение.

  • Len() int: Возвращает количество элементов.

  • IsEmpty() bool: Проверяет, пуст ли список.

  • Clear(): Очищает список.

Сложность:

  • Сложность для Insert, Search, Delete: O(log n) в среднем случае — за счет вероятностного распределения уровней узлов и их связей.

  • Сложность для Len, IsEmpty: O(1) — получение текущего размера или проверка выполняются за постоянное время.

  • Сложность для Clear: O(1) — обнуляются ссылки на заголовочный узел и счетчики.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/skiplist"
)

func main() {
    sl := skiplist.New[int](16, 0.5)
    sl.Insert(1)
    sl.Insert(2)
    sl.Insert(3)

    fmt.Println(sl.Search(2)) // Вывод: true
    sl.Delete(2)
    fmt.Println(sl.Search(2)) // Вывод: false
}

Graph (Граф)

Описание:

Графы представляют собой сети узлов и ребер, подходящие для различных алгоритмов, таких как поиск путей и построение остовных деревьев.

Конструктор:

func New[T comparable](directed bool) *Graph[T]

Параметры:

  • directed: Указывает, является ли граф ориентированным.

Методы:

  • AddNode(value T): Добавляет узел.

  • AddEdge(from, to T, weight float64): Добавляет ребро с опциональным весом.

  • RemoveNode(value T): Удаляет узел и связанные ребра.

  • RemoveEdge(from, to T): Удаляет ребро.

  • Neighbors(value T) []T: Возвращает соседние узлы.

  • HasNode(value T) bool: Проверяет наличие узла.

  • HasEdge(from, to T) bool: Проверяет наличие ребра.

  • GetEdgeWeight(from, to T) (float64, bool): Получает вес ребра.

  • Traverse(start T, visit func(T)): Обходит граф от начального узла (BFS).

  • Nodes() []T: Возвращает все узлы.

  • Edges() [][2]T: Возвращает все ребра.

Сложность:

  • Сложность для AddNode, AddEdge, RemoveNode, RemoveEdge: O(1) в среднем случае для операций с узлами и ребрами благодаря использованию хеш-таблиц.

  • Сложность для Neighbors, HasNode, HasEdge, GetEdgeWeight: O(1) в среднем случае при доступе к элементам через хеш-таблицы.

  • Сложность для Traverse: O(V + E), где V — количество узлов, E — количество ребер, так как необходимо пройти все узлы и ребра.

  • Сложность для Nodes, Edges: O(V) и O(E) соответственно, так как требуется собрать все узлы или ребра.

Пример использования:

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/graph"
)

func main() {
    g := graph.New[string](false)
    g.AddNode("A")
    g.AddNode("B")
    g.AddEdge("A", "B", 1.0)

    fmt.Println(g.HasEdge("A", "B")) // Вывод: true
}

3. Практические примеры

Пример 1: Использование Priority Queue для планирования задач

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/priorityqueue"
)

type Task struct {
    name     string
    priority int
}

func main() {
    pq := priorityqueue.New[Task](func(a, b Task) bool {
        return a.priority < b.priority
    })

    pq.Push(Task{"Task1", 3})
    pq.Push(Task{"Task2", 1})
    pq.Push(Task{"Task3", 2})

    for !pq.IsEmpty() {
        task, _ := pq.Pop()
        fmt.Printf("Executing %s with priority %d\n", task.name, task.priority)
    }
    // Вывод:
    // Executing Task2 with priority 1
    // Executing Task3 with priority 2
    // Executing Task1 with priority 3
}

Пример 2: Поиск слов в Trie

package main

import (
    "fmt"
    "github.com/idsulik/go-collections/trie"
)

func main() {
    dictionary := trie.New()
    words := []string{"go", "golang", "gopher", "goroutine"}

    for _, word := range words {
        dictionary.Insert(word)
    }

    fmt.Println(dictionary.Search("golang"))    // Вывод: true
    fmt.Println(dictionary.Search("python"))    // Вывод: false
    fmt.Println(dictionary.StartsWith("go"))    // Вывод: true
}

4. Заключение

Пакет go-collections предоставляет широкий набор структур данных с поддержкой дженериков, что упрощает разработку и повышает производительность приложений на Go. Благодаря понятному API и богатому функционалу, этот пакет становится незаменимым инструментом для разработчиков.


Если у вас есть вопросы или предложения, пишите в комментариях!
Просьба поставить ? для репозитория, если вы думаете, что пакет полезный.

p.s. Если вам нужна какая-то другая структура данных, то можете создать pull request или же напишите в комментариях, что вам нужно и я добавлю в репозиторий

Комментарии (21)


  1. dopusteam
    22.09.2024 06:33
    +3

    Кажется, в статье не хватает информации, что вы рекламируете свою библиотеку (написанную вчера). А то складывается впечатление, что это какая-то то популярная библиотека. И не раскрыта тема 'эффективности', упомянутая в заголовке


    1. idsulik Автор
      22.09.2024 06:33

      свою библиотеку (написанную вчера)

      idsulik released this Jul 12 - если 12 июль - это вчера, то вы правы

      И не раскрыта тема 'эффективности', упомянутая в заголовке

      справедливое замечание, пойду исправлю


      1. dopusteam
        22.09.2024 06:33

        Ну, судя по коммитам, skip list и graph вы добавили только вчера


        1. idsulik Автор
          22.09.2024 06:33

          библиотеку (написанную вчера)

          но вы писали про библиотеку.

          даже если эти структуры добавлены вчера, то что в этом такого? я может сегодня тоже добавлю еще структуры, к примеру - bloom filter


          1. dopusteam
            22.09.2024 06:33

            Согласен, выразился некорректно.

            Но сейчас выглядит так, будто вы намеренно вводите читателя в заблуждение, не указывая, что вы просто рекламите свою либу, которой всего два месяца


            1. idsulik Автор
              22.09.2024 06:33

              вводите читателя в заблуждение,

              каким образом? и как вы предлагаете сделать?


            1. idsulik Автор
              22.09.2024 06:33

              Готово, обновил статью и написал в начале, что это моя библиотека


  1. Sly_tom_cat
    22.09.2024 06:33
    +1

    А в юнит-тестах использовать if-ы вместо requeire/assert - это принципиально/религиозные соображения?


    1. idsulik Автор
      22.09.2024 06:33

      Если вам кажется, что require/assert лучше, то можете тут объяснить или же создать pull request, гляну и буду рад принять или сам поменять и запушить)


      1. Sly_tom_cat
        22.09.2024 06:33
        +2

        Честно говоря немного лень делать все это...
        Просто покажу пример
        if s == nil { t.Error("Expected new set to be non-nil") }
        заменяется на
        require.NotNil(t, s) - провалит тест сразу
        или
        assert.NotNil(t, s) - позволит тесту продолжиться дальше но тест уже будет считаться провалившимся.

        assert имеет смысл применять в табличных тестах где в одном цикле проверяется сразу несколько разных значений и есть смысл проверить все кейсы, а не валить тест на первом фейле.


        1. idsulik Автор
          22.09.2024 06:33

          Спасибо, я думал, что будет более веская причина поменять одно на другое)
          Но да, согласен, лаконично, как дойдут руки поменяю


          1. Sly_tom_cat
            22.09.2024 06:33

            И кстати не совсем понял по какой причине очередь и стек сделаны на слайсах?

            По мне имея linkedList - стек и очередь делаются просто алиасами на методы linkedList.

            Но тут конечно зависит от задач - где-то и слайс под капотом будет хорош, а где-то лучше бы все-таки linkedList.

            И собственно поэтому такая библиотека не самое лучшее решение. Да, взять готовое - иногда проще, но вот, имея понимание своей задачи, накидать свой связанный сипсок или слайс с методами - дело 5 минут.


            1. idsulik Автор
              22.09.2024 06:33

              пока на слайсах, в планах добавить еще и на linkedin, чтобы можно было выбрать нужный подход под задачу, как это сделано в Java.

              Ну..недостаточно 5 минут, тебе надо накидать логику, написать тесты, а это уже не 5 минут, хотя зависит от скорости. Порой легче и лучше взять готовое решение, а порой лучше самому написать)


  1. dcaraxes
    22.09.2024 06:33
    +1

    Мало чем похоже на статью, это больше документация к вашей библиотеке. И какие преимущества перед проверенным https://github.com/emirpasic/gods с 16к звездами?


    1. idsulik Автор
      22.09.2024 06:33
      +1

      Не знал про gods, у того и другого есть свои плюсы и минусы, к примеру, в go-collections есть Trie, есть SkipList, в планах добавить Bloom filter


    1. Sly_tom_cat
      22.09.2024 06:33

      Ну вот только там не модно-молодежно на дженириках. Там все по хадкору: на настоящих пустых интерфейсах. ;)


      1. dcaraxes
        22.09.2024 06:33

        Коммит 8 месяцев назад был по переводу на дженерики.


        1. idsulik Автор
          22.09.2024 06:33

          А это что-то меняет?) ощущение, что целенаправленно ищете к чему-бы придраться


          1. dcaraxes
            22.09.2024 06:33

            Нет, никак не придраться, я хотел понять, в чем же преимущества вашего пакета перед gods, и вы ответили. А коммент про дженерики это ответ для @Sly_tom_cat


  1. Dreddsa
    22.09.2024 06:33
    +1

    Звезду поставил - автор молодец, так держать.
    Но go - это про горутины там, многопоточность... А структуры в библиотеке без защиты от гонок.
    Будет время/желание - можно проработать.
    Удачи


    1. idsulik Автор
      22.09.2024 06:33

      Спасибо большое. Да, в планах сделать отдельный пакет sync и поместить туда thread safe реализацию структур