algos Flashcards

(47 cards)

1
Q
  1. Что такое структура данных?
A

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

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q
  1. Чем массив отличается от среза?
A

Массив в Go имеет фиксированную длину, задаваемую при создании. Срез — динамическая обёртка над массивом, может изменять размер (через append).

arr := [3]int{1, 2, 3} // массив длиной 3
slice := []int{1, 2, 3} // срез
slice = append(slice, 4) // [1,2,3,4]

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q
  1. Как реализован slice внутри Go?
A

Указатель на подлежащий массив
Длина (количество элементов)
Ёмкость (максимально возможное число элементов до перевыделения памяти)
slice := make([]int, 3, 5)
fmt.Println(len(slice)) // 3
fmt.Println(cap(slice)) // 5

https://stepik.org/lesson/1939232/step/3?unit=1965970

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q
  1. Что такое связанный список?
A

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

type Node struct {
value int
next *Node
}

func main() {
n1 := &Node{value: 1}
n2 := &Node{value: 2}
n1.next = n2
fmt.Println(n1.value, “->”, n1.next.value) // 1 -> 2

https://stepik.org/lesson/1939232/step/4?unit=1965970

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q
  1. Какие бывают виды списков (односвязный, двусвязный)?
A

Односвязный список: каждый узел хранит указатель только на следующий элемент. Двусвязный список: у каждого узла два указателя — на следующий и предыдущий элемент. Это упрощает удаление и вставку, но требует больше памяти.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q
  1. Чем список отличается от массива?
A

Массив — фиксированный размер, быстрый доступ по индексу (O(1)), дорогостоящая вставка/удаление.
Список — динамический размер, вставка/удаление O(1) при известном месте, но доступ к элементу O(n).

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q
  1. Что такое стек и как он работает?
A

Стек — структура данных LIFO (last in, first out). Последний добавленный элемент извлекается первым.

type Stack []int

func (s *Stack) Push(v int) {
s = append(s, v)
}

func (s Stack) Pop() int {
l := len(
s)
if l == 0 { panic(“empty stack”) }
v := (*s)[l-1]
s = (s)[:l-1]
return v
}

func main() {
var st Stack
st.Push(10)
st.Push(20)
fmt.Println(st.Pop()) // 20
}

https://stepik.org/lesson/1939232/step/7?unit=1965970

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q
  1. Что такое очередь и чем она отличается от стека?
A

Очередь — структура данных FIFO (first in, first out). В отличие от стека, элементы извлекаются в порядке добавления.

type Queue []int

func (q *Queue) Enqueue(v int) {
q = append(q, v)
}

func (q Queue) Dequeue() int {
if len(
q) == 0 { panic(“empty queue”) }
v := (*q)[0]
q = (q)[1:]
return v
}

func main() {
var q Queue
q.Enqueue(1)
q.Enqueue(2)
fmt.Println(q.Dequeue()) // 1
}

https://stepik.org/lesson/1939232/step/8?unit=1965970

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q
  1. Что такое очередь с приоритетами?
A

Очередь с приоритетами возвращает элементы не в порядке вставки, а по приоритету (обычно — наибольшему или наименьшему значению). В Go можно реализовать через container/heap.

import (
“container/heap”
“fmt”
)

type IntHeap []int

func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // min-heap
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) { h = append(h, x.(int)) }
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}

func main() {
h := &IntHeap{3, 1, 4}
heap.Init(h)
heap.Push(h, 2)
fmt.Println(heap.Pop(h)) // 1 (минимум)
}

https://stepik.org/lesson/1939232/step/9?unit=1965970

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q
  1. Что такое хэш-таблица и как она работает?
A

Хэш-таблица хранит пары «ключ-значение». Ключ пропускается через хэш-функцию, которая вычисляет индекс в массиве. Для разрешения коллизий используют метод цепочек или открытую адресацию.

m := make(map[string]int)
m[“alice”] = 25
m[“bob”] = 30
fmt.Println(m[“alice”]) // 25

https://stepik.org/lesson/1939232/step/10?unit=1965970

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q
  1. Чем отличается открытая адресация от метода цепочек?
A

Оба метода решают проблему коллизий в хэш-таблице.

Метод цепочек: каждая ячейка хранит список (цепочку) элементов с одинаковым хэшом. Просто реализуется, но требует выделения памяти под списки.
Открытая адресация: все элементы хранятся в массиве. При коллизии ищется следующая свободная ячейка (линейное/квадратичное пробирование или двойное хэширование). Экономит память, но таблица должна быть неплотной (load factor < 0.7).

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q
  1. Как устроены map в Go?
A

В Go map реализован как хэш-таблица с открытой адресацией и использованием бакетов. Каждый бакет хранит до 8 пар «ключ-значение». При переполнении создаётся новый бакет, связанный со старым. При высоком коэффициенте загрузки выполняется rehash (увеличение размера таблицы). Пример:

m := make(map[string]int)
m[“apple”] = 5
m[“orange”] = 10

fmt.Println(m[“apple”]) // 5

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q
  1. Что такое бинарное дерево?
A

Бинарное дерево — иерархическая структура, где каждый узел имеет не более двух потомков: left и right. Используется для поиска, сортировки и представления данных.

type Node struct {
value int
left *Node
right *Node
}

func main() {
root := &Node{value: 10}
root.left = &Node{value: 5}
root.right = &Node{value: 15}
fmt.Println(root.left.value) // 5
}

https://stepik.org/lesson/1939232/step/13?unit=1965970

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q
  1. Чем отличается бинарное дерево поиска от обычного?
A

В обычном дереве нет строгих правил расположения узлов. В бинарном дереве поиска (BST) выполняется условие:

Все значения в левом поддереве < корня
Все значения в правом поддереве > корня
Это позволяет выполнять поиск за O(log n) в сбалансированном дереве.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q
  1. Что такое сбалансированные деревья (AVL, красно-чёрные)?
A

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

AVL — поддерживает разницу высот поддеревьев ≤ 1. Быстрый поиск, но дорогие вставки/удаления.
Красно-чёрные — менее строгое балансирование, но более быстрые вставки/удаления.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q
  1. Что такое B-дерево и где оно используется?
A

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

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
17
Q
  1. Чем отличается граф от дерева?
A

Дерево — частный случай графа без циклов, связный и имеет n-1 ребро при n вершинах.
Граф — обобщённая структура: может быть направленным/ненаправленным, содержать циклы.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
18
Q
  1. Какие бывают виды графов?
A

По направленности: ориентированные (digraph) и неориентированные
По весу: взвешенные и невзвешенные
По плотности: плотные и разреженные
По наличию циклов: ациклические и циклические
Специальные: дерево (связный ациклический граф), DAG (ориентированный ациклический граф)

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
19
Q
  1. Как работает BFS?
A

BFS (поиск в ширину) обходит граф слоями от стартовой вершины. Использует очередь. Применяется для поиска кратчайшего пути в невзвешенных графах.

func BFS(start int, graph map[int][]int) {
visited := make(map[int]bool)
queue := []int{start}
for len(queue) > 0 {
v := queue[0]
queue = queue[1:]
if visited[v] {
continue
}
visited[v] = true
fmt.Println(v)

for _, nei := range graph[v] {
    if !visited[nei] {
        queue = append(queue, nei)
    }
} }

https://stepik.org/lesson/1939233/step/3?unit=1965971

20
Q
  1. Как работает DFS?
A

DFS (поиск в глубину) идёт вглубь по каждому пути, пока возможно. Реализуется рекурсией или стеком. Применяется для топологической сортировки, поиска компонент связности.

func DFS(v int, graph map[int][]int, visited map[int]bool) {
if visited[v] {
return
}
visited[v] = true
fmt.Println(v)
for _, nei := range graph[v] {
DFS(nei, graph, visited)
}
}

https://stepik.org/lesson/1939233/step/4?unit=1965971

21
Q
  1. Что такое алгоритм Дейкстры?
A

Алгоритм Дейкстры находит кратчайшие пути от стартовой вершины до всех остальных в графе с неотрицательными весами. Использует жадный подход и очередь с приоритетами.

func Dijkstra(graph map[int]map[int]int, start int) map[int]int {
dist := make(map[int]int)
for v := range graph {
dist[v] = 1«31 - 1 // бесконечность
}
dist[start] = 0
visited := make(map[int]bool)

for len(visited) < len(graph) {
    u := -1
    minDist := 1<<31 - 1
    for v, d := range dist {
        if !visited[v] && d < minDist {
            u = v
            minDist = d
        }
    }
    if u == -1 {
        break
    }
    visited[u] = true

    for nei, w := range graph[u] {
        if dist[u]+w < dist[nei] {
            dist[nei] = dist[u] + w
        }
    }
}
return dist

https://stepik.org/lesson/1939233/step/5?unit=1965971

22
Q
  1. Что такое алгоритм Беллмана–Форда?
A

Алгоритм Беллмана–Форда ищет кратчайшие пути от одной вершины до всех остальных, работает даже с отрицательными весами рёбер (но не с отрицательными циклами). Сложность — O(V·E). Также он позволяет обнаружить наличие отрицательного цикла.

Пример реализации:

type Edge struct {
from, to, weight int
}

func BellmanFord(edges []Edge, V, start int) []int {
dist := make([]int, V)
for i := range dist {
dist[i] = 1«31 - 1 // “бесконечность”
}
dist[start] = 0

for i := 0; i < V-1; i++ {
    for _, e := range edges {
        if dist[e.from] != 1<<31-1 && dist[e.from]+e.weight < dist[e.to] {
            dist[e.to] = dist[e.from] + e.weight
        }
    }
}
return dist }

https://stepik.org/lesson/1939233/step/6?unit=1965971

23
Q
  1. Как работает алгоритм Флойда–Уоршелла?
A

Алгоритм Флойда–Уоршелла ищет кратчайшие пути между всеми парами вершин графа. Он основан на динамическом программировании и имеет сложность O(V³).

Пример реализации:

func FloydWarshall(matrix [][]int) [][]int {
n := len(matrix)
dist := make([][]int, n)
for i := range dist {
dist[i] = make([]int, n)
copy(dist[i], matrix[i])
}

for k := 0; k < n; k++ {
    for i := 0; i < n; i++ {
        for j := 0; j < n; j++ {
            if dist[i][k]+dist[k][j] < dist[i][j] {
                dist[i][j] = dist[i][k] + dist[k][j]
            }
        }
    }
}
return dist }

https://stepik.org/lesson/1939233/step/7?unit=1965971

24
Q
  1. Что такое минимальное остовное дерево?
A

Минимальное остовное дерево (MST) — это подмножество рёбер связного графа, которое соединяет все вершины при минимальной сумме весов. Используется, например, в сетевых топологиях, задачах оптимизации и маршрутизации.

25
25. Чем отличается алгоритм Крускала от алгоритма Прима?
Оба алгоритма находят минимальное остовное дерево, но работают по-разному: Крускал сортирует все рёбра по весу и добавляет их, если они не образуют цикл (используется Union-Find). Прим начинает с одной вершины и расширяет MST, выбирая минимальное ребро к новой вершине. Сложность обоих алгоритмов примерно O(E log V).
26
26. Что такое сортировка?
Сортировка — это процесс упорядочивания элементов набора по заданному критерию. Может быть числовой (по возрастанию/убыванию) или лексикографической (по алфавиту). Существует множество алгоритмов сортировки: пузырьковая, быстрая, слиянием и др.
27
27. Как работает сортировка пузырьком?
Алгоритм многократно проходит по массиву и обменивает соседние элементы, если они стоят в неправильном порядке. Сложность — O(n²), на практике используется редко. Пример реализации: func BubbleSort(arr []int) { n := len(arr) for i := 0; i < n; i++ { for j := 0; j < n-i-1; j++ { if arr[j] > arr[j+1] { arr[j], arr[j+1] = arr[j+1], arr[j] } } } } https://stepik.org/lesson/1939233/step/11?unit=1965971
28
28. Как работает быстрая сортировка (Quicksort)?
Быстрая сортировка выбирает опорный элемент, делит массив на две части (меньше и больше опорного), а затем рекурсивно сортирует каждую часть. Средняя сложность — O(n log n), худший случай — O(n²). Пример реализации: func QuickSort(arr []int) []int { if len(arr) < 2 { return arr } pivot := arr[len(arr)/2] left, right := []int{}, []int{} for i, v := range arr { if i == len(arr)/2 { continue } if v <= pivot { left = append(left, v) } else { right = append(right, v) } } return append(append(QuickSort(left), pivot), QuickSort(right)...) } https://stepik.org/lesson/1939233/step/12?unit=1965971
29
29. Как работает сортировка слиянием?
Сортировка слиянием рекурсивно делит массив на две части до единичных элементов, а затем объединяет (сливает) отсортированные подмассивы. Сложность — O(n log n), алгоритм устойчивый. Пример реализации: func MergeSort(arr []int) []int { if len(arr) < 2 { return arr } mid := len(arr) / 2 left := MergeSort(arr[:mid]) right := MergeSort(arr[mid:]) return merge(left, right) } func merge(left, right []int) []int { result := []int{} i, j := 0, 0 for i < len(left) && j < len(right) { if left[i] < right[j] { result = append(result, left[i]) i++ } else { result = append(result, right[j]) j++ } } result = append(result, left[i:]...) result = append(result, right[j:]...) return result } https://stepik.org/lesson/1939233/step/13?unit=1965971
30
30. Чем отличается сортировка подсчётом от сравнения?
Сортировка подсчётом — это некомпаративный алгоритм, он работает путём подсчёта количества вхождений каждого элемента. Подходит для целых чисел в ограниченном диапазоне. Сложность — O(n + k), где k — диапазон значений. Алгоритмы сравнения (quicksort, mergesort, heapsort) сравнивают элементы попарно и имеют нижнюю границу сложности O(n log n).
31
31. Что такое бинарный поиск?
Бинарный поиск используется для нахождения элемента в отсортированном массиве. Идея — на каждом шаге делить массив пополам и выбирать ту часть, где может находиться элемент. Сложность — O(log n). Пример реализации: func BinarySearch(arr []int, target int) int { left, right := 0, len(arr)-1 for left <= right { mid := (left + right) / 2 if arr[mid] == target { return mid } else if arr[mid] < target { left = mid + 1 } else { right = mid - 1 } } return -1 } https://stepik.org/lesson/1939233/step/15?unit=1965971
32
32. Какова сложность бинарного поиска?
Бинарный поиск выполняет O(log n) шагов, где n — размер массива. Поиск выполняется быстро, но алгоритм требует отсортированных данных. Если данные неотсортированы, нужно сначала выполнить сортировку (O(n log n)).
33
33. Что такое сложность алгоритма?
Сложность алгоритма показывает, сколько ресурсов (времени и памяти) потребуется для его выполнения в зависимости от размера входных данных. Выделяют: Временная сложность — количество операций. Пространственная сложность — используемая память.
34
34. Что такое Big O, Big Ω и Big Θ?
Big O — верхняя граница (худший случай). Big Ω — нижняя граница (лучший случай). Big Θ — асимптотически точная оценка (и верхняя, и нижняя граница совпадают). Пример: для бинарного поиска O(log n), Ω(1), Θ(log n).
35
35. Чем отличается O(n) от O(log n)?
O(n) — линейная сложность: время растёт пропорционально числу элементов. O(log n) — логарифмическая: время растёт очень медленно, даже при большом n. Пример: линейный поиск — O(n), бинарный поиск — O(log n).
36
36. Чем отличается амортизированная сложность от худшего случая?
Худший случай — максимальное количество операций для одного вызова алгоритма. Амортизированная сложность — усреднённое время одной операции на длинной последовательности вызовов. Пример: вставка в динамический массив append() в Go: В худшем случае — O(n) (при расширении массива). Амортизированно — O(1), так как расширение происходит редко.
37
37. Что такое динамическое программирование?
Динамическое программирование (DP) — метод оптимизации, при котором задача разбивается на подзадачи, и результаты этих подзадач сохраняются для повторного использования. Применяется, например, в задачах поиска оптимального пути, рюкзака, чисел Фибоначчи. Пример: вычисление чисел Фибоначчи: func FibonacciDP(n int) int { dp := make([]int, n+1) dp[0], dp[1] = 0, 1 for i := 2; i <= n; i++ { dp[i] = dp[i-1] + dp[i-2] } return dp[n] } https://stepik.org/lesson/1939234/step/5?unit=1965972
38
38. Как работает задача о рюкзаке?
Задача о рюкзаке: дан набор предметов с весами и ценностью, и рюкзак с ограниченной вместимостью. Нужно выбрать предметы так, чтобы максимизировать ценность, не превышая вместимость. Решается динамическим программированием: dp[i][w] — максимальная ценность при использовании первых i предметов и вместимости w.
39
39. Что такое мемоизация?
Мемоизация — техника оптимизации рекурсивных алгоритмов, при которой результаты подзадач сохраняются в кэше и повторно используются при следующем вызове с теми же параметрами. Пример: числа Фибоначчи с мемоизацией: var memo = map[int]int{} func FibonacciMemo(n int) int { if n <= 1 { return n } if val, ok := memo[n]; ok { return val } memo[n] = FibonacciMemo(n-1) + FibonacciMemo(n-2) return memo[n] } https://stepik.org/lesson/1939234/step/7?unit=1965972
40
40. Как работает алгоритм Кадане?
Алгоритм Кадане находит подотрезок массива с максимальной суммой (Maximum Subarray Problem). Он проходит по массиву и накапливает текущую сумму, обнуляя её, если она становится меньше нуля. Пример реализации: func Kadane(arr []int) int { maxSoFar, maxEndingHere := arr[0], arr[0] for i := 1; i < len(arr); i++ { if maxEndingHere+arr[i] > arr[i] { maxEndingHere = maxEndingHere + arr[i] } else { maxEndingHere = arr[i] } if maxEndingHere > maxSoFar { maxSoFar = maxEndingHere } } return maxSoFar } https://stepik.org/lesson/1939234/step/8?unit=1965972
41
41. Что такое алгоритм Кнута-Морриса-Пратта?
Алгоритм Кнута–Морриса–Пратта (KMP) используется для поиска подстроки в строке за линейное время O(n+m), где n — длина текста, m — длина шаблона. Он строит префикс-функцию, которая позволяет избегать повторных проверок символов. Пример реализации: func KMPSearch(text, pattern string) []int { lps := computeLPS(pattern) res := []int{} i, j := 0, 0 for i < len(text) { if text[i] == pattern[j] { i++ j++ if j == len(pattern) { res = append(res, i-j) j = lps[j-1] } } else { if j != 0 { j = lps[j-1] } else { i++ } } } return res } func computeLPS(pattern string) []int { lps := make([]int, len(pattern)) length := 0 i := 1 for i < len(pattern) { if pattern[i] == pattern[length] { length++ lps[i] = length i++ } else { if length != 0 { length = lps[length-1] } else { lps[i] = 0 i++ } } } return lps } https://stepik.org/lesson/1939234/step/9?unit=1965972
42
42. Что такое алгоритм Рабина-Карпа?
Алгоритм Рабина–Карпа ищет подстроку с помощью хэширования. Вычисляется хэш шаблона и хэши всех подстрок той же длины в тексте, после чего сравниваются значения. Средняя сложность O(n+m), худшая O(n·m). Пример: func RabinKarp(text, pattern string) []int { const prime = 101 m, n := len(pattern), len(text) if m > n { return nil } var result []int patHash, txtHash, h := 0, 0, 1 for i := 0; i < m-1; i++ { h = (h * 256) % prime } for i := 0; i < m; i++ { patHash = (256*patHash + int(pattern[i])) % prime txtHash = (256*txtHash + int(text[i])) % prime } for i := 0; i <= n-m; i++ { if patHash == txtHash { if text[i:i+m] == pattern { result = append(result, i) } } if i < n-m { txtHash = (256*(txtHash-int(text[i])*h) + int(text[i+m])) % prime if txtHash < 0 { txtHash += prime } } } return result } https://stepik.org/lesson/1939234/step/10?unit=1965972
43
43. Что такое хэш-коллизия?
Хэш-коллизия возникает, когда две разные строки или данные имеют одинаковое значение хэш-функции. Для обработки коллизий используют: Метод цепочек (каждый ключ хранит список значений). Открытую адресацию (поиск следующей свободной ячейки).
44
44. Чем отличается хэш-таблица от хэш-мапы?
По сути, это одно и то же. В Go структура map — это реализация хэш-таблицы. Термин «хэш-таблица» — общий, а «хэш-мапа» — конкретная реализация в языке программирования.
45
45. Что такое Bloom filter?
Bloom Filter — это вероятностная структура данных, позволяющая проверить, принадлежит ли элемент множеству. Он может дать ложноположительный ответ, но никогда — ложноотрицательный. Используется в базах данных, кешах и сетевых фильтрах. Пример простого Bloom filter на Go: type BloomFilter struct { bitset []bool size int } func NewBloomFilter(size int) *BloomFilter { return &BloomFilter{bitset: make([]bool, size), size: size} } func (bf *BloomFilter) Add(item int) { bf.bitset[item%bf.size] = true } func (bf *BloomFilter) Contains(item int) bool { return bf.bitset[item%bf.size] } https://stepik.org/lesson/1939234/step/13?unit=1965972
46
46. Что такое trie?
Trie (префиксное дерево) — это структура данных для хранения строк. Каждый узел соответствует символу, а путь от корня до листа образует строку. Применяется для поиска слов, автодополнения и работы со словарями. Пример реализации: type TrieNode struct { children map[rune]*TrieNode isEnd bool } type Trie struct { root *TrieNode } func NewTrie() *Trie { return &Trie{root: &TrieNode{children: make(map[rune]*TrieNode)}} } func (t *Trie) Insert(word string) { node := t.root for _, ch := range word { if _, ok := node.children[ch]; !ok { node.children[ch] = &TrieNode{children: make(map[rune]*TrieNode)} } node = node.children[ch] } node.isEnd = true } func (t *Trie) Search(word string) bool { node := t.root for _, ch := range word { if _, ok := node.children[ch]; !ok { return false } node = node.children[ch] } return node.isEnd } https://stepik.org/lesson/1939234/step/14?unit=1965972
47