From 1b4c7d84621ba37fd58d4cd1155ea11980b92381 Mon Sep 17 00:00:00 2001 From: cloudnative0x0 <> Date: Thu, 13 Aug 2026 15:57:17 +0300 Subject: [PATCH 1/2] new structure added. --- linked_list/README.md | 165 +++++++++++++++++++++++ linked_list/linked_list.go | 225 ++++++++++++++++++++++++++++++++ linked_list/linked_list_test.go | 209 +++++++++++++++++++++++++++++ 3 files changed, 599 insertions(+) create mode 100644 linked_list/README.md create mode 100644 linked_list/linked_list.go create mode 100644 linked_list/linked_list_test.go diff --git a/linked_list/README.md b/linked_list/README.md new file mode 100644 index 0000000..ae82ff7 --- /dev/null +++ b/linked_list/README.md @@ -0,0 +1,165 @@ +# Linked List + +

+ РусскийEnglish +

+ +--- + +## Русский + +Связный список — линейная структура данных, в которой элементы (узлы) хранятся не в непрерывном блоке памяти, а по отдельности и связаны между собой через указатели. Каждый узел знает только адрес следующего узла — этим список отличается от массива или среза, где все элементы лежат подряд. + +Реализация в этом пакете — односвязный список: переход возможен только в одну сторону, от начала к концу. + +### Внутреннее устройство + +Список хранит три поля: `head`, `tail` и `size`. + +- `head` — указатель на первый узел списка. +- `tail` — указатель на последний узел списка. Он нужен, чтобы добавлять элемент в конец за O(1), не пробегая весь список каждый раз. +- `size` — текущее количество элементов. + +Каждый узел (`Node[T]`) состоит из значения `Value` и указателя `Next` на следующий узел. У последнего узла `Next` равен `nil` — это признак конца списка. + +Если список пуст, оба указателя, `head` и `tail`, равны `nil`. + +### Добавление и удаление + +- `Prepend` создаёт новый узел и ставит его перед текущим `head`, а сам становится новым `head`. Если список был пуст, этот же узел становится и `tail`. +- `Append` создаёт узел и подвешивает его к `tail.Next`, после чего `tail` сдвигается на него. Если список был пуст, новый узел становится и `head`, и `tail`. +- `InsertAt` вставляет значение на произвольную позицию. Для позиций `0` и `size` используются `Prepend` и `Append`, для остальных случаев список проходится до узла перед нужной позицией, и указатели переставляются. +- `RemoveFirst` возвращает значение из `head` и сдвигает `head` на следующий узел. Если после этого список опустел, `tail` тоже обнуляется. +- `RemoveLast` вынужден пройти список от начала до предпоследнего узла, поскольку у узлов нет указателя назад, на предыдущий элемент. Из-за этого удаление последнего элемента стоит O(n), в отличие от добавления. +- `Remove` ищет первый узел с заданным значением и вырезает его из цепочки, переставляя `Next` у предыдущего узла. + +### Поиск и обход + +`Search` и `Get` проходят список последовательно от `head`, пока не найдут нужный элемент или не дойдут до конца — прямого доступа по индексу, как в массиве, у связного списка нет. + +`Traverse` принимает функцию-колбэк и вызывает её для значения каждого узла по порядку. + +`Reverse` разворачивает список на месте: проходит по узлам, у каждого перекладывает `Next` так, чтобы он указывал на предыдущий узел, и в конце меняет местами `head` и `tail`. + +`ToSlice` собирает значения всех узлов в обычный срез — удобно для отладки или передачи данных туда, где нужен произвольный доступ по индексу. + +### Использование + +```go +list := New[int]() +list.Append(1) +list.Append(2) +list.Prepend(0) + +val, _ := list.Get(1) // val = 1 +list.Reverse() +slice := list.ToSlice() // [2, 1, 0] +``` + +### Операции + +| Операция | Сложность | Описание | +|---|---|---| +| `Prepend(x)` | O(1) | добавить элемент в начало списка | +| `Append(x)` | O(1) | добавить элемент в конец списка | +| `InsertAt(pos, x)` | O(n) | вставить элемент на произвольную позицию | +| `RemoveFirst()` | O(1) | удалить и вернуть первый элемент | +| `RemoveLast()` | O(n) | удалить и вернуть последний элемент | +| `Remove(x)` | O(n) | удалить первый узел с заданным значением | +| `Search(x)` | O(n) | проверить, есть ли значение в списке | +| `Get(pos)` | O(n) | получить значение по позиции | +| `Traverse(fn)` | O(n) | пройти список и вызвать функцию для каждого значения | +| `Reverse()` | O(n) | развернуть список на месте | +| `ToSlice()` | O(n) | получить список значений в виде среза | +| `Len()` | O(1) | текущее количество элементов | +| `IsEmpty()` | O(1) | проверка, пуст ли список | + +### Сборка и тестирование + +```bash +go test -v ./... +``` + +--- + +## English + +A linked list is a linear data structure where elements (nodes) are not stored in one contiguous block of memory, but individually, connected to each other through pointers. Each node only knows the address of the next node — this is what sets it apart from an array or a slice, where all elements sit next to each other. + +The implementation in this package is a singly linked list: traversal is only possible in one direction, from the start toward the end. + +### Internal layout + +The list keeps three fields: `head`, `tail`, and `size`. + +- `head` points to the first node of the list. +- `tail` points to the last node of the list. It exists so that adding an element to the end costs O(1) instead of walking the whole list every time. +- `size` holds the current number of elements. + +Each node (`Node[T]`) consists of a value `Value` and a pointer `Next` to the following node. The last node has `Next` set to `nil` — that is the marker for the end of the list. + +When the list is empty, both `head` and `tail` are `nil`. + +### Insertion and removal + +- `Prepend` creates a new node and places it before the current `head`, becoming the new `head`. If the list was empty, this same node also becomes `tail`. +- `Append` creates a node and attaches it to `tail.Next`, then moves `tail` to it. If the list was empty, the new node becomes both `head` and `tail`. +- `InsertAt` inserts a value at an arbitrary position. Positions `0` and `size` fall back to `Prepend` and `Append`; for anything in between, the list is walked up to the node just before the target position, and the pointers are relinked. +- `RemoveFirst` returns the value at `head` and moves `head` to the next node. If the list becomes empty as a result, `tail` is cleared too. +- `RemoveLast` has to walk the list from the beginning up to the second-to-last node, since nodes have no pointer back to their predecessor. Because of this, removing the last element costs O(n), unlike appending. +- `Remove` looks for the first node holding the given value and cuts it out of the chain by relinking the previous node's `Next`. + +### Search and traversal + +`Search` and `Get` walk the list sequentially from `head` until they find the target element or reach the end — a linked list has no direct index-based access the way an array does. + +`Traverse` takes a callback function and calls it with the value of every node in order. + +`Reverse` reverses the list in place: it walks the nodes, flips each one's `Next` to point at the previous node, and finally swaps `head` and `tail`. + +`ToSlice` collects the values of all nodes into a plain slice — useful for debugging or for passing the data to something that needs index-based access. + +### Usage + +```go +list := New[int]() +list.Append(1) +list.Append(2) +list.Prepend(0) + +val, _ := list.Get(1) // val = 1 +list.Reverse() +slice := list.ToSlice() // [2, 1, 0] +``` + +### Operations + +| Operation | Complexity | Description | +|---|---|---| +| `Prepend(x)` | O(1) | add an element to the front of the list | +| `Append(x)` | O(1) | add an element to the back of the list | +| `InsertAt(pos, x)` | O(n) | insert an element at an arbitrary position | +| `RemoveFirst()` | O(1) | remove and return the first element | +| `RemoveLast()` | O(n) | remove and return the last element | +| `Remove(x)` | O(n) | remove the first node holding the given value | +| `Search(x)` | O(n) | check whether a value exists in the list | +| `Get(pos)` | O(n) | get the value at a given position | +| `Traverse(fn)` | O(n) | walk the list and call a function for each value | +| `Reverse()` | O(n) | reverse the list in place | +| `ToSlice()` | O(n) | get the list's values as a slice | +| `Len()` | O(1) | current number of elements | +| `IsEmpty()` | O(1) | check whether the list is empty | + +### Build and test + +```bash +go test -v ./... +``` + +--- + +
+ +> Связный список не хранит карту всего пути — только адрес следующего шага. Чтобы дойти до конца, нужно пройти через каждое звено цепи, ни одно не пропустить. +> +> *A linked list keeps no map of the whole path — only the address of the next step. To reach the end, you have to pass through every link in the chain, skipping none.* \ No newline at end of file diff --git a/linked_list/linked_list.go b/linked_list/linked_list.go new file mode 100644 index 0000000..f7a628d --- /dev/null +++ b/linked_list/linked_list.go @@ -0,0 +1,225 @@ +package linked_list + +import ( + "errors" + "strings" +) + +var ( + ErrEmptyList = errors.New("list is empty") + ErrOutOfBounds = errors.New("index out of bounds") + ErrNotFound = errors.New("value not found") +) + +type Node[T any] struct { + Value T + Next *Node[T] +} + +type LinkedList[T comparable] struct { + head *Node[T] + tail *Node[T] + size int +} + +func New[T comparable]() *LinkedList[T] { + return &LinkedList[T]{} +} + +func (ll *LinkedList[T]) Len() int { return ll.size } + +func (ll *LinkedList[T]) IsEmpty() bool { return ll.size == 0 } + +func (ll *LinkedList[T]) Prepend(value T) { + node := &Node[T]{Value: value, Next: ll.head} + ll.head = node + if ll.tail == nil { + ll.tail = node + } + + ll.size++ +} + +func (ll *LinkedList[T]) Append(value T) { + node := &Node[T]{Value: value} + if ll.tail == nil { + ll.head = node + ll.tail = node + } else { + ll.tail.Next = node + ll.tail = node + } + + ll.size++ +} + +func (ll *LinkedList[T]) InsertAt(position int, value T) error { + if position < 0 || position > ll.size { + return ErrOutOfBounds + } + if position == 0 { + ll.Prepend(value) + return nil + } + if position == ll.size { + ll.Append(value) + return nil + } + + prev := ll.head + for i := 0; i < position-1; i++ { + prev = prev.Next + } + + node := &Node[T]{Value: value, Next: prev.Next} + prev.Next = node + ll.size++ + + return nil +} + +func (ll *LinkedList[T]) RemoveFirst() (T, error) { + var zero T + if ll.head == nil { + return zero, ErrEmptyList + } + + val := ll.head.Value + + ll.head = ll.head.Next + if ll.head == nil { + ll.tail = nil + } + + ll.size-- + + return val, nil +} + +func (ll *LinkedList[T]) RemoveLast() (T, error) { + var zero T + if ll.head == nil { + return zero, ErrEmptyList + } + if ll.head.Next == nil { + val := ll.head.Value + ll.head = nil + ll.tail = nil + ll.size-- + return val, nil + } + + prev := ll.head + for prev.Next.Next != nil { + prev = prev.Next + } + + val := prev.Next.Value + prev.Next = nil + ll.tail = prev + ll.size-- + + return val, nil +} + +func (ll *LinkedList[T]) Remove(value T) error { + if ll.head == nil { + return ErrEmptyList + } + if ll.head.Value == value { + ll.head = ll.head.Next + + if ll.head == nil { + ll.tail = nil + } + ll.size-- + + return nil + } + + prev := ll.head + for prev.Next != nil { + if prev.Next.Value == value { + if prev.Next == ll.tail { + ll.tail = prev + } + + prev.Next = prev.Next.Next + ll.size-- + + return nil + } + + prev = prev.Next + } + + return ErrNotFound +} + +func (ll *LinkedList[T]) Search(value T) bool { + for cur := ll.head; cur != nil; cur = cur.Next { + if cur.Value == value { + return true + } + } + + return false +} + +func (ll *LinkedList[T]) Get(position int) (T, error) { + var zero T + if position < 0 || position >= ll.size { + return zero, ErrOutOfBounds + } + cur := ll.head + for i := 0; i < position; i++ { + cur = cur.Next + } + + return cur.Value, nil +} + +func (ll *LinkedList[T]) Traverse(callback func(value T)) { + for cur := ll.head; cur != nil; cur = cur.Next { + callback(cur.Value) + } +} + +func (ll *LinkedList[T]) Reverse() { + var prev *Node[T] + + cur := ll.head + ll.tail = ll.head + + for cur != nil { + next := cur.Next + cur.Next = prev + prev = cur + cur = next + } + + ll.head = prev +} + +func (ll *LinkedList[T]) ToSlice() []T { + slice := make([]T, 0, ll.size) + for cur := ll.head; cur != nil; cur = cur.Next { + slice = append(slice, cur.Value) + } + + return slice +} + +func (ll *LinkedList[T]) String() string { + var b strings.Builder + b.WriteString("[") + for cur := ll.head; cur != nil; cur = cur.Next { + + if cur.Next != nil { + b.WriteString(" -> ") + } + } + b.WriteString("]") + + return b.String() +} diff --git a/linked_list/linked_list_test.go b/linked_list/linked_list_test.go new file mode 100644 index 0000000..ced4f83 --- /dev/null +++ b/linked_list/linked_list_test.go @@ -0,0 +1,209 @@ +package linked_list + +import ( + "errors" + "reflect" + "testing" +) + +func TestPrependOnEmptyList(t *testing.T) { + ll := New[int]() + ll.Prepend(1) // must not panic on empty list + if got := ll.ToSlice(); !reflect.DeepEqual(got, []int{1}) { + t.Fatalf("got %v", got) + } +} + +func TestPrependKeepsRestOfList(t *testing.T) { + ll := New[int]() + ll.Append(2) + ll.Append(3) + ll.Prepend(1) + want := []int{1, 2, 3} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestAppendOnEmptyList(t *testing.T) { + ll := New[int]() + ll.Append(1) // must not panic on empty list + if got := ll.ToSlice(); !reflect.DeepEqual(got, []int{1}) { + t.Fatalf("got %v", got) + } +} + +func TestAppendDoesNotCorruptList(t *testing.T) { + ll := New[int]() + ll.Append(1) + ll.Append(2) + ll.Append(3) + ll.Append(4) + want := []int{1, 2, 3, 4} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } + if ll.Len() != 4 { + t.Fatalf("len = %d, want 4", ll.Len()) + } +} + +func TestInsertAtDoesNotDropTail(t *testing.T) { + ll := New[int]() + ll.Append(1) + ll.Append(2) + ll.Append(4) + if err := ll.InsertAt(2, 3); err != nil { + t.Fatalf("unexpected error: %v", err) + } + want := []int{1, 2, 3, 4} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestInsertAtBoundaries(t *testing.T) { + ll := New[int]() + ll.Append(2) + ll.Append(3) + + if err := ll.InsertAt(0, 1); err != nil { + t.Fatal(err) + } + if err := ll.InsertAt(ll.Len(), 4); err != nil { + t.Fatal(err) + } + want := []int{1, 2, 3, 4} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } + + if err := ll.InsertAt(-1, 99); !errors.Is(err, ErrOutOfBounds) { + t.Fatalf("want ErrOutOfBounds, got %v", err) + } + if err := ll.InsertAt(ll.Len()+1, 99); !errors.Is(err, ErrOutOfBounds) { + t.Fatalf("want ErrOutOfBounds, got %v", err) + } +} + +func TestRemoveFirstAndLastOnEmpty(t *testing.T) { + ll := New[int]() + if _, err := ll.RemoveFirst(); !errors.Is(err, ErrEmptyList) { + t.Fatalf("want ErrEmptyList, got %v", err) + } + if _, err := ll.RemoveLast(); !errors.Is(err, ErrEmptyList) { + t.Fatalf("want ErrEmptyList, got %v", err) + } +} + +func TestRemoveLastKeepsTailConsistent(t *testing.T) { + ll := New[int]() + ll.Append(1) + ll.Append(2) + ll.Append(3) + + if _, err := ll.RemoveLast(); err != nil { + t.Fatal(err) + } + ll.Append(30) + want := []int{1, 2, 30} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestRemoveMiddleAndTailKeepsTailPointer(t *testing.T) { + ll := New[int]() + ll.Append(1) + ll.Append(2) + ll.Append(3) + + if err := ll.Remove(3); err != nil { + t.Fatal(err) + } + ll.Append(30) // must correctly attach to the new tail (2) + want := []int{1, 2, 30} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestRemoveNotFound(t *testing.T) { + ll := New[int]() + ll.Append(1) + if err := ll.Remove(99); !errors.Is(err, ErrNotFound) { + t.Fatalf("want ErrNotFound, got %v", err) + } +} + +func TestSearch(t *testing.T) { + ll := New[int]() + ll.Append(1) + ll.Append(2) + if !ll.Search(2) { + t.Fatal("expected to find 2") + } + if ll.Search(99) { + t.Fatal("did not expect to find 99") + } +} + +func TestReverse(t *testing.T) { + ll := New[int]() + for _, v := range []int{1, 2, 3, 4} { + ll.Append(v) + } + ll.Reverse() + want := []int{4, 3, 2, 1} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } + + ll.Append(0) + want = []int{4, 3, 2, 1, 0} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestReverseEmptyAndSingle(t *testing.T) { + ll := New[int]() + ll.Reverse() // must not panic + if ll.Len() != 0 { + t.Fatalf("len = %d, want 0", ll.Len()) + } + + ll.Append(1) + ll.Reverse() + if got := ll.ToSlice(); !reflect.DeepEqual(got, []int{1}) { + t.Fatalf("got %v", got) + } +} + +func TestStringType(t *testing.T) { + ll := New[string]() + ll.Append("a") + ll.Append("b") + ll.Prepend("z") + want := []string{"z", "a", "b"} + if got := ll.ToSlice(); !reflect.DeepEqual(got, want) { + t.Fatalf("got %v, want %v", got, want) + } +} + +func TestSizeConsistencyAcrossOps(t *testing.T) { + ll := New[int]() + ll.Append(1) + ll.Append(2) + ll.Prepend(0) + ll.InsertAt(2, 99) + if ll.Len() != 4 { + t.Fatalf("len = %d, want 4", ll.Len()) + } + ll.Remove(99) + ll.RemoveFirst() + ll.RemoveLast() + if ll.Len() != 1 { + t.Fatalf("len = %d, want 1", ll.Len()) + } +} From f8cd3eee6cb673f6a799ff23ed955ad6bff774d0 Mon Sep 17 00:00:00 2001 From: cloudnative0x0 <> Date: Thu, 13 Aug 2026 16:02:18 +0300 Subject: [PATCH 2/2] errors handled. --- linked_list/linked_list_test.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/linked_list/linked_list_test.go b/linked_list/linked_list_test.go index ced4f83..fe959df 100644 --- a/linked_list/linked_list_test.go +++ b/linked_list/linked_list_test.go @@ -196,13 +196,25 @@ func TestSizeConsistencyAcrossOps(t *testing.T) { ll.Append(1) ll.Append(2) ll.Prepend(0) - ll.InsertAt(2, 99) + err := ll.InsertAt(2, 99) + if err != nil { + return + } if ll.Len() != 4 { t.Fatalf("len = %d, want 4", ll.Len()) } - ll.Remove(99) - ll.RemoveFirst() - ll.RemoveLast() + errR := ll.Remove(99) + if errR != nil { + return + } + _, errRF := ll.RemoveFirst() + if errRF != nil { + return + } + _, errRL := ll.RemoveLast() + if errRL != nil { + return + } if ll.Len() != 1 { t.Fatalf("len = %d, want 1", ll.Len()) }