|
1 | 1 | # Обход графа глубину |
2 | | -https://leetcode.com/explore/learn/card/queue-stack/231/practical-application-queue/1376/ |
3 | | -https://leetcode.com/explore/learn/card/queue-stack/232/practical-application-stack/1377/ |
4 | 2 |
|
5 | | -https://blog.skillfactory.ru/glossary/bfs/ |
6 | | -https://blog.skillfactory.ru/glossary/dfs/ |
| 3 | +**Depth first search (DFS)** - поиск в глубину, способ обхода или нахождения узлов в графах и деревьях. Аналогично |
| 4 | +поиску в ширину(``BFS``) алгоритм ``DFS`` также обходит каждый узел структуры, однако в случае поиска в глубину |
| 5 | +учитывается иерархия хранения данных. Другими словами, поиск в ширину не осуществляет переход на следующий, более |
| 6 | +глубокий уровень до тех пор, пока не будут обработаны все узлы текущего уровня. Напротив, при реализации поиска в |
| 7 | +глубину алгоритм не выполняет переход к следующему узлу текущего уровня, пока не достигнет самого конечного элемента |
| 8 | +поддерева обрабатываемого элемента. Также следует заметить, что поиск в глубину ищет не самый короткий, а случайный |
| 9 | +путь. |
| 10 | + |
| 11 | +## Стек и DFS |
| 12 | + |
| 13 | +Обычно, алгоритм DFS реализуют при помощи стека и рекурсии. Шаблон реализации DFS с использованием стека и рекурсии |
| 14 | +представлен ниже: |
| 15 | + |
| 16 | +```python |
| 17 | +from typing import Optional, List, Set |
| 18 | + |
| 19 | + |
| 20 | +class Node: |
| 21 | + """ |
| 22 | + Graph node |
| 23 | + """ |
| 24 | + |
| 25 | + def __init__(self, data: int, children: Optional[List['Node']] = None): |
| 26 | + self.data = data |
| 27 | + self.children = children |
| 28 | + |
| 29 | + def __repr__(self): |
| 30 | + return str(self.data) |
| 31 | + |
| 32 | + def __hash__(self): |
| 33 | + return hash(self.data) |
| 34 | + |
| 35 | + def __eq__(self, other): |
| 36 | + return self.data == other.data |
| 37 | + |
| 38 | + |
| 39 | +def dfs(current: Node, target: int, visited: Set[Node]) -> Optional[Node]: |
| 40 | + """ |
| 41 | + Обход графа в глубину с поиском значения атрибута Node.data |
| 42 | + :param current: Начальная вершина графа. |
| 43 | + :param target: искомое значение |
| 44 | + :param visited: посещенные вершины |
| 45 | + :return: node - найденная вершина. Если не найдено, None |
| 46 | + """ |
| 47 | + if current.data == target: |
| 48 | + return current |
| 49 | + |
| 50 | + if current.children: |
| 51 | + for child in current.children: |
| 52 | + if child not in visited: |
| 53 | + visited.add(child) |
| 54 | + node = dfs(child, target, visited) |
| 55 | + if node: |
| 56 | + return node |
| 57 | + |
| 58 | + return None # вершина не найдена |
| 59 | +``` |
| 60 | + |
| 61 | +Может показаться, что мы не используем стек в данной реализации, однако здесь неявно применяется системный стек |
| 62 | +вызовов (call stack) - стек, хранящий информацию для возврата управления из функций в программу. Текущий размер стека |
| 63 | +равен глубине обхода, поэтому в худшем случае **пространственная сложность** обхода составит **O(h)**, где h - |
| 64 | +максимальная глубина графа или дерева. В случае очень большой глубины графа, при использовании шаблона выше может |
| 65 | +произойти переполнение системного стека вызова, и произойдет ошибка ``stack overflow``. Чтобы этого избежать, следует |
| 66 | +явно использовать стек в реализации: |
| 67 | + |
| 68 | +```python |
| 69 | +from typing import Optional, List |
| 70 | + |
| 71 | + |
| 72 | +class Node: |
| 73 | + """ |
| 74 | + Graph node |
| 75 | + """ |
| 76 | + |
| 77 | + def __init__(self, data: int, children: Optional[List['Node']] = None): |
| 78 | + self.data = data |
| 79 | + self.children = children |
| 80 | + |
| 81 | + def __repr__(self): |
| 82 | + return str(self.data) |
| 83 | + |
| 84 | + def __hash__(self): |
| 85 | + return hash(self.data) |
| 86 | + |
| 87 | + def __eq__(self, other): |
| 88 | + return self.data == other.data |
| 89 | + |
| 90 | + |
| 91 | +def dfs(root: Node, target: int) -> Optional[Node]: |
| 92 | + """ |
| 93 | + Обход графа в глубину с поиском значения атрибута Node.data |
| 94 | + :param root: Начальная вершина графа. |
| 95 | + :param target: искомое значение |
| 96 | + :return: node - найденная вершина. Если не найдено, None |
| 97 | + """ |
| 98 | + visited = set() # посещенные вершины |
| 99 | + stack = [root] |
| 100 | + while len(stack) > 0: # пока стек не пуст |
| 101 | + current = stack.pop() |
| 102 | + |
| 103 | + if current.data == target: |
| 104 | + return current |
| 105 | + |
| 106 | + if current.children: |
| 107 | + for child in current.children: |
| 108 | + if child not in visited: |
| 109 | + visited.add(child) |
| 110 | + stack.append(child) |
| 111 | + |
| 112 | + return None # вершина не найдена |
| 113 | +``` |
| 114 | + |
| 115 | +Также существует три типа обхода: ``pre-order``,``in-order`` и ``post-order``. |
| 116 | + |
| 117 | +## Прямой обход (pre-order) |
| 118 | + |
| 119 | +При pre-order обходе алгоритм следующий: |
| 120 | + |
| 121 | +1) Обработка целевого узла |
| 122 | +2) Обход левого поддерева целевого узла |
| 123 | +3) Обход правого поддерева целевого узла |
| 124 | + |
| 125 | +Ниже изображен обход бинарного дерева, где стрелками с номерами описан порядок действий |
| 126 | + |
| 127 | + |
| 128 | + |
| 129 | +Ниже представлены реализации рекурсивным способом ``preorder_recursive_dfs`` и с использованием |
| 130 | +стека ``preorder_stack_dfs``. |
| 131 | + |
| 132 | +```python |
| 133 | +from typing import Optional, Callable |
| 134 | + |
| 135 | + |
| 136 | +class TreeNode: |
| 137 | + def __init__(self, val=None, left=None, right=None): |
| 138 | + self.val = val |
| 139 | + self.left = left |
| 140 | + self.right = right |
| 141 | + |
| 142 | + def __repr__(self): |
| 143 | + return str(self.val) |
| 144 | + |
| 145 | + |
| 146 | +def preorder_recursive_dfs(node_function: Callable, node: Optional[TreeNode] = None): |
| 147 | + if node: |
| 148 | + node_function(node) |
| 149 | + |
| 150 | + preorder_recursive_dfs(node_function, node.left) |
| 151 | + preorder_recursive_dfs(node_function, node.right) |
| 152 | + |
| 153 | + |
| 154 | +def preorder_stack_dfs(node_function: Callable, node: Optional[TreeNode] = None): |
| 155 | + stack = [node] |
| 156 | + |
| 157 | + while stack: |
| 158 | + node = stack.pop() |
| 159 | + if node: |
| 160 | + node_function(node) |
| 161 | + |
| 162 | + if node.right: |
| 163 | + stack.append(node.right) |
| 164 | + |
| 165 | + if node.left: |
| 166 | + stack.append(node.left) |
| 167 | +``` |
| 168 | + |
| 169 | +## Центрированный обход (in-order) |
| 170 | + |
| 171 | +При in-order обходе алгоритм следующий: |
| 172 | + |
| 173 | +1) Обход левого поддерева целевого узла |
| 174 | +2) Обработка целевого узла |
| 175 | +3) Обход правого поддерева целевого узла |
| 176 | + |
| 177 | +Используя данный тип обхода бинарного дерева, мы получаем данные в отсортированном виде. Ниже представлены реализации |
| 178 | +рекурсивным способом ``inorder_recursive_dfs`` и с использованием стека ``inorder_stack_dfs``. |
| 179 | + |
| 180 | + |
| 181 | + |
| 182 | +```python |
| 183 | +from typing import Optional, Callable |
| 184 | + |
| 185 | + |
| 186 | +class TreeNode: |
| 187 | + def __init__(self, val=None, left=None, right=None): |
| 188 | + self.val = val |
| 189 | + self.left = left |
| 190 | + self.right = right |
| 191 | + |
| 192 | + def __repr__(self): |
| 193 | + return str(self.val) |
| 194 | + |
| 195 | + |
| 196 | +def inorder_recursive_dfs(node_function: Callable, node: Optional[TreeNode] = None): |
| 197 | + if node: |
| 198 | + inorder_recursive_dfs(node_function, node.left) |
| 199 | + node_function(node) |
| 200 | + inorder_recursive_dfs(node_function, node.right) |
| 201 | + |
| 202 | + |
| 203 | +def inorder_stack_dfs(node_function: Callable, node: Optional[TreeNode] = None): |
| 204 | + stack = [] |
| 205 | + current = node |
| 206 | + while current or len(stack) > 0: |
| 207 | + while current: |
| 208 | + stack.append(current) |
| 209 | + current = current.left |
| 210 | + |
| 211 | + current = stack.pop() |
| 212 | + node_function(current) |
| 213 | + |
| 214 | + current = current.right |
| 215 | +``` |
| 216 | + |
| 217 | +## Обратный обход (post-order) |
| 218 | + |
| 219 | +При post-order обходе алгоритм следующий: |
| 220 | + |
| 221 | +1) Обход левого поддерева целевого узла |
| 222 | +2) Обход правого поддерева целевого узла |
| 223 | +3) Обработка целевого узла |
| 224 | + |
| 225 | + |
| 226 | + |
| 227 | +```python |
| 228 | +from typing import Optional, Callable |
| 229 | + |
| 230 | + |
| 231 | +class TreeNode: |
| 232 | + def __init__(self, val=None, left=None, right=None): |
| 233 | + self.val = val |
| 234 | + self.left = left |
| 235 | + self.right = right |
| 236 | + |
| 237 | + def __repr__(self): |
| 238 | + return str(self.val) |
| 239 | + |
| 240 | + |
| 241 | +def postorder_recursive_dfs(node_function: Callable, node: Optional[TreeNode] = None): |
| 242 | + if node: |
| 243 | + postorder_recursive_dfs(node_function, node.left) |
| 244 | + postorder_recursive_dfs(node_function, node.right) |
| 245 | + |
| 246 | + node_function(node) |
| 247 | + |
| 248 | + |
| 249 | +def postorder_stack_dfs(node_function: Callable, node: Optional[TreeNode] = None): |
| 250 | + stack = [] |
| 251 | + last_node_visited = None |
| 252 | + while node or len(stack) > 0: |
| 253 | + if node: |
| 254 | + stack.append(node) |
| 255 | + node = node.left |
| 256 | + else: |
| 257 | + peek_node = stack[-1] |
| 258 | + if peek_node.right and last_node_visited != peek_node.right: |
| 259 | + node = peek_node.right |
| 260 | + else: |
| 261 | + node_function(peek_node) |
| 262 | + last_node_visited = stack.pop() |
| 263 | +``` |
| 264 | + |
| 265 | +## Сложность операций |
| 266 | + |
| 267 | +Для всех ``pre-order``,``in-order`` и ``post-order`` обходов: |
| 268 | + |
| 269 | +Временная сложность - **O(n), где n - общее количество узлов в дереве |
| 270 | + |
| 271 | +Пространственная сложность - **O(n). Пространственная сложность зависит от высоты дерева и в худшем случае равна |
| 272 | +количеству узлов в нем. |
0 commit comments