|
4 | 4 | [](https://github.com/maxbarsukov-itmo/functional-programming-2/actions/workflows/markdown.yml)
|
5 | 5 | [](https://coveralls.io/github/maxbarsukov-itmo/functional-programming-2?branch=master)
|
6 | 6 |
|
7 |
| -## Вариант: `rb-set` |
| 7 | +## Вариант `rb-set` |
8 | 8 |
|
9 | 9 | <img alt="dancing" src="./.resources/anime.gif" height="240">
|
10 | 10 |
|
|
26 | 26 |
|
27 | 27 | ---
|
28 | 28 |
|
29 |
| -Интерфейс — *Red-Black Tree*, структура данных — *Set*. |
| 29 | +## Требования |
| 30 | + |
| 31 | +Интерфейс — `Set`, структура данных — `Red-Black Tree`. |
| 32 | + |
| 33 | +1. Функции: |
| 34 | + * [x] добавление и удаление элементов; |
| 35 | + * [x] фильтрация; |
| 36 | + * [x] отображение (`map`); |
| 37 | + * [x] свертки (левая и правая); |
| 38 | + * [x] структура должна быть [моноидом](https://ru.m.wikipedia.org/wiki/%D0%9C%D0%BE%D0%BD%D0%BE%D0%B8%D0%B4). |
| 39 | +2. Структуры данных должны быть **неизменяемыми**. |
| 40 | +3. Библиотека должна быть протестирована в рамках **unit testing**. |
| 41 | +4. Библиотека должна быть протестирована в рамках **property-based** тестирования (*как минимум 3 свойства*, включая свойства моноида). |
| 42 | +5. Структура должна быть **полиморфной**. |
| 43 | +6. Требуется использовать идиоматичный для технологии стиль программирования. Примечание: некоторые языки позволяют получить большую часть API через реализацию небольшого интерфейса. Так как лабораторная работа про ФП, а не про экосистему языка — необходимо реализовать их вручную и по возможности — обеспечить совместимость. |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +## Ключевые элементы реализации |
| 48 | + |
| 49 | +Добавление, получение и удаление элементов: |
| 50 | + |
| 51 | +```elixir |
| 52 | +# in `rb_set.ex` |
| 53 | + |
| 54 | +@spec put(t, any) :: t |
| 55 | +def put(%RBSet{tree: tree}, element) do |
| 56 | + %RBSet{tree: RedBlackTree.insert(tree, element, element)} |
| 57 | +end |
| 58 | + |
| 59 | +@spec delete(t, any) :: t |
| 60 | +def delete(%RBSet{tree: tree}, element) do |
| 61 | + %RBSet{tree: RedBlackTree.delete(tree, element)} |
| 62 | +end |
| 63 | + |
| 64 | +@spec member?(t, any) :: boolean |
| 65 | +def member?(%RBSet{tree: tree}, element) do |
| 66 | + RedBlackTree.has_key?(tree, element) |
| 67 | +end |
| 68 | + |
| 69 | + |
| 70 | +# in `red_black_tree.ex` |
| 71 | + |
| 72 | +def insert(%RedBlackTree{root: nil} = tree, key, value) do |
| 73 | + %RedBlackTree{tree | root: Node.new(key, value), size: 1} |
| 74 | +end |
| 75 | + |
| 76 | +def insert(%RedBlackTree{root: root, size: size, comparator: comparator} = tree, key, value) do |
| 77 | + {nodes_added, new_root} = do_insert(root, key, value, 1, comparator) |
| 78 | + |
| 79 | + %RedBlackTree{ |
| 80 | + tree |
| 81 | + | root: make_node_black(new_root), |
| 82 | + size: size + nodes_added |
| 83 | + } |
| 84 | +end |
| 85 | + |
| 86 | +defp do_insert(nil, insert_key, insert_value, depth, _comparator) do |
| 87 | + { |
| 88 | + 1, |
| 89 | + %Node{ |
| 90 | + Node.new(insert_key, insert_value, depth) |
| 91 | + | color: :red |
| 92 | + } |
| 93 | + } |
| 94 | +end |
| 95 | + |
| 96 | +defp do_insert(%Node{key: node_key} = node, insert_key, insert_value, depth, comparator) do |
| 97 | + case comparator.(insert_key, node_key) do |
| 98 | + 0 -> {0, %Node{node | value: insert_value}} |
| 99 | + -1 -> do_insert_left(node, insert_key, insert_value, depth, comparator) |
| 100 | + 1 -> do_insert_right(node, insert_key, insert_value, depth, comparator) |
| 101 | + end |
| 102 | +end |
| 103 | + |
| 104 | +defp do_insert_left(%Node{left: left} = node, insert_key, insert_value, depth, comparator) do |
| 105 | + {nodes_added, new_left} = do_insert(left, insert_key, insert_value, depth + 1, comparator) |
| 106 | + {nodes_added, %Node{node | left: do_balance(new_left)}} |
| 107 | +end |
| 108 | + |
| 109 | +defp do_insert_right(%Node{right: right} = node, insert_key, insert_value, depth, comparator) do |
| 110 | + {nodes_added, new_right} = do_insert(right, insert_key, insert_value, depth + 1, comparator) |
| 111 | + {nodes_added, %Node{node | right: do_balance(new_right)}} |
| 112 | +end |
| 113 | + |
| 114 | +def delete(%RedBlackTree{root: root, size: size, comparator: comparator} = tree, key) do |
| 115 | + {nodes_removed, new_root} = do_delete(root, key, comparator) |
| 116 | + |
| 117 | + %RedBlackTree{ |
| 118 | + tree |
| 119 | + | root: new_root, |
| 120 | + size: size - nodes_removed |
| 121 | + } |
| 122 | +end |
| 123 | + |
| 124 | +defp do_delete(nil, _key, _comparator) do |
| 125 | + {0, nil} |
| 126 | +end |
| 127 | + |
| 128 | +defp do_delete(%Node{key: node_key} = node, delete_key, comparator) do |
| 129 | + case comparator.(delete_key, node_key) do |
| 130 | + 0 -> do_delete_node(node) |
| 131 | + -1 -> do_delete_left(node, delete_key, comparator) |
| 132 | + 1 -> do_delete_right(node, delete_key, comparator) |
| 133 | + end |
| 134 | +end |
| 135 | + |
| 136 | +defp do_delete_node(%Node{left: left, right: right}) do |
| 137 | + cond do |
| 138 | + # If both the right and left are nil, the new tree is nil. For example, |
| 139 | + # deleting A in the following tree results in B having no left |
| 140 | + # |
| 141 | + # B |
| 142 | + # / \ |
| 143 | + # A C |
| 144 | + # |
| 145 | + left === nil && right === nil -> |
| 146 | + {1, nil} |
| 147 | + |
| 148 | + # If left is nil and there is a right, promote the right. For example, |
| 149 | + # deleting C in the following tree results in B's right becoming D |
| 150 | + # |
| 151 | + # B |
| 152 | + # / \ |
| 153 | + # A C |
| 154 | + # \ |
| 155 | + # D |
| 156 | + # |
| 157 | + left === nil && right -> |
| 158 | + {1, %Node{right | depth: right.depth - 1}} |
| 159 | + |
| 160 | + # If there is only a left promote it. For example, |
| 161 | + # deleting B in the following tree results in C's left becoming A |
| 162 | + # |
| 163 | + # C |
| 164 | + # / \ |
| 165 | + # B D |
| 166 | + # / |
| 167 | + # A |
| 168 | + # |
| 169 | + left && right === nil -> |
| 170 | + {1, %Node{left | depth: left.depth - 1}} |
| 171 | + |
| 172 | + # If there are both left and right nodes, recursively promote the left-most |
| 173 | + # nodes. For example, deleting E below results in the following: |
| 174 | + # |
| 175 | + # G => G |
| 176 | + # / \ / \ |
| 177 | + # E H => C H |
| 178 | + # / \ / \ |
| 179 | + # C F => B D |
| 180 | + # / \ / \ |
| 181 | + # A D => A F |
| 182 | + # \ |
| 183 | + # B |
| 184 | + # |
| 185 | + # |
| 186 | + true -> |
| 187 | + { |
| 188 | + 1, |
| 189 | + do_balance(%Node{ |
| 190 | + left |
| 191 | + | depth: left.depth - 1, |
| 192 | + left: do_balance(promote(left)), |
| 193 | + right: right |
| 194 | + }) |
| 195 | + } |
| 196 | + end |
| 197 | +end |
| 198 | + |
| 199 | +defp do_delete_left(%Node{left: left} = node, delete_key, comparator) do |
| 200 | + {nodes_removed, new_left} = do_delete(left, delete_key, comparator) |
| 201 | + |
| 202 | + { |
| 203 | + nodes_removed, |
| 204 | + %Node{ |
| 205 | + node |
| 206 | + | left: do_balance(new_left) |
| 207 | + } |
| 208 | + } |
| 209 | +end |
| 210 | + |
| 211 | +defp do_delete_right(%Node{right: right} = node, delete_key, comparator) do |
| 212 | + {nodes_removed, new_right} = do_delete(right, delete_key, comparator) |
| 213 | + |
| 214 | + { |
| 215 | + nodes_removed, |
| 216 | + %Node{ |
| 217 | + node |
| 218 | + | right: do_balance(new_right) |
| 219 | + } |
| 220 | + } |
| 221 | +end |
| 222 | + |
| 223 | + |
| 224 | +def get(%RedBlackTree{root: root, comparator: comparator}, key) do |
| 225 | + do_get(root, key, comparator) |
| 226 | +end |
| 227 | + |
| 228 | +defp do_get(nil, _key, _comparator) do |
| 229 | + nil |
| 230 | +end |
| 231 | + |
| 232 | +defp do_get(%Node{key: node_key, left: left, right: right, value: value}, get_key, comparator) do |
| 233 | + case comparator.(get_key, node_key) do |
| 234 | + 0 -> value |
| 235 | + -1 -> do_get(left, get_key, comparator) |
| 236 | + 1 -> do_get(right, get_key, comparator) |
| 237 | + end |
| 238 | +end |
| 239 | +``` |
| 240 | + |
| 241 | +Фильтрация: |
| 242 | + |
| 243 | +```elixir |
| 244 | +# in `rb_set.ex` |
| 245 | + |
| 246 | +@spec filter(t, (any -> boolean)) :: t |
| 247 | +def filter(set, predicate) do |
| 248 | + Enum.reduce(set, new(), fn element, acc -> |
| 249 | + if predicate.(element) do |
| 250 | + put(acc, element) |
| 251 | + else |
| 252 | + acc |
| 253 | + end |
| 254 | + end) |
| 255 | +end |
| 256 | +``` |
| 257 | + |
| 258 | +Отображение (`map`): |
| 259 | + |
| 260 | +```elixir |
| 261 | +# in `rb_set.ex` |
| 262 | + |
| 263 | +@spec map(t, (any -> any)) :: t |
| 264 | +def map(set, fun) do |
| 265 | + Enum.reduce(set, new(), fn element, acc -> put(acc, fun.(element)) end) |
| 266 | +end |
| 267 | +``` |
| 268 | + |
| 269 | +Свертки (левая и правая): |
| 270 | + |
| 271 | +```elixir |
| 272 | +# in `rb_set.ex` |
| 273 | + |
| 274 | +@spec reduce(t, any, any) :: any |
| 275 | +def reduce(set, acc, fun), do: fold_right(set, acc, fun) |
| 276 | +def fold_left(set, acc, fun), do: RedBlackTree.fold_left(RBSet.to_list(set), acc, fun) |
| 277 | +def fold_right(set, acc, fun), do: RedBlackTree.fold_right(RBSet.to_list(set), acc, fun) |
| 278 | + |
| 279 | + |
| 280 | +# in `red_black_tree.ex` |
| 281 | + |
| 282 | +def fold_left(list, acc, fun), do: do_fold_left(list, acc, fun) |
| 283 | +defp do_fold_left([], acc, _fun), do: acc |
| 284 | + |
| 285 | +defp do_fold_left([head | tail], acc, fun) do |
| 286 | + new_acc = fun.(acc, head) |
| 287 | + do_fold_left(tail, new_acc, fun) |
| 288 | +end |
| 289 | + |
| 290 | +def fold_right(list, acc, fun), do: do_fold_right(list, acc, fun) |
| 291 | +defp do_fold_right([], acc, _fun), do: acc |
| 292 | + |
| 293 | +defp do_fold_right([head | tail], acc, fun) do |
| 294 | + do_fold_right(tail, fun.(head, acc), fun) |
| 295 | +end |
| 296 | + |
| 297 | +def reduce(%RedBlackTree{root: nil}, acc, _fun), do: acc |
| 298 | + |
| 299 | +def reduce(tree, acc, fun) do |
| 300 | + to_list(tree) |> fold_right(acc, fun) |
| 301 | +end |
| 302 | +``` |
| 303 | + |
| 304 | +### Соответствие свойству [моноида]((https://ru.m.wikipedia.org/wiki/%D0%9C%D0%BE%D0%BD%D0%BE%D0%B8%D0%B4)) |
| 305 | + |
| 306 | +Определили пустой элемент: |
| 307 | + |
| 308 | +```elixir |
| 309 | +# in `rb_set.ex` |
| 310 | + |
| 311 | +@spec new() :: t |
| 312 | +def new do |
| 313 | + %RBSet{tree: RedBlackTree.new()} |
| 314 | +end |
| 315 | + |
| 316 | + |
| 317 | +# in `red_black_tree.ex` |
| 318 | + |
| 319 | +def empty, do: new() |
| 320 | +def new, do: %RedBlackTree{} |
| 321 | +``` |
| 322 | + |
| 323 | +Определили бинарную операцию `union`: |
| 324 | + |
| 325 | +```elixir |
| 326 | +# in `rb_set.ex` |
| 327 | + |
| 328 | +@spec union(t, t) :: t |
| 329 | +def union(%RBSet{tree: tree1}, %RBSet{tree: tree2}) do |
| 330 | + %RBSet{tree: RedBlackTree.union(tree1, tree2)} |
| 331 | +end |
| 332 | + |
| 333 | +@spec t ||| t :: t |
| 334 | +def set1 ||| set2 do |
| 335 | + union(set1, set2) |
| 336 | +end |
| 337 | + |
| 338 | + |
| 339 | +# in `red_black_tree.ex` |
| 340 | + |
| 341 | +@spec union(t(), t()) :: t |
| 342 | +def union(%RedBlackTree{root: nil} = _tree1, %RedBlackTree{} = tree2), do: tree2 |
| 343 | +def union(%RedBlackTree{} = tree1, %RedBlackTree{root: nil}), do: tree1 |
| 344 | +def union(%RedBlackTree{} = tree1, %RedBlackTree{} = tree2) do |
| 345 | + RedBlackTree.reduce(tree1, tree2, fn {k, v}, acc -> RedBlackTree.insert(acc, k, v) end) |
| 346 | +end |
| 347 | +``` |
| 348 | + |
| 349 | +## Тестирование |
| 350 | + |
| 351 | +В рамках данной работы были применены два инструмента: |
| 352 | + |
| 353 | + * [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html) - для модульного тестирования; |
| 354 | + * [Quixir](https://github.com/pragdave/quixir) - для тестирования свойств (property-based). |
30 | 355 |
|
31 | 356 | ## Выводы
|
32 | 357 |
|
33 |
| -... |
| 358 | +В данной лабораторной работе была реализована структура данных "Красно-чёрное дерево" (Red-Black Tree). Красно-чёрное дерево - это самобалансирующееся двоичное дерево поиска, которое обеспечивает эффективную вставку, удаление и поиск элементов. |
| 359 | + |
| 360 | +В реализации были использованы следующие приёмы программирования: |
| 361 | + |
| 362 | + * Использование модулей: для реализации красно-чёрного дерева был создан отдельный модуль `RedBlackTree`, а для реализации множества на основе красно-чёрного дерева - модуль `RBSet`. |
| 363 | + * Использование структур данных: для представления узлов дерева была использована структура `Node`, а для представления самого дерева - структура `RedBlackTree`. |
| 364 | + * Для реализации операций над деревом, таких как вставка, удаление и поиск, были использованы функции высшего порядка. |
| 365 | + * Для реализации логики работы с деревом был использован pattern matching, который позволяет эффективно обрабатывать различные случаи и исключения. |
| 366 | + * Для реализации операций над деревом, таких как обход дерева и поиск элементов, была использована рекурсия -- обычная и хвостовая. |
0 commit comments