head и tail на изображении выше указываются на первый и последний узел списка, prev и next указатели на предыдущий и следующий узлы. Ниже представлен код объявления связного списка:
// Узел списка. // value - полезные данные, добавляемые в список // next - указатель на следующий узел списка // prev - указатель на предыдущий узел списка type LinkedListNode[T any] struct { value T next *LinkedListNode[T] prev *LinkedListNode[T] } // Связный список. // size - длина списка // head - первый узел списка // tail - последний узел списка type LinkedList[T any] struct { size uint head *LinkedListNode[T] tail *LinkedListNode[T] }
LinkedListNode - узел списка, который хранит полезные данные в поле value, указатель на предыдущий узел prev и на следующий next.
LinkedList - связный список в котором поле head указывает на первый элемент списка, а tail на последний, по-умолчанию оба ссылаются на NULL. Поле size это длина списка, при вставке происходит инкремент его значения, а при удалении значение уменьшается на единицу.
Указатель next нового узла должен ссылаться на текущей первый узел.
Если список был пуст, то tail также должен ссылаться на новый узел, иначе указатель prev текущего первого узла должен ссылаться на новый узел.
Указатель head должен ссылаться на новый узел.
// Добавление элемента в начало списка. // element - новый элемент для добавления func (list *LinkedList[T]) InsertFirst(element T) { node := &LinkedListNode[T]{value: element, next: list.head} if list.head == nil { list.tail = node } else { list.head.prev = node } list.head = node list.size++ }
Добавление элемента в конец списка осуществляется схожим образом, но все манипуляции проходят с использованием указателя tail, чтобы добавить элемент в конец списка требуется выполнить следующие шаги:
Указатель prev нового узла должен ссылаться на текущий последний узел.
Если список был пуст, то head также должен ссылаться на новый узел, иначе указатель next текущего последнего узла должен ссылаться на новый узел.
Указатель tail должен ссылаться на новый узел.
// Добавление элемента в конец списка. // element - новый элемент для добавления func (list *LinkedList[T]) InsertLast(element T) { node := &LinkedListNode[T]{value: element, prev: list.tail} if list.tail == nil { list.head = node } else { list.tail.next = node } list.tail = node list.size++ }
Если список содержит только один узел достаточно присвоить указателям head и tail значение NULL. В ином случае указатель prev следующего после head узла должен ссылаться на NULL.
Указатель head должен ссылаться на следующий узел в списке.
// Удаление первого элемента списка. func (list *LinkedList[T]) RemoveFirst() { if list.head != nil { next := list.head.next if next != nil { next.prev = nil } else { list.tail = nil } list.head = next list.size-- } }
Последний элемент списка удаляется аналогичным образом только с использованием указателя tail:
Если список содержит только один узел достаточно присвоить указателям tail и head значение NULL. В ином случае указатель next узла предшествующего tail должен ссылаться на NULL.
Указатель tail должен ссылаться на предыдущий узел в списке.
// Удаление последнего элемента списка. func (list *LinkedList[T]) RemoveLast() { if list.tail != nil { prev := list.tail.prev if prev != nil { prev.next = nil } else { list.head = nil } list.tail = prev list.size-- } }
Все описанные выше операции для работы со связным списком выполняются за константное время O(1). Указанные операции являются наиболее распространенными для данной структуры данных, и в целом определяют основные сценарии использования.
Связный список крайне неэффективен в операциях требующих доступ к случайным узлам списка, к примеру, обращение к узлам по индексу выполняется за O(n), где n количество узлов списка, так как, необходимо перебирать узлы до тех пор пока не будет найден нужный узел.
Весь исходный код примеров к данной статье вы можете найти на GitHub.