Расскажите о связных списках. Как их можно реализовать в Go, и в каких случаях их использование предпочтительнее срезов (slices)?

Ответ

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

Реализация в Go

В Go можно легко реализовать односвязный список с помощью структур и указателей:

// Node представляет узел в списке
type Node[T any] struct {
    data T
    next *Node[T]
}

// LinkedList представляет сам список
type LinkedList[T any] struct {
    head *Node[T]
    size int
}

// Append добавляет новый узел в конец списка
func (l *LinkedList[T]) Append(data T) {
    newNode := &Node[T]{data: data}
    if l.head == nil {
        l.head = newNode
    } else {
        current := l.head
        for current.next != nil {
            current = current.next
        }
        current.next = newNode
    }
    l.size++
}

В стандартной библиотеке Go есть пакет container/list, который предоставляет готовую реализацию двусвязного списка.

Связные списки vs. Срезы (Slices)

Выбор между связным списком и срезом зависит от конкретной задачи.

Используйте связный список, когда:

  1. Нужны частые и быстрые вставки/удаления в начало или середину коллекции. Для списка это операция O(1) (если у вас есть указатель на узел), в то время как для среза это O(n), так как требует сдвига всех последующих элементов.
  2. Размер коллекции заранее неизвестен и часто меняется. Списки растут динамически без необходимости переаллокации всего массива, в отличие от срезов, которые при достижении capacity создают новый, больший массив и копируют в него все элементы.

Используйте срез (slice), когда:

  1. Нужен быстрый доступ к элементам по индексу. Доступ к элементу среза — это операция O(1). В связном списке для доступа к i-тому элементу нужно пройти i узлов, что является операцией O(n).
  2. Важна локальность данных в памяти (cache-friendliness). Элементы среза хранятся в памяти непрерывным блоком, что позволяет процессору эффективно их кешировать. Узлы списка могут быть разбросаны по всей памяти, что приводит к большему числу промахов кеша.
  3. Накладные расходы на память критичны. Каждый узел списка хранит не только данные, но и один или два указателя, что увеличивает общий расход памяти.

Итог: Срезы в Go являются более универсальной и в большинстве случаев более производительной структурой данных. Связные списки — это нишевый инструмент, который незаменим при определённых требованиях к операциям вставки и удаления.