lab 2 complete v1

This commit is contained in:
2025-12-06 13:44:45 +03:00
parent 9023a42c46
commit 080bb8d422
4 changed files with 225 additions and 325 deletions

View File

@@ -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 <https://hexdocs.pm/lab2>.
## 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 раза длиннее самого короткого пути, что обеспечивает логарифмическую сложность операций.

View File

@@ -4,7 +4,7 @@
**Факультет программной инженерии и компьютерной техники** **Факультет программной инженерии и компьютерной техники**
**Студент:** Владимиров Владислав Александрович **Студент:** Владимиров Владислав Александрович
**Группа:** P3222 **Группа:** P3322
**Тема:** Реализация Red-Black Tree с ленивыми вычислениями **Тема:** Реализация Red-Black Tree с ленивыми вычислениями
**Лабораторная работа №2** **Лабораторная работа №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** - красно-чёрное дерево на ленивых вычислениях. **Red-Black Tree Lazy** - красно-чёрное дерево на ленивых вычислениях.
@@ -55,19 +76,19 @@
### Основные типы данных ### Основные типы данных
```gleam ```gleam
/// Цвета узлов красно-черного дерева // Цвета узлов красно-черного дерева
pub type Color { pub type Color {
Red Red
Black Black
} }
/// Ленивое значение для отложенных вычислений // Ленивое значение для отложенных вычислений
pub type Lazy(a) { pub type Lazy(a) {
Thunk(fn() -> a) // Отложенное вычисление Thunk(fn() -> a) // Отложенное вычисление
Value(a) // Уже вычисленное значение Value(a) // Уже вычисленное значение
} }
/// Красно-черное дерево с ленивыми вычислениями // Красно-черное дерево с ленивыми вычислениями
pub type RBTree(k, v) { pub type RBTree(k, v) {
Empty // Пустое дерево Empty // Пустое дерево
Node( Node(
@@ -83,7 +104,7 @@ pub type RBTree(k, v) {
### Управление ленивыми вычислениями ### Управление ленивыми вычислениями
```gleam ```gleam
/// Форсирует вычисление ленивого значения // Форсирует вычисление ленивого значения
fn force(lazy: Lazy(a)) -> a { fn force(lazy: Lazy(a)) -> a {
case lazy { case lazy {
Value(val) -> val Value(val) -> val
@@ -91,7 +112,7 @@ fn force(lazy: Lazy(a)) -> a {
} }
} }
/// Создаёт ленивое значение из функции // Создаёт ленивое значение из функции
fn delay(f: fn() -> a) -> Lazy(a) { fn delay(f: fn() -> a) -> Lazy(a) {
Thunk(f) 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 ```gleam
pub fn insert(tree: RBTree(k, v), key: k, value: v, 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 ```gleam
/// Нейтральный элемент // Нейтральный элемент
pub fn mempty() -> RBTree(k, v) { pub fn mempty() -> RBTree(k, v) {
empty() empty()
} }
/// Операция объединения // Операция объединения
pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v), pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v),
compare: fn(k, k) -> Order) -> RBTree(k, v) { compare: fn(k, k) -> Order) -> RBTree(k, v) {
fold_left(tree2, tree1, fn(acc, key, value) { fold_left(tree2, tree1, fn(acc, key, value) {
@@ -247,14 +302,19 @@ pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v),
## Тесты и метрики ## Тесты и метрики
### Unit тесты ### Unit тесты
- `empty_tree_test()` - создание пустого дерева - `empty_tree_test()` - создание пустого дерева и проверка `is_empty()`, `size()`
- `single_insert_test()` - вставка одного элемента - `single_insert_test()` - вставка одного элемента, проверка `lookup()` и размера
- `multiple_insert_test()` - множественные вставки - `multiple_insert_test()` - множественные вставки с проверкой всех элементов
- `delete_test()` - удаление элементов - `delete_test()` - удаление элементов с проверкой корректности
- `filter_test()` - тестирование фильтрации - `update_existing_key_test()` - обновление существующих ключей
- `map_test()` - тестирование отображения - `filter_test()` - фильтрация четных чисел, проверка размера и содержимого
- `fold_left_test()`, `fold_right_test()` - тестирование свёрток - `map_test()` - отображение значений (умножение на 2), проверка корректности
- `to_from_list_test()` - конвертация в список и обратно - `fold_left_test()` - левая свёртка для суммирования значений
- `fold_right_test()` - правая свёртка для конкатенации строк
- `to_from_list_test()` - конвертация дерево→список→дерево с проверкой эквивалентности
- `red_black_invariant_test()` - работа с большим деревом (10 элементов), проверка балансировки
- `semantic_equal_test()` - семантическое равенство (независимо от структуры)
- `structure_equal_test()` - структурное равенство без учёта цветов узлов
### Property-based тесты ### Property-based тесты
@@ -279,92 +339,44 @@ pub fn concat(tree1: RBTree(k, v), tree2: RBTree(k, v),
### Тесты моноида ### Тесты моноида
```gleam ```gleam
/// Левая единица: mempty ∘ a = a // Левая единица: mempty ∘ a = a
pub fn monoid_left_identity_test() pub fn monoid_left_identity_test()
/// Правая единица: a ∘ mempty = a // Правая единица: a ∘ mempty = a
pub fn monoid_right_identity_test() 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_associativity_test()
/// Коммутативность (для непересекающихся множеств ключей) // Коммутативность (для непересекающихся множеств ключей)
pub fn monoid_commutativity_test() pub fn monoid_commutativity_test()
``` ```
### Отчёт тестирования ### Отчёт тестирования
``` ```
Running lab2_test.main > gleam test
..................... Compiling lab2
21 passed, no failures
Compiled in 0.46s Compiled in 0.46s
Running lab2_test.main
.......................
23 passed, no failures
``` ```
### Метрики производительности ### Метрики производительности
| Операция | Теоретическая сложность | Реализованная сложность | | Операция | Сложность|
|----------|------------------------|------------------------| |----------|------------------------|
| lookup | O(log n) | O(log n) | | lookup | O(log n) |
| insert | O(log n) | O(log n) | | insert | O(log n) |
| delete | O(log n) | O(log n) | | delete | O(log n) |
| size | O(n) | O(n) | | size | O(n) |
| map | O(n) | O(n) | | map | O(n) |
| filter | O(n log n) | O(n log n) | | filter | O(n log n) |
| fold | O(n) | O(n) | | fold | O(n) |
--- ---
## Выводы ## Выводы
Использование ленивых вычислений повышает эффективность использования памяти при операциях со структурами данных высокой вложенности (в нашем случае) ценой cpu на вычисление значения.
### Использованные приёмы программирования Также использовал типы, а ещё писал своё сравнение без высокоуровневых абстракций.
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 продемонстрировала эффективность функциональных подходов к программированию. Использование неизменяемых структур данных, полиморфизма и функций высшего порядка привело к созданию гибкой, безопасной и переиспользуемой библиотеки.
Ленивые вычисления показали свою полезность для оптимизации производительности при работе с большими деревьями, где не все поддеревья могут быть использованы.

View File

@@ -1,19 +1,16 @@
// Модуль красно-чёрного дерева с ленивыми вычислениями
// Реализует самобалансирующееся двоичное дерево поиска с гарантированной
// логарифмической сложностью операций вставки, удаления и поиска
import gleam/option.{type Option, None, Some} import gleam/option.{type Option, None, Some}
import gleam/order.{type Order} import gleam/order.{type Order}
/// Цвета узлов красно-черного дерева // Цвета узлов красно-черного дерева
/// Red - красный узел, Black - чёрный узел // Red - красный узел, Black - чёрный узел
pub type Color { pub type Color {
Red Red
Black Black
} }
/// Ленивое значение для отложенных вычислений // Ленивое значение для отложенных вычислений
/// Позволяет откладывать создание поддеревьев до момента их использования // Позволяет откладывать создание поддеревьев до момента их использования
// Не знаю правильно это или нет на самом деле, в gleam вроде lazy нету встроенного
pub type Lazy(a) { pub type Lazy(a) {
Thunk(fn() -> a) Thunk(fn() -> a)
// Отложенное вычисление через функцию // Отложенное вычисление через функцию
@@ -21,9 +18,9 @@ pub type Lazy(a) {
// Уже вычисленное значение // Уже вычисленное значение
} }
/// Красно-черное дерево с ленивыми вычислениями // Красно-черное дерево с ленивыми вычислениями
/// Полиморфная структура данных с ключами типа k и значениями типа v // Полиморфная структура данных с ключами типа k и значениями типа v
/// Поддерживает инварианты красно-чёрного дерева для гарантии балансировки // Поддерживает инварианты красно-чёрного дерева для гарантии балансировки
pub type RBTree(k, v) { pub type RBTree(k, v) {
Empty Empty
// Пустое дерево // Пустое дерево
@@ -42,8 +39,8 @@ pub type RBTree(k, v) {
) )
} }
/// Форсирует вычисление ленивого значения // Форсирует вычисление ленивого значения
/// Если значение уже вычислено, возвращает его, иначе выполняет функцию // Если значение уже вычислено, возвращает его, иначе выполняет функцию
fn force(lazy: Lazy(a)) -> a { fn force(lazy: Lazy(a)) -> a {
case lazy { case lazy {
Value(val) -> val Value(val) -> val
@@ -51,20 +48,20 @@ fn force(lazy: Lazy(a)) -> a {
} }
} }
/// Создаёт ленивое значение из функции // Создаёт ленивое значение из функции
/// Отложенное вычисление будет выполнено при первом обращении // Отложенное вычисление будет выполнено при первом обращении
fn delay(f: fn() -> a) -> Lazy(a) { fn delay(f: fn() -> a) -> Lazy(a) {
Thunk(f) Thunk(f)
} }
/// Создаёт пустое дерево - нейтральный элемент моноида // Создаёт пустое дерево - нейтральный элемент моноида
/// Время выполнения: O(1) // Время выполнения: O(1)
pub fn empty() -> RBTree(k, v) { pub fn empty() -> RBTree(k, v) {
Empty Empty
} }
/// Проверяет, является ли дерево пустым // Проверяет, является ли дерево пустым
/// Время выполнения: O(1) // Время выполнения: O(1)
pub fn is_empty(tree: RBTree(k, v)) -> Bool { pub fn is_empty(tree: RBTree(k, v)) -> Bool {
case tree { case tree {
Empty -> True 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) { fn make_black(tree: RBTree(k, v)) -> RBTree(k, v) {
case tree { case tree {
Node(_, key, value, left, right) -> Node(Black, key, value, left, right) 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( fn balance(
color: Color, color: Color,
key: k, key: k,
@@ -147,9 +144,9 @@ fn balance(
} }
} }
/// Вставляет элемент в дерево с сохранением инвариантов красно-чёрного дерева // Вставляет элемент в дерево с сохранением инвариантов красно-чёрного дерева
/// Время выполнения: O(log n) // Время выполнения: O(log n)
/// compare - функция сравнения ключей, должна возвращать order.Lt, order.Eq или order.Gt // compare - функция сравнения ключей, должна возвращать order.Lt, order.Eq или order.Gt
pub fn insert( pub fn insert(
tree: RBTree(k, v), tree: RBTree(k, v),
key: k, key: k,
@@ -158,7 +155,7 @@ pub fn insert(
) -> RBTree(k, v) { ) -> RBTree(k, v) {
let result = insert_helper(tree, key, value, compare) let result = insert_helper(tree, key, value, compare)
make_black(result) make_black(result)
// Гарантируем, что корень чёрный // гарантия что корень чёрный
} }
fn insert_helper( fn insert_helper(
@@ -192,7 +189,7 @@ fn insert_helper(
} }
} }
/// Поиск элемента в дереве // Поиск элемента в дереве
pub fn lookup( pub fn lookup(
tree: RBTree(k, v), tree: RBTree(k, v),
key: k, key: k,
@@ -209,7 +206,7 @@ pub fn lookup(
} }
} }
/// Находит минимальный элемент в дереве // Находит минимальный элемент в дереве
fn find_min(tree: RBTree(k, v)) -> Option(#(k, v)) { fn find_min(tree: RBTree(k, v)) -> Option(#(k, v)) {
case tree { case tree {
Empty -> None 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) { fn delete_min(tree: RBTree(k, v)) -> RBTree(k, v) {
case tree { case tree {
Empty -> Empty Empty -> Empty
@@ -287,7 +284,7 @@ pub fn delete(
} }
} }
/// Фильтрация элементов дерева // Фильтрация элементов дерева
pub fn filter( pub fn filter(
tree: RBTree(k, v), tree: RBTree(k, v),
predicate: fn(k, v) -> Bool, 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) { pub fn map(tree: RBTree(k, v), f: fn(v) -> w) -> RBTree(k, w) {
case tree { case tree {
Empty -> Empty 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 { pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a {
case tree { case tree {
Empty -> acc 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 { pub fn fold_right(tree: RBTree(k, v), acc: a, f: fn(k, v, a) -> a) -> a {
case tree { case tree {
Empty -> acc 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( pub fn concat(
tree1: RBTree(k, v), tree1: RBTree(k, v),
tree2: RBTree(k, v), tree2: RBTree(k, v),
@@ -351,22 +348,22 @@ pub fn concat(
}) })
} }
/// Нейтральный элемент моноида (пустое дерево) // Нейтральный элемент моноида (пустое дерево)
pub fn mempty() -> RBTree(k, v) { pub fn mempty() -> RBTree(k, v) {
empty() empty()
} }
/// Размер дерева // Размер дерева
pub fn size(tree: RBTree(k, v)) -> Int { pub fn size(tree: RBTree(k, v)) -> Int {
fold_left(tree, 0, fn(acc, _, _) { acc + 1 }) fold_left(tree, 0, fn(acc, _, _) { acc + 1 })
} }
/// Преобразование дерева в список пар // Преобразование дерева в список пар
pub fn to_list(tree: RBTree(k, v)) -> List(#(k, v)) { pub fn to_list(tree: RBTree(k, v)) -> List(#(k, v)) {
fold_right(tree, [], fn(key, value, acc) { [#(key, value), ..acc] }) fold_right(tree, [], fn(key, value, acc) { [#(key, value), ..acc] })
} }
/// Создание дерева из списка пар // Создание дерева из списка пар
pub fn from_list( pub fn from_list(
list: List(#(k, v)), list: List(#(k, v)),
compare: fn(k, k) -> Order, 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 { fn list_fold_left(list: List(a), acc: b, f: fn(b, a) -> b) -> b {
case list { case list {
[] -> acc [] -> 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( pub fn equal(
tree1: RBTree(k, v), tree1: RBTree(k, v),
tree2: RBTree(k, v), tree2: RBTree(k, v),
@@ -398,8 +395,8 @@ pub fn equal(
equal_helper(tree1, tree2, key_compare, value_compare) equal_helper(tree1, tree2, key_compare, value_compare)
} }
/// Вспомогательная функция для структурного сравнения деревьев // Вспомогательная функция для структурного сравнения деревьев
/// Использует короткое замыкание при первом несовпадении // Использует замыкание при первом несовпадении
fn equal_helper( fn equal_helper(
tree1: RBTree(k, v), tree1: RBTree(k, v),
tree2: RBTree(k, v), tree2: RBTree(k, v),
@@ -409,29 +406,27 @@ fn equal_helper(
case tree1, tree2 { case tree1, tree2 {
// Оба дерева пустые - равны // Оба дерева пустые - равны
Empty, Empty -> True Empty, Empty -> True
// Одно пустое, другое нет - не равны
Empty, _ -> False Empty, _ -> False
_, Empty -> False _, Empty -> False
// Оба узла - сравниваем структурно
Node(color1, key1, value1, left1, right1), Node(color1, key1, value1, left1, right1),
Node(color2, key2, value2, left2, right2) -> { Node(color2, key2, value2, left2, right2)
// Быстрая проверка: цвета должны совпадать -> {
color1 == color2 && 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 { pub fn contains(tree: RBTree(k, v), key: k, compare: fn(k, k) -> Order) -> Bool {
case lookup(tree, key, compare) { case lookup(tree, key, compare) {
Some(_) -> True 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) { pub fn keys(tree: RBTree(k, v)) -> List(k) {
fold_right(tree, [], fn(key, _, acc) { [key, ..acc] }) fold_right(tree, [], fn(key, _, acc) { [key, ..acc] })
} }
/// Получение всех значений дерева // Получение всех значений дерева
pub fn values(tree: RBTree(k, v)) -> List(v) { pub fn values(tree: RBTree(k, v)) -> List(v) {
fold_right(tree, [], fn(_, value, acc) { [value, ..acc] }) fold_right(tree, [], fn(_, value, acc) { [value, ..acc] })
} }
/// Семантическое равенство деревьев - сравнивает содержимое независимо от структуры // Семантическое равенство деревьев - сравнивает содержимое независимо от структуры
/// Два дерева семантически равны, если содержат одинаковые пары ключ-значение // Два дерева семантически равны, если содержат одинаковые пары ключ-значение
/// Время выполнения: O(n log n) из-за необходимости итерации // Время выполнения: O(n log n) из-за необходимости итерации
pub fn semantic_equal( pub fn semantic_equal(
tree1: RBTree(k, v), tree1: RBTree(k, v),
tree2: RBTree(k, v), tree2: RBTree(k, v),
@@ -464,8 +459,8 @@ pub fn semantic_equal(
True -> { True -> {
// Проверяем, что все элементы из tree1 есть в tree2 с теми же значениями // Проверяем, что все элементы из tree1 есть в tree2 с теми же значениями
fold_left(tree1, True, fn(acc, key, value) { fold_left(tree1, True, fn(acc, key, value) {
acc && acc
case lookup(tree2, key, key_order) { && case lookup(tree2, key, key_order) {
Some(other_value) -> value_equal(value, other_value) Some(other_value) -> value_equal(value, other_value)
None -> False None -> False
} }
@@ -474,8 +469,8 @@ pub fn semantic_equal(
} }
} }
/// Сравнение с игнорированием цветов узлов (только структура и данные) // Сравнение с игнорированием цветов узлов (только структура и данные)
/// Полезно когда важна только логическая структура дерева поиска // Полезно когда важна только логическая структура дерева поиска
pub fn structure_equal( pub fn structure_equal(
tree1: RBTree(k, v), tree1: RBTree(k, v),
tree2: RBTree(k, v), tree2: RBTree(k, v),
@@ -486,13 +481,17 @@ pub fn structure_equal(
Empty, Empty -> True Empty, Empty -> True
Empty, _ -> False Empty, _ -> False
_, Empty -> False _, Empty -> False
Node(_, key1, value1, left1, right1), Node(_, key1, value1, left1, right1), Node(_, key2, value2, left2, right2) -> {
Node(_, key2, value2, left2, right2) -> {
// Игнорируем цвет, сравниваем только ключи, значения и структуру // Игнорируем цвет, сравниваем только ключи, значения и структуру
key_compare(key1, key2) && key_compare(key1, key2)
value_compare(value1, value2) && && value_compare(value1, value2)
structure_equal(force(left1), force(left2), key_compare, value_compare) && && structure_equal(force(left1), force(left2), key_compare, value_compare)
structure_equal(force(right1), force(right2), key_compare, value_compare) && structure_equal(
force(right1),
force(right2),
key_compare,
value_compare,
)
} }
} }
} }

View File

@@ -24,16 +24,14 @@ fn string_equal(a: String, b: String) -> Bool {
a == b a == b
} }
// Unit тесты // Тест создания пустого дерева
/// Тест создания пустого дерева
pub fn empty_tree_test() { pub fn empty_tree_test() {
let tree = lab2.empty() let tree = lab2.empty()
lab2.is_empty(tree) |> should.be_true lab2.is_empty(tree) |> should.be_true
lab2.size(tree) |> should.equal(0) lab2.size(tree) |> should.equal(0)
} }
/// Тест вставки одного элемента // Тест вставки одного элемента
pub fn single_insert_test() { pub fn single_insert_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -45,7 +43,7 @@ pub fn single_insert_test() {
lab2.lookup(tree, 10, int_compare) |> should.equal(option.None) lab2.lookup(tree, 10, int_compare) |> should.equal(option.None)
} }
/// Тест множественных вставок // Тест множественных вставок
pub fn multiple_insert_test() { pub fn multiple_insert_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -63,7 +61,7 @@ pub fn multiple_insert_test() {
lab2.lookup(tree, 9, int_compare) |> should.equal(option.Some("nine")) lab2.lookup(tree, 9, int_compare) |> should.equal(option.Some("nine"))
} }
/// Тест удаления элементов // Тест удаления элементов
pub fn delete_test() { pub fn delete_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -78,7 +76,7 @@ pub fn delete_test() {
lab2.lookup(tree, 7, int_compare) |> should.equal(option.Some("seven")) lab2.lookup(tree, 7, int_compare) |> should.equal(option.Some("seven"))
} }
/// Тест фильтрации // Тест фильтрации
pub fn filter_test() { pub fn filter_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -96,7 +94,7 @@ pub fn filter_test() {
lab2.contains(filtered, 1, int_compare) |> should.be_false lab2.contains(filtered, 1, int_compare) |> should.be_false
} }
/// Тест отображения // Тест отображения
pub fn map_test() { pub fn map_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -111,7 +109,7 @@ pub fn map_test() {
lab2.lookup(mapped, 3, int_compare) |> should.equal(option.Some(60)) lab2.lookup(mapped, 3, int_compare) |> should.equal(option.Some(60))
} }
/// Тест левой свёртки // Тест левой свёртки
pub fn fold_left_test() { pub fn fold_left_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -123,7 +121,7 @@ pub fn fold_left_test() {
sum |> should.equal(60) sum |> should.equal(60)
} }
/// Тест правой свёртки // Тест правой свёртки
pub fn fold_right_test() { pub fn fold_right_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -136,7 +134,7 @@ pub fn fold_right_test() {
result |> string.length |> should.equal(3) result |> string.length |> should.equal(3)
} }
/// Тест преобразования в список и обратно // Тест преобразования в список и обратно
pub fn to_from_list_test() { pub fn to_from_list_test() {
let original_list = [#(1, "one"), #(2, "two"), #(3, "three")] let original_list = [#(1, "one"), #(2, "two"), #(3, "three")]
let tree = lab2.from_list(original_list, int_compare) 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() { pub fn monoid_left_identity_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -163,7 +161,7 @@ pub fn monoid_left_identity_test() {
lab2.equal(tree, result, int_equal, string_equal) |> should.be_true lab2.equal(tree, result, int_equal, string_equal) |> should.be_true
} }
/// Тест нейтрального элемента (правая единица) // Тест нейтрального элемента (правая единица)
pub fn monoid_right_identity_test() { pub fn monoid_right_identity_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -175,7 +173,7 @@ pub fn monoid_right_identity_test() {
lab2.equal(tree, result, int_equal, string_equal) |> should.be_true lab2.equal(tree, result, int_equal, string_equal) |> should.be_true
} }
/// Тест ассоциативности // Тест ассоциативности
pub fn monoid_associativity_test() { pub fn monoid_associativity_test() {
let tree1 = lab2.empty() |> lab2.insert(1, "one", int_compare) let tree1 = lab2.empty() |> lab2.insert(1, "one", int_compare)
let tree2 = lab2.empty() |> lab2.insert(2, "two", 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 lab2.contains(right_assoc, 3, int_compare) |> should.be_true
} }
/// Тест инвариантов красно-чёрного дерева (упрощённая проверка) // Тест инвариантов красно-чёрного дерева (упрощённая проверка)
pub fn red_black_invariant_test() { pub fn red_black_invariant_test() {
// Создаём большое дерево и проверяем, что поиск работает корректно // Создаём большое дерево и проверяем, что поиск работает корректно
let tree = let tree =
@@ -228,7 +226,7 @@ pub fn red_black_invariant_test() {
all_found |> should.be_true all_found |> should.be_true
} }
/// Тест обновления существующего ключа // Тест обновления существующего ключа
pub fn update_existing_key_test() { pub fn update_existing_key_test() {
let tree = let tree =
lab2.empty() lab2.empty()
@@ -239,7 +237,7 @@ pub fn update_existing_key_test() {
lab2.lookup(tree, 1, int_compare) |> should.equal(option.Some("new")) lab2.lookup(tree, 1, int_compare) |> should.equal(option.Some("new"))
} }
/// Тест свойства: insert -> lookup должен возвращать вставленное значение // Тест свойства: insert -> lookup должен возвращать вставленное значение
pub fn insert_lookup_property_test() { pub fn insert_lookup_property_test() {
let test_cases = [ let test_cases = [
#(1, "one"), #(1, "one"),
@@ -255,7 +253,7 @@ pub fn insert_lookup_property_test() {
}) })
} }
/// Тест свойства: size увеличивается при вставке новых элементов // Тест свойства: size увеличивается при вставке новых элементов
pub fn insert_size_property_test() { pub fn insert_size_property_test() {
let tree = lab2.empty() let tree = lab2.empty()
lab2.size(tree) |> should.equal(0) lab2.size(tree) |> should.equal(0)
@@ -274,7 +272,7 @@ pub fn insert_size_property_test() {
lab2.size(tree4) |> should.equal(3) lab2.size(tree4) |> should.equal(3)
} }
/// Тест свойства: filter сохраняет порядок и структуру // Тест свойства: filter сохраняет порядок и структуру
pub fn filter_property_test() { pub fn filter_property_test() {
let original = let original =
lab2.from_list( lab2.from_list(
@@ -298,7 +296,7 @@ pub fn filter_property_test() {
lab2.contains(partial_filter, 5, int_compare) |> should.be_true lab2.contains(partial_filter, 5, int_compare) |> should.be_true
} }
/// Тест свойства: map сохраняет структуру дерева // Тест свойства: map сохраняет структуру дерева
pub fn map_property_test() { pub fn map_property_test() {
let original = lab2.from_list([#(1, 10), #(2, 20), #(3, 30)], int_compare) 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)) lab2.lookup(doubled, 3, int_compare) |> should.equal(option.Some(60))
} }
/// Тест свойства: fold_left и fold_right дают корректные результаты // Тест свойства: fold_left и fold_right дают корректные результаты
pub fn fold_property_test() { pub fn fold_property_test() {
let tree = let tree =
lab2.from_list([#(1, 1), #(2, 2), #(3, 3), #(4, 4), #(5, 5)], int_compare) 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) count_right |> should.equal(5)
} }
/// Тест свойства: to_list -> from_list должно давать эквивалентное дерево // Тест свойства: to_list -> from_list должно давать эквивалентное дерево
pub fn list_roundtrip_property_test() { pub fn list_roundtrip_property_test() {
let original_list = [#(3, "c"), #(1, "a"), #(4, "d"), #(2, "b")] let original_list = [#(3, "c"), #(1, "a"), #(4, "d"), #(2, "b")]
let tree = lab2.from_list(original_list, int_compare) let tree = lab2.from_list(original_list, int_compare)
@@ -352,7 +350,7 @@ pub fn list_roundtrip_property_test() {
}) })
} }
/// Тест инвариантов моноида (коммутативность для деревьев с разными ключами) // Тест инвариантов моноида (коммутативность для деревьев с разными ключами)
pub fn monoid_commutativity_test() { pub fn monoid_commutativity_test() {
let tree1 = lab2.from_list([#(1, "a"), #(2, "b")], int_compare) let tree1 = lab2.from_list([#(1, "a"), #(2, "b")], int_compare)
let tree2 = lab2.from_list([#(3, "c"), #(4, "d")], 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 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
}