Форум программистов, компьютерный форум, киберфорум
alhaos
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

[golang] Breadth-First Search

Запись от alhaos размещена 19.05.2026 в 14:47. Обновил(-а) alhaos 19.05.2026 в 20:51
Показов 830 Комментарии 0
Метки algorithms, golang, graph

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
}
Метки algorithms, golang, graph
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
[golang] Двоичная куча, min-heap
alhaos 20.05.2026
Двоичная куча Двоичная куча — структура данных, которая всегда держит самый важный элемент наготове. Представьте очередь к хилеру в игре, и очередь из игроков в приоритете те у кого меньше. . .
[golang] Breadth-First Search
alhaos 19.05.2026
BFS (Breadth-First Search) — это базовый алгоритм обхода графа в ширину, который поуровнево исследует все связанные вершины. Он начинает с выбранной точки и проверяет всех соседей, прежде чем. . .
[golang] Алгоритм «Хак Госпера»
alhaos 17.05.2026
Алгоритм «Хак Госпера» Хак Госпера (Gosper's Hack) — алгоритм нахождения следующего по величине числа с тем же количеством установленных бит. Придуман Биллом Госпером в 1970-х, опубликован в. . .
Рисование бинарного древа до 6-го колена на js, svg.
russiannick 17.05.2026
<svg width="335" height="240" viewBox="0 0 335 240" fill="#e5e1bb"> <style> <!]> </ style> <g id="bush"> </ g> </ svg> function fn(){ let rost;/ / высота древа let xx=165,yy=210,w=256;
FSharp: interface of module
DevAlt 16.05.2026
Интерфейс модуля F# позволяет управлять доступностью членов, содержащихся в реализации модуля. По-умолчанию все члены модуля доступны: module Foo let x = 10 let boo () = printfn "boo" . . .
Хитросплетение родственных связей пантеона греческих богов.
russiannick 14.05.2026
Однооконник, позволяющий узреть и изучить отдельных героев древней Греции. <!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible". . .
[golang] Угол между стрелками часов
alhaos 12.05.2026
По заданным значениям часа и минуты необходимо определить значение меньшего угла между стрелками аналогового циферблата часов. import "math" func angleClock(hour int, minutes int) float64 { . . .
Debian 13: Установка Lazarus QT5
ВитГо 09.05.2026
Эта инструкция моя компиляция инструкций volvo https:/ / www. cyberforum. ru/ blogs/ 203668/ 10753. html и его же старой инструкции по установке Lazarus с gtk2. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru