BFS (Breadth-First Search) — это базовый алгоритм обхода графа в ширину, который поуровнево исследует все связанные вершины. Он начинает с выбранной точки и проверяет всех соседей, прежде чем переходить на следующий уровень. Алгоритм идеально подходит для поиска кратчайшего пути в невзвешенных графах.
Очередь (Queue): Используется для упорядочивания вершин по принципу «первым вошел — первым вышел» (FIFO).
Массив посещенных: Предотвращает зацикливание и повторную обработку уже пройденных вершин.

Граф задаётся в формате adjacency list (список смежности).
Тип: map[int][]int
- Ключ (int): вершина графа (например, 0, 1, 2...)
- Значение ([]int): срез вершин, в которые можно перейти напрямую из текущей
Данный граф:
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
| graph := map[int][]int{
0: {1, 5}, // Из вершины 0 можно перейти в вершины 1 и 5
1: {2, 6}, // Из вершины 1 можно перейти в вершины 2 и 6
2: {3, 7}, // Из вершины 2 можно перейти в вершины 3 и 7
3: {4, 8}, // Из вершины 3 можно перейти в вершины 4 и 8
4: {0, 9}, // Из вершины 4 можно перейти в вершины 0 и 9
5: {7}, // Из вершины 5 можно перейти в вершину 7
7: {9}, // Из вершины 7 можно перейти в вершину 9
9: {6}, // Из вершины 9 можно перейти в вершину 6
6: {8}, // Из вершины 6 можно перейти в вершину 8
8: {5}, // Из вершины 8 можно перейти в вершину 5
} |
|
Особенности формата:
1. Неориентированный граф: если есть ребро 0->1, то должно быть и 1->0.
В данном графе рёбра направленные? Проверим: 0->1 есть, но 1->0 НЕТ!
Значит, граф ориентированный (directed graph).
2. Если вершина отсутствует как ключ — у неё нет исходящих рёбер (но могут быть входящие).
3. Порядок соседей в срезе определяет порядок обхода (важно для детерминированного BFS).
4. Граф может содержать циклы: 0→1→2→3→4→0 образует цикл длиной 5.
Также есть цикл 5→7→9→6→8→5 (длиной 5).
Преимущества такого представления:
- Экономия памяти: хранятся только существующие рёбра O(V+E)
- Быстрый доступ ко всем соседям: O(1) по ключу
- Удобно для BFS/DFS: достаточно пройтись по graph[currentNode]
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| // breadthFirstSearch выполняет обход графа в ширину (BFS), начиная с указанной вершины.
//
// Параметры:
// - graph: граф, представленный в виде map[int][]int, где ключ — вершина,
// а значение — срез смежных вершин (направленные или ненаправленные рёбра).
// - start: начальная вершина, с которой начинается обход.
//
// Возвращаемое значение:
// - []int: порядок обхода вершин в ширину.
//
// Особенности:
// - Функция использует множество visited (map[int]struct{}{}) для отслеживания
// уже посещённых вершин, что предотвращает зацикливание и повторный обход.
// - Обход выполняется итеративно с использованием очереди (queue).
// - Если граф содержит недостижимые из start вершины, они не будут включены в результат.
// - Функция корректно работает как с направленными, так и с ненаправленными графами
// (при условии корректного задания смежных вершин).
func breadthFirstSearch(graph map[int][]int, start int) []int {
// visited — Heshset посещённых вершин.
// Используем map[int]struct{}{}, а не map[int]bool, потому что:
// - struct{}{} занимает 0 байт (экономия памяти)
// - это идиоматичный способ реализации set в Go
// - явно показывает, что значение не имеет смысла, важен только ключ
visited := map[int]struct{}{}
// queue — очередь для обхода в ширину (BFS).
// Используем срез []int как очередь FIFO (First-In-First-Out):
// - добавление в конец: queue = append(queue, node)
// - извлечение из начала: currentNode := queue[0]; queue = queue[1:]
queue := []int{start}
// visited[start] = struct{}{} — отмечаем начальную вершину как посещённую.
// Добавляем в множество (hash set) до помещения в очередь, чтобы избежать повторного добавления.
// struct{}{} — пустая структура, занимающая 0 байт (экономия памяти).
// Альтернатива с true была бы visited[start] = true, но struct{}{} — идиоматичный способ для set в Go.
visited[start] = struct{}{}
// path — результирующий срез для хранения вершин в порядке обхода BFS.
// Nil-срез (нулевое значение), память выделится динамически при первом append.
var path []int
// Продолжаем обход, пока очередь не опустеет.
// Условие len(queue) > 0 — идиоматичный способ проверить, есть ли элементы в очереди на основе среза.
// Пустая очередь означает, что все достижимые вершины обработаны.
for len(queue) > 0 {
// Берём вершину из начала очереди (принцип FIFO для BFS).
currentNode := queue[0]
// Удаляем первый элемент из очереди (сдвигаем начало среза).
// queue[1:] создаёт новый срез, указывающий на те же данные, но без первого элемента.
// Это эквивалент операции "dequeue" в очереди FIFO.
queue = queue[1:]
// Добавляем текущую вершину в результат обхода.
// Порядок добавления соответствует порядку посещения вершин (BFS order).
// append автоматически расширит срез при необходимости.
path = append(path, currentNode)
// Перебираем всех соседей (смежные вершины) текущей вершины.
// graph[currentNode] возвращает срез всех вершин, в которые можно перейти из currentNode.
// _ — игнорируем индекс, используем только значение (саму вершину-соседа).
for _, childNode := range graph[currentNode] {
// Проверяем, не посещали ли мы уже эту вершину.
// _, ok — comma ok идиома для проверки существования ключа в map.
// ok == true — вершина уже в множестве (посещена).
// !ok — вершина ещё не посещена, значит можно добавить в очередь.
if _, ok := visited[childNode]; !ok {
// Отмечаем соседнюю вершину как посещённую.
// Делаем это ДО добавления в очередь, чтобы избежать дублирования.
// Если отметить после добавления, вершина может попасть в очередь несколько раз.
visited[childNode] = struct{}{}
// Добавляем не посещённого соседа в конец очереди.
// Теперь он будет обработан после всех текущих вершин (FIFO).
// Это и есть суть BFS — сначала обрабатываем вершины на текущем уровне, потом на следующем.
queue = append(queue, childNode)
}
}
}
return path
} |
|
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| func TestBreadthFirstSearch(t *testing.T) {
testCases := []struct {
desc string
graph map[int][]int
start int
expected []int
}{
{
desc: "binary tree",
graph: map[int][]int{
0: {1, 2},
1: {3, 4},
2: {5, 6},
3: {},
4: {},
5: {},
6: {},
},
start: 0,
expected: []int{0, 1, 2, 3, 4, 5, 6},
},
{
desc: "peterson graph",
graph: map[int][]int{
0: {1, 5},
1: {2, 6},
2: {3, 7},
3: {4, 8},
4: {0, 9},
5: {7},
7: {9},
9: {6},
6: {8},
8: {5},
},
start: 0,
expected: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
},
{
desc: "single node",
graph: map[int][]int{
0: {},
},
start: 0,
expected: []int{0},
},
{
desc: "linear chain",
graph: map[int][]int{
0: {1},
1: {2},
2: {3},
3: {},
},
start: 0,
expected: []int{0, 1, 2, 3},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
result := breadthFirstSearch(tC.graph, tC.start)
got := make([]int, len(result))
copy(got, result)
exp := make([]int, len(tC.expected))
copy(exp, tC.expected)
slices.Sort(got)
slices.Sort(exp)
if !reflect.DeepEqual(got, exp) {
t.Errorf("breadthFirstSearch (%v, start=%d): expected %v, got %v", tC.graph, tC.start, tC.expected, result)
}
})
}
} |
|
| Code | 1
2
3
4
5
6
7
8
9
10
11
12
13
| go test -run TestBreadthFirstSearch -v
=== RUN TestBreadthFirstSearch
=== RUN TestBreadthFirstSearch/binary_tree
=== RUN TestBreadthFirstSearch/peterson_graph
=== RUN TestBreadthFirstSearch/single_node
=== RUN TestBreadthFirstSearch/linear_chain
--- PASS: TestBreadthFirstSearch (0.00s)
--- PASS: TestBreadthFirstSearch/binary_tree (0.00s)
--- PASS: TestBreadthFirstSearch/peterson_graph (0.00s)
--- PASS: TestBreadthFirstSearch/single_node (0.00s)
--- PASS: TestBreadthFirstSearch/linear_chain (0.00s)
PASS
ok github.com/alhaos/problems/misc 0.292s |
|
В процессе разбора работы алгоритма и написания поста импользовал claude.ai и deepseek
код доступен тут
А надо мне это было для решения вот этой задачи
| Go | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| // ConcurrentBFSQueries обходит граф конкурентно из нескольких вершин queries
func ConcurrentBFSQueries(graph map[int][]int, queries []int, numWorkers int) map[int][]int {
// К заданию был тест с нулевым количеством воркунов который должен вернуть map[int][]int{}
// поэтому просто оставлю это тут
if numWorkers == 0 {
return map[int][]int{}
}
// Приметив синхронизации WaitGroup
// используется для того чтобы дождаться выполнения всех параллельных
// горутин и двигаться дальше
var wg sync.WaitGroup
// Приметив синхронизации Mutex, реализация дефолтного map потоко не безопасна
// использую Mutex для безлопастной конкурентной работы с map
var mu sync.Mutex
// Map для результатов
result := make(map[int][]int)
// Шаблон семафор реализуется в go через канал буферизированный канал в который пишется
// пустая структура для экономии памяти, когда буфер заполнен следующая горутина
// пытающаяся записать в канал выхватывает блокировку до момента пока кто то не вычитает из канала
semaphore := make(chan struct{}, numWorkers)
// Цикл по узлам с которых начинается поиск в ширину
for _, query := range queries {
// занимаем один слот в WaitGroup
wg.Add(1)
// занимаем слот в семафоре
semaphore <- struct{}{}
// конкурентно запускаем функцию
go func(q int) {
// это выполнится в конце функции независимо от того где из нее вышли
// высвобождаем слот в WaitGroup
defer wg.Done()
// это выполнится в конце функции независимо от того где из нее вышли
// высвобождаем слот в семафоре
defer func() { <-semaphore }()
// получаем результат поиска
bfsResult := breadthFirstSearch(graph, q)
// вешаем блокировку
mu.Lock()
// безопасно добавляем результат
result[q] = bfsResult
// снимаем блокировку
mu.Unlock()
}(query)
}
// тут ждем пока все слоты в WaitGroup высвободятся
wg.Wait()
// возвращаем result
return result
} |
|
|