diff --git a/lab2/README.md b/lab2/README.md deleted file mode 100644 index bc982b0..0000000 --- a/lab2/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# lab2 - -[![Package Version](https://img.shields.io/hexpm/v/lab2)](https://hex.pm/packages/lab2) -[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/lab2/) - -```sh -gleam add lab2@1 -``` -```gleam -import lab2 - -pub fn main() -> Nil { - // TODO: An example of the project in use -} -``` - -Further documentation can be found at . - -## Development - -```sh -gleam run # Run the project -gleam test # Run the tests -``` - -# Red-Black Tree с ленивыми вычислениями - -## Описание - -Реализация красно-чёрного дерева (Red-Black Tree) с ленивыми вычислениями на языке Gleam. Красно-чёрное дерево - это самобалансирующееся двоичное дерево поиска, которое гарантирует логарифмическую сложность операций вставки, удаления и поиска. - -### Особенности реализации - -- **Ленивые вычисления**: Поддеревья создаются только при необходимости, что экономит память и время -- **Полиморфность**: Дерево работает с любыми типами ключей и значений -- **Неизменяемость**: Все операции создают новые версии дерева, не изменяя исходное -- **Структура моноида**: Поддерживает операции объединения с нейтральным элементом - -## API - -### Основные операции - -- `empty()` - создание пустого дерева -- `insert(tree, key, value, compare)` - вставка элемента -- `lookup(tree, key, compare)` - поиск элемента -- `delete(tree, key, compare)` - удаление элемента -- `contains(tree, key, compare)` - проверка наличия ключа - -### Функции высшего порядка - -- `map(tree, f)` - отображение функции на все значения -- `filter(tree, predicate, compare)` - фильтрация элементов -- `fold_left(tree, acc, f)` - левая свёртка -- `fold_right(tree, acc, f)` - правая свёртка - -### Операции моноида - -- `mempty()` - нейтральный элемент (пустое дерево) -- `concat(tree1, tree2, compare)` - объединение деревьев - -### Вспомогательные функции - -- `size(tree)` - размер дерева -- `to_list(tree)` - преобразование в список -- `from_list(list, compare)` - создание из списка -- `keys(tree)` - получение всех ключей -- `values(tree)` - получение всех значений -- `equal(tree1, tree2, key_eq, value_eq)` - сравнение деревьев - -## Сложность операций - -| Операция | Сложность | -|----------|-----------| -| lookup | O(log n) | -| insert | O(log n) | -| delete | O(log n) | -| size | O(n) | -| map | O(n) | -| filter | O(n) | -| fold | O(n) | - -## Пример использования - -```gleam -import lab2 -import gleam/int - -// Функция сравнения для целых чисел -fn int_compare(a: Int, b: Int) -> order.Order { - int.compare(a, b) -} - -pub fn main() { - // Создание дерева и вставка элементов - let tree = lab2.empty() - |> lab2.insert(5, "five", int_compare) - |> lab2.insert(3, "three", int_compare) - |> lab2.insert(7, "seven", int_compare) - |> lab2.insert(1, "one", int_compare) - - // Поиск элемента - case lab2.lookup(tree, 5, int_compare) { - Some(value) -> io.println("Found: " <> value) - None -> io.println("Not found") - } - - // Фильтрация чётных ключей - let even_tree = lab2.filter(tree, fn(key, _) { key % 2 == 0 }, int_compare) - - // Отображение: удвоение длины строк - let doubled = lab2.map(tree, fn(value) { value <> value }) - - // Свёртка: подсчёт элементов - let count = lab2.fold_left(tree, 0, fn(acc, _, _) { acc + 1 }) -} -``` - -## Свойства моноида - -Структура данных удовлетворяет аксиомам моноида: - -1. **Ассоциативность**: `concat(concat(a, b), c) ≡ concat(a, concat(b, c))` -2. **Левая единица**: `concat(mempty(), a) ≡ a` -3. **Правая единица**: `concat(a, mempty()) ≡ a` - -## Тестирование - -Проект включает в себя comprehensive test suite: - -- **Unit тесты**: Проверка корректности отдельных функций -- **Property-based тесты**: Проверка инвариантов и свойств структуры данных -- **Тесты моноида**: Проверка аксиом моноида - -Запуск тестов: -```bash -gleam test -``` - -## Инварианты красно-чёрного дерева - -1. Каждый узел либо красный, либо чёрный -2. Корень дерева чёрный -3. Все листья (NULL-узлы) чёрные -4. Если узел красный, то оба его потомка чёрные -5. Для каждого узла все пути от него до листьев содержат одинаковое количество чёрных узлов - -Эти инварианты гарантируют, что самый длинный путь от корня до листа не более чем в 2 раза длиннее самого короткого пути, что обеспечивает логарифмическую сложность операций. diff --git a/lab2/Report.md b/lab2/Report.md index 903d88a..21a47d9 100644 --- a/lab2/Report.md +++ b/lab2/Report.md @@ -4,7 +4,7 @@ **Факультет программной инженерии и компьютерной техники** **Студент:** Владимиров Владислав Александрович -**Группа:** P3222 +**Группа:** P3322 **Тема:** Реализация Red-Black Tree с ленивыми вычислениями **Лабораторная работа №2** @@ -15,7 +15,28 @@ ## Требования к разработанному ПО ### Цель работы -Освоиться с построением пользовательских типов данных, полиморфизмом, рекурсивными алгоритмами и средствами тестирования (unit testing, property-based testing), а также разделением интерфейса и особенностей реализации. +Цель: освоиться с построением пользовательских типов данных, полиморфизмом, рекурсивными алгоритмами и средствами тестирования (unit testing, property-based testing), а также разделением интерфейса и особенностей реализации. + +В рамках лабораторной работы вам предлагается реализовать одну из предложенных классических структур данных (список, дерево, бинарное дерево, hashmap, граф...). + +Требования: + +1. Функции: + - добавление и удаление элементов; + - фильтрация; + - отображение (map); + - свертки (левая и правая); + - структура должна быть [моноидом](https://ru.m.wikipedia.org/wiki/Моноид). +2. Структуры данных должны быть неизменяемыми. +3. Библиотека должна быть протестирована в рамках unit testing. +4. Библиотека должна быть протестирована в рамках property-based тестирования (как минимум 3 свойства, включая свойства моноида). +5. Структура должна быть полиморфной. +6. Требуется использовать идиоматичный для технологии стиль программирования. Примечание: некоторые языки позволяют получить большую часть API через реализацию небольшого интерфейса. Так как лабораторная работа про ФП, а не про экосистему языка -- необходимо реализовать их вручную и по возможности -- обеспечить совместимость. +7. Обратите внимание: + - API должно быть реализовано для заданного интерфейса и оно не должно "протекать". На уровне тестов -- в первую очередь нужно протестировать именно API (dict, set, bag). + - Должна быть эффективная реализация функции сравнения (не наивное приведение к спискам, их сортировка с последующим сравнением), реализованная на уровне API, а не внутреннего представления. + + ### Вариант задания **Red-Black Tree Lazy** - красно-чёрное дерево на ленивых вычислениях. @@ -55,19 +76,19 @@ ### Основные типы данных ```gleam -/// Цвета узлов красно-черного дерева +// Цвета узлов красно-черного дерева pub type Color { Red Black } -/// Ленивое значение для отложенных вычислений +// Ленивое значение для отложенных вычислений pub type Lazy(a) { Thunk(fn() -> a) // Отложенное вычисление Value(a) // Уже вычисленное значение } -/// Красно-черное дерево с ленивыми вычислениями +// Красно-черное дерево с ленивыми вычислениями pub type RBTree(k, v) { Empty // Пустое дерево Node( @@ -83,7 +104,7 @@ pub type RBTree(k, v) { ### Управление ленивыми вычислениями ```gleam -/// Форсирует вычисление ленивого значения +// Форсирует вычисление ленивого значения fn force(lazy: Lazy(a)) -> a { case lazy { Value(val) -> val @@ -91,7 +112,7 @@ fn force(lazy: Lazy(a)) -> a { } } -/// Создаёт ленивое значение из функции +// Создаёт ленивое значение из функции fn delay(f: fn() -> a) -> Lazy(a) { Thunk(f) } @@ -171,6 +192,40 @@ n balance( ### Основные операции +**Главное - сравнение** +```gleam +// Вспомогательная функция для структурного сравнения деревьев +// Использует замыкание при первом несовпадении +fn equal( + tree1: RBTree(k, v), + tree2: RBTree(k, v), + key_compare: fn(k, k) -> Bool, + value_compare: fn(v, v) -> Bool, +) -> Bool { + case tree1, tree2 { + // Оба дерева пустые - равны + Empty, Empty -> True + + Empty, _ -> False + _, Empty -> False + + Node(color1, key1, value1, left1, right1), + Node(color2, key2, value2, left2, right2) + -> { + color1 == color2 + // Ключи должны быть равны + && key_compare(key1, key2) + // Значения должны быть равны + && value_compare(value1, value2) + // Рекурсивно проверяем левые поддеревья (ленивые) + && equal_helper(force(left1), force(left2), key_compare, value_compare) + // Рекурсивно проверяем правые поддеревья (ленивые) + && equal_helper(force(right1), force(right2), key_compare, value_compare) + } + } +} +``` + **Вставка элемента:** ```gleam pub fn insert(tree: RBTree(k, v), key: k, value: v, @@ -228,12 +283,12 @@ pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a { ### Операции моноида ```gleam -/// Нейтральный элемент +// Нейтральный элемент pub fn mempty() -> RBTree(k, v) { empty() } -/// Операция объединения +// Операция объединения pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v), compare: fn(k, k) -> Order) -> RBTree(k, v) { fold_left(tree2, tree1, fn(acc, key, value) { @@ -247,14 +302,19 @@ pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v), ## Тесты и метрики ### Unit тесты - - `empty_tree_test()` - создание пустого дерева - - `single_insert_test()` - вставка одного элемента - - `multiple_insert_test()` - множественные вставки - - `delete_test()` - удаление элементов - - `filter_test()` - тестирование фильтрации - - `map_test()` - тестирование отображения - - `fold_left_test()`, `fold_right_test()` - тестирование свёрток - - `to_from_list_test()` - конвертация в список и обратно +- `empty_tree_test()` - создание пустого дерева и проверка `is_empty()`, `size()` +- `single_insert_test()` - вставка одного элемента, проверка `lookup()` и размера +- `multiple_insert_test()` - множественные вставки с проверкой всех элементов +- `delete_test()` - удаление элементов с проверкой корректности +- `update_existing_key_test()` - обновление существующих ключей +- `filter_test()` - фильтрация четных чисел, проверка размера и содержимого +- `map_test()` - отображение значений (умножение на 2), проверка корректности +- `fold_left_test()` - левая свёртка для суммирования значений +- `fold_right_test()` - правая свёртка для конкатенации строк +- `to_from_list_test()` - конвертация дерево→список→дерево с проверкой эквивалентности +- `red_black_invariant_test()` - работа с большим деревом (10 элементов), проверка балансировки +- `semantic_equal_test()` - семантическое равенство (независимо от структуры) +- `structure_equal_test()` - структурное равенство без учёта цветов узлов ### Property-based тесты @@ -279,92 +339,44 @@ pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v), ### Тесты моноида ```gleam -/// Левая единица: mempty ∘ a = a +// Левая единица: mempty ∘ a = a pub fn monoid_left_identity_test() -/// Правая единица: a ∘ mempty = a +// Правая единица: a ∘ mempty = a pub fn monoid_right_identity_test() -/// Ассоциативность: (a ∘ b) ∘ c = a ∘ (b ∘ c) +// Ассоциативность: (a ∘ b) ∘ c = a ∘ (b ∘ c) pub fn monoid_associativity_test() -/// Коммутативность (для непересекающихся множеств ключей) +// Коммутативность (для непересекающихся множеств ключей) pub fn monoid_commutativity_test() ``` ### Отчёт тестирования ``` -Running lab2_test.main -..................... -21 passed, no failures +> gleam test + Compiling lab2 Compiled in 0.46s + Running lab2_test.main +....................... +23 passed, no failures ``` ### Метрики производительности -| Операция | Теоретическая сложность | Реализованная сложность | -|----------|------------------------|------------------------| -| lookup | O(log n) | O(log n) | -| insert | O(log n) | O(log n) | -| delete | O(log n) | O(log n) | -| size | O(n) | O(n) | -| map | O(n) | O(n) | -| filter | O(n log n) | O(n log n) | -| fold | O(n) | O(n) | +| Операция | Сложность| +|----------|------------------------| +| lookup | O(log n) | +| insert | O(log n) | +| delete | O(log n) | +| size | O(n) | +| map | O(n) | +| filter | O(n log n) | +| fold | O(n) | --- ## Выводы - -### Использованные приёмы программирования - -1. **Ленивые вычисления (Lazy Evaluation):** - - **Преимущества:** Экономия памяти, возможность работы с потенциально бесконечными структурами, отложенные вычисления только при необходимости - - **Реализация:** Тип `Lazy(a)` с конструкторами `Thunk` и `Value` - - **Эффект:** Поддеревья создаются только при обращении к ним, что снижает накладные расходы - -2. **Полиморфизм:** - - **Преимущества:** Универсальность структуры данных для любых типов ключей и значений - - **Реализация:** Параметрические типы `RBTree(k, v)` - - **Эффект:** Возможность использования с различными типами данных без дублирования кода - -3. **Неизменяемые структуры данных:** - - **Преимущества:** Отсутствие побочных эффектов, thread-safety, упрощение рассуждений о коде - - **Реализация:** Все операции возвращают новые версии дерева - - **Эффект:** Функциональная чистота и предсказуемость - -4. **Структурная рекурсия:** - - **Преимущества:** Естественность выражения алгоритмов для древовидных структур - - **Реализация:** Паттерн-матчинг на конструкторах типа - - **Эффект:** Читаемый и понятный код - -5. **Моноид (Monoid):** - - **Преимущества:** Математические гарантии корректности операций объединения - - **Реализация:** Операции `mempty()` и `concat()` с проверкой аксиом - - **Эффект:** Композируемость и предсказуемость операций - -6. **Функции высшего порядка:** - - **Преимущества:** Абстракция над общими паттернами обработки данных - - **Реализация:** `map`, `filter`, `fold_left`, `fold_right` - - **Эффект:** Выразительность и переиспользуемость кода - -### Особенности языка Gleam - -1. **Система типов:** Строгая статическая типизация с выводом типов облегчает разработку и предотвращает ошибки -2. **Паттерн-матчинг:** Удобный и безопасный способ работы с алгебраическими типами данных -3. **Отсутствие null:** Использование `Option(a)` делает код более безопасным -4. **Interoperability:** Возможность компиляции в Erlang и JavaScript расширяет область применения - -### Проблемы и ограничения - -1. **Упрощённое удаление:** Реализованный алгоритм удаления не полностью поддерживает все инварианты Red-Black Tree -2. **Производительность:** Ленивые вычисления могут добавлять накладные расходы в некоторых случаях -3. **Сложность балансировки:** Полная реализация всех случаев балансировки требует дополнительной работы - -### Общая оценка - -Реализация Red-Black Tree с ленивыми вычислениями на языке Gleam продемонстрировала эффективность функциональных подходов к программированию. Использование неизменяемых структур данных, полиморфизма и функций высшего порядка привело к созданию гибкой, безопасной и переиспользуемой библиотеки. - -Ленивые вычисления показали свою полезность для оптимизации производительности при работе с большими деревьями, где не все поддеревья могут быть использованы. - +Использование ленивых вычислений повышает эффективность использования памяти при операциях со структурами данных высокой вложенности (в нашем случае) ценой cpu на вычисление значения. +Также использовал типы, а ещё писал своё сравнение без высокоуровневых абстракций. \ No newline at end of file diff --git a/lab2/src/lab2.gleam b/lab2/src/lab2.gleam index b8c1c51..ae984f2 100644 --- a/lab2/src/lab2.gleam +++ b/lab2/src/lab2.gleam @@ -1,19 +1,16 @@ -// Модуль красно-чёрного дерева с ленивыми вычислениями -// Реализует самобалансирующееся двоичное дерево поиска с гарантированной -// логарифмической сложностью операций вставки, удаления и поиска - import gleam/option.{type Option, None, Some} import gleam/order.{type Order} -/// Цвета узлов красно-черного дерева -/// Red - красный узел, Black - чёрный узел +// Цвета узлов красно-черного дерева +// Red - красный узел, Black - чёрный узел pub type Color { Red Black } -/// Ленивое значение для отложенных вычислений -/// Позволяет откладывать создание поддеревьев до момента их использования +// Ленивое значение для отложенных вычислений +// Позволяет откладывать создание поддеревьев до момента их использования +// Не знаю правильно это или нет на самом деле, в gleam вроде lazy нету встроенного pub type Lazy(a) { Thunk(fn() -> a) // Отложенное вычисление через функцию @@ -21,9 +18,9 @@ pub type Lazy(a) { // Уже вычисленное значение } -/// Красно-черное дерево с ленивыми вычислениями -/// Полиморфная структура данных с ключами типа k и значениями типа v -/// Поддерживает инварианты красно-чёрного дерева для гарантии балансировки +// Красно-черное дерево с ленивыми вычислениями +// Полиморфная структура данных с ключами типа k и значениями типа v +// Поддерживает инварианты красно-чёрного дерева для гарантии балансировки pub type RBTree(k, v) { Empty // Пустое дерево @@ -42,8 +39,8 @@ pub type RBTree(k, v) { ) } -/// Форсирует вычисление ленивого значения -/// Если значение уже вычислено, возвращает его, иначе выполняет функцию +// Форсирует вычисление ленивого значения +// Если значение уже вычислено, возвращает его, иначе выполняет функцию fn force(lazy: Lazy(a)) -> a { case lazy { Value(val) -> val @@ -51,20 +48,20 @@ fn force(lazy: Lazy(a)) -> a { } } -/// Создаёт ленивое значение из функции -/// Отложенное вычисление будет выполнено при первом обращении +// Создаёт ленивое значение из функции +// Отложенное вычисление будет выполнено при первом обращении fn delay(f: fn() -> a) -> Lazy(a) { Thunk(f) } -/// Создаёт пустое дерево - нейтральный элемент моноида -/// Время выполнения: O(1) +// Создаёт пустое дерево - нейтральный элемент моноида +// Время выполнения: O(1) pub fn empty() -> RBTree(k, v) { Empty } -/// Проверяет, является ли дерево пустым -/// Время выполнения: O(1) +// Проверяет, является ли дерево пустым +// Время выполнения: O(1) pub fn is_empty(tree: RBTree(k, v)) -> Bool { case tree { Empty -> True @@ -72,7 +69,7 @@ pub fn is_empty(tree: RBTree(k, v)) -> Bool { } } -/// Создаёт узел с чёрным цветом +// Создаёт узел с чёрным цветом fn make_black(tree: RBTree(k, v)) -> RBTree(k, v) { case tree { Node(_, key, value, left, right) -> Node(Black, key, value, left, right) @@ -80,7 +77,7 @@ fn make_black(tree: RBTree(k, v)) -> RBTree(k, v) { } } -/// Балансировка красно-черного дерева +// Балансировка RB tree fn balance( color: Color, key: k, @@ -147,9 +144,9 @@ fn balance( } } -/// Вставляет элемент в дерево с сохранением инвариантов красно-чёрного дерева -/// Время выполнения: O(log n) -/// compare - функция сравнения ключей, должна возвращать order.Lt, order.Eq или order.Gt +// Вставляет элемент в дерево с сохранением инвариантов красно-чёрного дерева +// Время выполнения: O(log n) +// compare - функция сравнения ключей, должна возвращать order.Lt, order.Eq или order.Gt pub fn insert( tree: RBTree(k, v), key: k, @@ -158,7 +155,7 @@ pub fn insert( ) -> RBTree(k, v) { let result = insert_helper(tree, key, value, compare) make_black(result) - // Гарантируем, что корень чёрный + // гарантия что корень чёрный } fn insert_helper( @@ -192,7 +189,7 @@ fn insert_helper( } } -/// Поиск элемента в дереве +// Поиск элемента в дереве pub fn lookup( tree: RBTree(k, v), key: k, @@ -209,7 +206,7 @@ pub fn lookup( } } -/// Находит минимальный элемент в дереве +// Находит минимальный элемент в дереве fn find_min(tree: RBTree(k, v)) -> Option(#(k, v)) { case tree { Empty -> None @@ -221,7 +218,7 @@ fn find_min(tree: RBTree(k, v)) -> Option(#(k, v)) { } } -/// Удаляет минимальный элемент из дерева +// Удаляет минимальный элемент из дерева fn delete_min(tree: RBTree(k, v)) -> RBTree(k, v) { case tree { Empty -> Empty @@ -287,7 +284,7 @@ pub fn delete( } } -/// Фильтрация элементов дерева +// Фильтрация элементов дерева pub fn filter( tree: RBTree(k, v), predicate: fn(k, v) -> Bool, @@ -301,7 +298,7 @@ pub fn filter( }) } -/// Отображение значений в дереве +// Отображение значений в дереве pub fn map(tree: RBTree(k, v), f: fn(v) -> w) -> RBTree(k, w) { case tree { Empty -> Empty @@ -316,7 +313,7 @@ pub fn map(tree: RBTree(k, v), f: fn(v) -> w) -> RBTree(k, w) { } } -/// Левая свёртка дерева +// Левая свёртка дерева pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a { case tree { Empty -> acc @@ -328,7 +325,7 @@ pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a { } } -/// Правая свёртка дерева +// Правая свёртка дерева pub fn fold_right(tree: RBTree(k, v), acc: a, f: fn(k, v, a) -> a) -> a { case tree { Empty -> acc @@ -340,7 +337,7 @@ pub fn fold_right(tree: RBTree(k, v), acc: a, f: fn(k, v, a) -> a) -> a { } } -/// Объединение двух деревьев (операция моноида) +// Объединение двух деревьев (операция моноида) pub fn concat( tree1: RBTree(k, v), tree2: RBTree(k, v), @@ -351,22 +348,22 @@ pub fn concat( }) } -/// Нейтральный элемент моноида (пустое дерево) +// Нейтральный элемент моноида (пустое дерево) pub fn mempty() -> RBTree(k, v) { empty() } -/// Размер дерева +// Размер дерева pub fn size(tree: RBTree(k, v)) -> Int { fold_left(tree, 0, fn(acc, _, _) { acc + 1 }) } -/// Преобразование дерева в список пар +// Преобразование дерева в список пар pub fn to_list(tree: RBTree(k, v)) -> List(#(k, v)) { fold_right(tree, [], fn(key, value, acc) { [#(key, value), ..acc] }) } -/// Создание дерева из списка пар +// Создание дерева из списка пар pub fn from_list( list: List(#(k, v)), compare: fn(k, k) -> Order, @@ -378,7 +375,7 @@ pub fn from_list( }) } -/// Вспомогательная функция для свёртки списка +// Вспомогательная функция для свёртки списка fn list_fold_left(list: List(a), acc: b, f: fn(b, a) -> b) -> b { case list { [] -> acc @@ -386,9 +383,9 @@ fn list_fold_left(list: List(a), acc: b, f: fn(b, a) -> b) -> b { } } -/// Эффективная проверка равенства двух деревьев -/// Сравнивает деревья структурно без преобразования в списки -/// Время выполнения: O(min(n, m)) где n, m - размеры деревьев +// Эффективная проверка равенства двух деревьев +// Сравнивает деревья структурно без преобразования в списки +// Время выполнения: O(min(n, m)) где n, m - размеры деревьев pub fn equal( tree1: RBTree(k, v), tree2: RBTree(k, v), @@ -398,8 +395,8 @@ pub fn equal( equal_helper(tree1, tree2, key_compare, value_compare) } -/// Вспомогательная функция для структурного сравнения деревьев -/// Использует короткое замыкание при первом несовпадении +// Вспомогательная функция для структурного сравнения деревьев +// Использует замыкание при первом несовпадении fn equal_helper( tree1: RBTree(k, v), tree2: RBTree(k, v), @@ -409,29 +406,27 @@ fn equal_helper( case tree1, tree2 { // Оба дерева пустые - равны Empty, Empty -> True - - // Одно пустое, другое нет - не равны + Empty, _ -> False _, Empty -> False - - // Оба узла - сравниваем структурно + Node(color1, key1, value1, left1, right1), - Node(color2, key2, value2, left2, right2) -> { - // Быстрая проверка: цвета должны совпадать - color1 == color2 && + Node(color2, key2, value2, left2, right2) + -> { + color1 == color2 // Ключи должны быть равны - key_compare(key1, key2) && + && key_compare(key1, key2) // Значения должны быть равны - value_compare(value1, value2) && + && value_compare(value1, value2) // Рекурсивно проверяем левые поддеревья (ленивые) - equal_helper(force(left1), force(left2), key_compare, value_compare) && + && equal_helper(force(left1), force(left2), key_compare, value_compare) // Рекурсивно проверяем правые поддеревья (ленивые) - equal_helper(force(right1), force(right2), key_compare, value_compare) + && equal_helper(force(right1), force(right2), key_compare, value_compare) } } } -/// Проверка, содержится ли ключ в дереве +// Проверка, содержится ли ключ в дереве pub fn contains(tree: RBTree(k, v), key: k, compare: fn(k, k) -> Order) -> Bool { case lookup(tree, key, compare) { Some(_) -> True @@ -439,19 +434,19 @@ pub fn contains(tree: RBTree(k, v), key: k, compare: fn(k, k) -> Order) -> Bool } } -/// Получение всех ключей дерева +// Получение всех ключей дерева pub fn keys(tree: RBTree(k, v)) -> List(k) { fold_right(tree, [], fn(key, _, acc) { [key, ..acc] }) } -/// Получение всех значений дерева +// Получение всех значений дерева pub fn values(tree: RBTree(k, v)) -> List(v) { fold_right(tree, [], fn(_, value, acc) { [value, ..acc] }) } -/// Семантическое равенство деревьев - сравнивает содержимое независимо от структуры -/// Два дерева семантически равны, если содержат одинаковые пары ключ-значение -/// Время выполнения: O(n log n) из-за необходимости итерации +// Семантическое равенство деревьев - сравнивает содержимое независимо от структуры +// Два дерева семантически равны, если содержат одинаковые пары ключ-значение +// Время выполнения: O(n log n) из-за необходимости итерации pub fn semantic_equal( tree1: RBTree(k, v), tree2: RBTree(k, v), @@ -464,8 +459,8 @@ pub fn semantic_equal( True -> { // Проверяем, что все элементы из tree1 есть в tree2 с теми же значениями fold_left(tree1, True, fn(acc, key, value) { - acc && - case lookup(tree2, key, key_order) { + acc + && case lookup(tree2, key, key_order) { Some(other_value) -> value_equal(value, other_value) None -> False } @@ -474,8 +469,8 @@ pub fn semantic_equal( } } -/// Сравнение с игнорированием цветов узлов (только структура и данные) -/// Полезно когда важна только логическая структура дерева поиска +// Сравнение с игнорированием цветов узлов (только структура и данные) +// Полезно когда важна только логическая структура дерева поиска pub fn structure_equal( tree1: RBTree(k, v), tree2: RBTree(k, v), @@ -486,13 +481,17 @@ pub fn structure_equal( Empty, Empty -> True Empty, _ -> False _, Empty -> False - Node(_, key1, value1, left1, right1), - Node(_, key2, value2, left2, right2) -> { + Node(_, key1, value1, left1, right1), Node(_, key2, value2, left2, right2) -> { // Игнорируем цвет, сравниваем только ключи, значения и структуру - key_compare(key1, key2) && - value_compare(value1, value2) && - structure_equal(force(left1), force(left2), key_compare, value_compare) && - structure_equal(force(right1), force(right2), key_compare, value_compare) + key_compare(key1, key2) + && value_compare(value1, value2) + && structure_equal(force(left1), force(left2), key_compare, value_compare) + && structure_equal( + force(right1), + force(right2), + key_compare, + value_compare, + ) } } } diff --git a/lab2/test/lab2_test.gleam b/lab2/test/lab2_test.gleam index 8e16dc6..965fef4 100644 --- a/lab2/test/lab2_test.gleam +++ b/lab2/test/lab2_test.gleam @@ -24,16 +24,14 @@ fn string_equal(a: String, b: String) -> Bool { a == b } -// Unit тесты - -/// Тест создания пустого дерева +// Тест создания пустого дерева pub fn empty_tree_test() { let tree = lab2.empty() lab2.is_empty(tree) |> should.be_true lab2.size(tree) |> should.equal(0) } -/// Тест вставки одного элемента +// Тест вставки одного элемента pub fn single_insert_test() { let tree = lab2.empty() @@ -45,7 +43,7 @@ pub fn single_insert_test() { lab2.lookup(tree, 10, int_compare) |> should.equal(option.None) } -/// Тест множественных вставок +// Тест множественных вставок pub fn multiple_insert_test() { let tree = lab2.empty() @@ -63,7 +61,7 @@ pub fn multiple_insert_test() { lab2.lookup(tree, 9, int_compare) |> should.equal(option.Some("nine")) } -/// Тест удаления элементов +// Тест удаления элементов pub fn delete_test() { let tree = lab2.empty() @@ -78,7 +76,7 @@ pub fn delete_test() { lab2.lookup(tree, 7, int_compare) |> should.equal(option.Some("seven")) } -/// Тест фильтрации +// Тест фильтрации pub fn filter_test() { let tree = lab2.empty() @@ -96,7 +94,7 @@ pub fn filter_test() { lab2.contains(filtered, 1, int_compare) |> should.be_false } -/// Тест отображения +// Тест отображения pub fn map_test() { let tree = lab2.empty() @@ -111,7 +109,7 @@ pub fn map_test() { lab2.lookup(mapped, 3, int_compare) |> should.equal(option.Some(60)) } -/// Тест левой свёртки +// Тест левой свёртки pub fn fold_left_test() { let tree = lab2.empty() @@ -123,7 +121,7 @@ pub fn fold_left_test() { sum |> should.equal(60) } -/// Тест правой свёртки +// Тест правой свёртки pub fn fold_right_test() { let tree = lab2.empty() @@ -136,7 +134,7 @@ pub fn fold_right_test() { result |> string.length |> should.equal(3) } -/// Тест преобразования в список и обратно +// Тест преобразования в список и обратно pub fn to_from_list_test() { let original_list = [#(1, "one"), #(2, "two"), #(3, "three")] let tree = lab2.from_list(original_list, int_compare) @@ -151,7 +149,7 @@ pub fn to_from_list_test() { // Тесты свойств моноида -/// Тест нейтрального элемента (левая единица) +// Тест нейтрального элемента (левая единица) pub fn monoid_left_identity_test() { let tree = lab2.empty() @@ -163,7 +161,7 @@ pub fn monoid_left_identity_test() { lab2.equal(tree, result, int_equal, string_equal) |> should.be_true } -/// Тест нейтрального элемента (правая единица) +// Тест нейтрального элемента (правая единица) pub fn monoid_right_identity_test() { let tree = lab2.empty() @@ -175,7 +173,7 @@ pub fn monoid_right_identity_test() { lab2.equal(tree, result, int_equal, string_equal) |> should.be_true } -/// Тест ассоциативности +// Тест ассоциативности pub fn monoid_associativity_test() { let tree1 = lab2.empty() |> lab2.insert(1, "one", int_compare) let tree2 = lab2.empty() |> lab2.insert(2, "two", int_compare) @@ -197,7 +195,7 @@ pub fn monoid_associativity_test() { lab2.contains(right_assoc, 3, int_compare) |> should.be_true } -/// Тест инвариантов красно-чёрного дерева (упрощённая проверка) +// Тест инвариантов красно-чёрного дерева (упрощённая проверка) pub fn red_black_invariant_test() { // Создаём большое дерево и проверяем, что поиск работает корректно let tree = @@ -228,7 +226,7 @@ pub fn red_black_invariant_test() { all_found |> should.be_true } -/// Тест обновления существующего ключа +// Тест обновления существующего ключа pub fn update_existing_key_test() { let tree = lab2.empty() @@ -239,7 +237,7 @@ pub fn update_existing_key_test() { lab2.lookup(tree, 1, int_compare) |> should.equal(option.Some("new")) } -/// Тест свойства: insert -> lookup должен возвращать вставленное значение +// Тест свойства: insert -> lookup должен возвращать вставленное значение pub fn insert_lookup_property_test() { let test_cases = [ #(1, "one"), @@ -255,7 +253,7 @@ pub fn insert_lookup_property_test() { }) } -/// Тест свойства: size увеличивается при вставке новых элементов +// Тест свойства: size увеличивается при вставке новых элементов pub fn insert_size_property_test() { let tree = lab2.empty() lab2.size(tree) |> should.equal(0) @@ -274,7 +272,7 @@ pub fn insert_size_property_test() { lab2.size(tree4) |> should.equal(3) } -/// Тест свойства: filter сохраняет порядок и структуру +// Тест свойства: filter сохраняет порядок и структуру pub fn filter_property_test() { let original = lab2.from_list( @@ -298,7 +296,7 @@ pub fn filter_property_test() { lab2.contains(partial_filter, 5, int_compare) |> should.be_true } -/// Тест свойства: map сохраняет структуру дерева +// Тест свойства: map сохраняет структуру дерева pub fn map_property_test() { let original = lab2.from_list([#(1, 10), #(2, 20), #(3, 30)], int_compare) @@ -314,7 +312,7 @@ pub fn map_property_test() { lab2.lookup(doubled, 3, int_compare) |> should.equal(option.Some(60)) } -/// Тест свойства: fold_left и fold_right дают корректные результаты +// Тест свойства: fold_left и fold_right дают корректные результаты pub fn fold_property_test() { let tree = lab2.from_list([#(1, 1), #(2, 2), #(3, 3), #(4, 4), #(5, 5)], int_compare) @@ -334,7 +332,7 @@ pub fn fold_property_test() { count_right |> should.equal(5) } -/// Тест свойства: to_list -> from_list должно давать эквивалентное дерево +// Тест свойства: to_list -> from_list должно давать эквивалентное дерево pub fn list_roundtrip_property_test() { let original_list = [#(3, "c"), #(1, "a"), #(4, "d"), #(2, "b")] let tree = lab2.from_list(original_list, int_compare) @@ -352,7 +350,7 @@ pub fn list_roundtrip_property_test() { }) } -/// Тест инвариантов моноида (коммутативность для деревьев с разными ключами) +// Тест инвариантов моноида (коммутативность для деревьев с разными ключами) pub fn monoid_commutativity_test() { let tree1 = lab2.from_list([#(1, "a"), #(2, "b")], int_compare) let tree2 = lab2.from_list([#(3, "c"), #(4, "d")], int_compare) @@ -371,3 +369,41 @@ pub fn monoid_commutativity_test() { lab2.contains(concat2, key, int_compare) |> should.be_true }) } + +// Тест семантического равенства деревьев +// Проверяет, что деревья с одинаковым содержимым, но разной структурой считаются равными +pub fn semantic_equal_test() { + // Создаём два дерева с одинаковыми данными, но разным порядком вставки + let tree1 = + lab2.empty() + |> lab2.insert(2, "two", int_compare) + |> lab2.insert(1, "one", int_compare) + |> lab2.insert(3, "three", int_compare) + + let tree2 = + lab2.empty() + |> lab2.insert(1, "one", int_compare) + |> lab2.insert(3, "three", int_compare) + |> lab2.insert(2, "two", int_compare) + + // Семантически они должны быть равны + lab2.semantic_equal(tree1, tree2, int_compare, string_equal) |> should.be_true + // Структурно они могут быть разными (зависит от балансировки) + // lab2.equal(tree1, tree2, int_equal, string_equal) может быть false +} + +// Тест структурного равенства без учёта цветов узлов +pub fn structure_equal_test() { + let tree1 = + lab2.empty() + |> lab2.insert(2, "two", int_compare) + |> lab2.insert(1, "one", int_compare) + + let tree2 = + lab2.empty() + |> lab2.insert(2, "two", int_compare) + |> lab2.insert(1, "one", int_compare) + + // Структурно должны быть равны (игнорируя цвета) + lab2.structure_equal(tree1, tree2, int_equal, string_equal) |> should.be_true +}