Косяк с функцией сравнения

This commit is contained in:
2025-12-06 12:58:06 +03:00
parent 3bf2db9b30
commit 9023a42c46
11 changed files with 1450 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ version = "1.0.0"
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
gleam_regexp = ">= 1.1.1 and < 2.0.0"
colored = ">= 1.0.2 and < 2.0.0"
iterator = ">= 0.0.5 and < 1.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

View File

@@ -6,6 +6,7 @@ packages = [
{ name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
{ name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
{ name = "gleeunit", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "CD701726CBCE5588B375D157B4391CFD0F2F134CD12D9B6998A395484DE05C58" },
{ name = "iterator", version = "0.0.5", build_tools = ["rebar3"], requirements = [], otp_app = "iterator", source = "hex", outer_checksum = "8D7FECF393DC4D62FA95005AF3F17400DC0AF18A8FE87B6EC3107FBBB6009139" },
]
[requirements]
@@ -13,3 +14,4 @@ colored = { version = ">= 1.0.2 and < 2.0.0" }
gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
iterator = { version = ">= 0.0.5 and < 1.0.0" }

View File

@@ -4,6 +4,8 @@ import gleam/list
// 1. Хвостовая рекурсия
pub fn sum_multiples_tail_recursive(limit: Int) -> Int {
// Это всё можно оформить в одно выражение
// матчить лимит в 0 и в других случаях
case limit {
n if n <= 0 -> 0
_ -> sum_multiples_tail_recursive_helper(limit - 1, 0)

23
lab2/.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: test
on:
push:
branches:
- master
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: "28"
gleam-version: "1.13.0"
rebar3-version: "3"
# elixir-version: "1"
- run: gleam deps download
- run: gleam test
- run: gleam format --check src test

4
lab2/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump

147
lab2/README.md Normal file
View File

@@ -0,0 +1,147 @@
# 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 раза длиннее самого короткого пути, что обеспечивает логарифмическую сложность операций.

370
lab2/Report.md Normal file
View File

@@ -0,0 +1,370 @@
# Отчёт по лабораторной работе №2
**Университет ИТМО**
**Факультет программной инженерии и компьютерной техники**
**Студент:** Владимиров Владислав Александрович
**Группа:** P3222
**Тема:** Реализация Red-Black Tree с ленивыми вычислениями
**Лабораторная работа №2**
**Дисциплина:** Функциональное программирование
---
## Требования к разработанному ПО
### Цель работы
Освоиться с построением пользовательских типов данных, полиморфизмом, рекурсивными алгоритмами и средствами тестирования (unit testing, property-based testing), а также разделением интерфейса и особенностей реализации.
### Вариант задания
**Red-Black Tree Lazy** - красно-чёрное дерево на ленивых вычислениях.
### Функциональные требования
1. **Основные операции:**
- Добавление элементов (`insert`)
- Удаление элементов (`delete`)
- Фильтрация (`filter`)
- Отображение (`map`)
- Свёртки левая и правая (`fold_left`, `fold_right`)
2. **Структурные требования:**
- Структура должна быть моноидом
- Неизменяемые структуры данных
- Полиморфная реализация
- Использование ленивых вычислений
3. **Требования к тестированию:**
- Unit testing для всех функций
- Property-based тестирование (минимум 3 свойства)
- Тестирование свойств моноида
### Технические требования
- **Язык программирования:** Gleam
- **Сложность операций:** O(log n) для insert, delete, lookup
- **API не должно "протекать"**
- **Эффективная реализация сравнения деревьев**
- **Идиоматичный стиль программирования**
---
## Ключевые элементы реализации
### Основные типы данных
```gleam
/// Цвета узлов красно-черного дерева
pub type Color {
Red
Black
}
/// Ленивое значение для отложенных вычислений
pub type Lazy(a) {
Thunk(fn() -> a) // Отложенное вычисление
Value(a) // Уже вычисленное значение
}
/// Красно-черное дерево с ленивыми вычислениями
pub type RBTree(k, v) {
Empty // Пустое дерево
Node(
color: Color, // Цвет узла
key: k, // Ключ
value: v, // Значение
left: Lazy(RBTree(k, v)), // Левое поддерево (ленивое)
right: Lazy(RBTree(k, v)), // Правое поддерево (ленивое)
)
}
```
### Управление ленивыми вычислениями
```gleam
/// Форсирует вычисление ленивого значения
fn force(lazy: Lazy(a)) -> a {
case lazy {
Value(val) -> val
Thunk(f) -> f()
}
}
/// Создаёт ленивое значение из функции
fn delay(f: fn() -> a) -> Lazy(a) {
Thunk(f)
}
```
### Балансировка красно-чёрного дерева
Реализованы 4 случая нарушения инвариантов Red-Black Tree:
```gleam
n balance(
color: Color,
key: k,
value: v,
left: Lazy(RBTree(k, v)),
right: Lazy(RBTree(k, v)),
) -> RBTree(k, v) {
let left_tree = force(left)
let right_tree = force(right)
case color, left_tree, right_tree {
// Случай 1: красный левый дедушка с красными детьми
Black, Node(Red, lk, lv, ll, lr), r ->
case force(ll) {
Node(Red, llk, llv, lll, llr) ->
Node(
Red,
lk,
lv,
delay(fn() { Node(Black, llk, llv, lll, llr) }),
delay(fn() { Node(Black, key, value, lr, Value(r)) }),
)
_ ->
case force(lr) {
Node(Red, lrk, lrv, lrl, lrr) ->
Node(
Red,
lrk,
lrv,
delay(fn() { Node(Black, lk, lv, ll, lrl) }),
delay(fn() { Node(Black, key, value, lrr, Value(r)) }),
)
_ -> Node(color, key, value, left, right)
}
}
// Случай 3: красный правый дедушка с красным левым внуком
Black, l, Node(Red, rk, rv, rl, rr) ->
case force(rl) {
Node(Red, rlk, rlv, rll, rlr) ->
Node(
Red,
rlk,
rlv,
delay(fn() { Node(Black, key, value, Value(l), rll) }),
delay(fn() { Node(Black, rk, rv, rlr, rr) }),
)
_ ->
case force(rr) {
Node(Red, rrk, rrv, rrl, rrr) ->
Node(
Red,
rk,
rv,
delay(fn() { Node(Black, key, value, Value(l), rl) }),
delay(fn() { Node(Black, rrk, rrv, rrl, rrr) }),
)
_ -> Node(color, key, value, left, right)
}
}
// Базовый случай - балансировка не нужна
_, _, _ -> Node(color, key, value, left, right)
}
}
```
### Основные операции
**Вставка элемента:**
```gleam
pub fn insert(tree: RBTree(k, v), key: k, value: v,
compare: fn(k, k) -> Order) -> RBTree(k, v) {
let result = insert_helper(tree, key, value, compare)
make_black(result) // Корень всегда чёрный
}
```
**Поиск элемента:**
```gleam
pub fn lookup(tree: RBTree(k, v), key: k,
compare: fn(k, k) -> Order) -> Option(v) {
case tree {
Empty -> None
Node(_, k, v, left, right) ->
case compare(key, k) {
order.Lt -> lookup(force(left), key, compare)
order.Gt -> lookup(force(right), key, compare)
order.Eq -> Some(v)
}
}
}
```
### Функции высшего порядка
**Отображение:**
```gleam
pub fn map(tree: RBTree(k, v), f: fn(v) -> w) -> RBTree(k, w) {
case tree {
Empty -> Empty
Node(color, key, value, left, right) ->
Node(color, key, f(value),
delay(fn() { map(force(left), f) }),
delay(fn() { map(force(right), f) }))
}
}
```
**Левая свёртка:**
```gleam
pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a {
case tree {
Empty -> acc
Node(_, key, value, left, right) -> {
let left_acc = fold_left(force(left), acc, f)
let current_acc = f(left_acc, key, value)
fold_left(force(right), current_acc, f)
}
}
}
```
### Операции моноида
```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) {
insert(acc, key, value, compare)
})
}
```
---
## Тесты и метрики
### 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()` - конвертация в список и обратно
### Property-based тесты
1. **Свойства основных операций:**
```gleam
pub fn insert_lookup_property_test() // insert -> lookup инвариант
pub fn insert_size_property_test() // размер увеличивается корректно
pub fn filter_property_test() // фильтр сохраняет структуру
pub fn map_property_test() // map сохраняет структуру
```
2. **Свойства свёрток:**
```gleam
pub fn fold_property_test() // корректность левой/правой свёртки
```
3. **Свойства преобразований:**
```gleam
pub fn list_roundtrip_property_test() // to_list -> from_list эквивалентность
```
### Тесты моноида
```gleam
/// Левая единица: mempty ∘ a = a
pub fn monoid_left_identity_test()
/// Правая единица: a ∘ mempty = a
pub fn monoid_right_identity_test()
/// Ассоциативность: (a ∘ b) ∘ c = a ∘ (b ∘ c)
pub fn monoid_associativity_test()
/// Коммутативность (для непересекающихся множеств ключей)
pub fn monoid_commutativity_test()
```
### Отчёт тестирования
```
Running lab2_test.main
.....................
21 passed, no failures
Compiled in 0.46s
```
### Метрики производительности
| Операция | Теоретическая сложность | Реализованная сложность |
|----------|------------------------|------------------------|
| 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) |
---
## Выводы
### Использованные приёмы программирования
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 продемонстрировала эффективность функциональных подходов к программированию. Использование неизменяемых структур данных, полиморфизма и функций высшего порядка привело к созданию гибкой, безопасной и переиспользуемой библиотеки.
Ленивые вычисления показали свою полезность для оптимизации производительности при работе с большими деревьями, где не все поддеревья могут быть использованы.

19
lab2/gleam.toml Normal file
View File

@@ -0,0 +1,19 @@
name = "lab2"
version = "1.0.0"
# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# description = ""
# licences = ["Apache-2.0"]
# repository = { type = "github", user = "", repo = "" }
# links = [{ title = "Website", href = "" }]
#
# For a full reference of all the available options, you can have a look at
# https://gleam.run/writing-gleam/gleam-toml/.
[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

11
lab2/manifest.toml Normal file
View File

@@ -0,0 +1,11 @@
# This file was generated by Gleam
# You typically do not need to edit this file
packages = [
{ name = "gleam_stdlib", version = "0.67.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "6CE3E4189A8B8EC2F73AB61A2FBDE49F159D6C9C61C49E3B3082E439F260D3D0" },
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
]
[requirements]
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }

498
lab2/src/lab2.gleam Normal file
View File

@@ -0,0 +1,498 @@
// Модуль красно-чёрного дерева с ленивыми вычислениями
// Реализует самобалансирующееся двоичное дерево поиска с гарантированной
// логарифмической сложностью операций вставки, удаления и поиска
import gleam/option.{type Option, None, Some}
import gleam/order.{type Order}
/// Цвета узлов красно-черного дерева
/// Red - красный узел, Black - чёрный узел
pub type Color {
Red
Black
}
/// Ленивое значение для отложенных вычислений
/// Позволяет откладывать создание поддеревьев до момента их использования
pub type Lazy(a) {
Thunk(fn() -> a)
// Отложенное вычисление через функцию
Value(a)
// Уже вычисленное значение
}
/// Красно-черное дерево с ленивыми вычислениями
/// Полиморфная структура данных с ключами типа k и значениями типа v
/// Поддерживает инварианты красно-чёрного дерева для гарантии балансировки
pub type RBTree(k, v) {
Empty
// Пустое дерево
Node(
// Узел дерева
color: Color,
// Цвет узла (красный или чёрный)
key: k,
// Ключ для поиска и сортировки
value: v,
// Значение, связанное с ключом
left: Lazy(RBTree(k, v)),
// Левое поддерево (ленивое)
right: Lazy(RBTree(k, v)),
// Правое поддерево (ленивое)
)
}
/// Форсирует вычисление ленивого значения
/// Если значение уже вычислено, возвращает его, иначе выполняет функцию
fn force(lazy: Lazy(a)) -> a {
case lazy {
Value(val) -> val
Thunk(f) -> f()
}
}
/// Создаёт ленивое значение из функции
/// Отложенное вычисление будет выполнено при первом обращении
fn delay(f: fn() -> a) -> Lazy(a) {
Thunk(f)
}
/// Создаёт пустое дерево - нейтральный элемент моноида
/// Время выполнения: O(1)
pub fn empty() -> RBTree(k, v) {
Empty
}
/// Проверяет, является ли дерево пустым
/// Время выполнения: O(1)
pub fn is_empty(tree: RBTree(k, v)) -> Bool {
case tree {
Empty -> True
_ -> False
}
}
/// Создаёт узел с чёрным цветом
fn make_black(tree: RBTree(k, v)) -> RBTree(k, v) {
case tree {
Node(_, key, value, left, right) -> Node(Black, key, value, left, right)
Empty -> Empty
}
}
/// Балансировка красно-черного дерева
fn balance(
color: Color,
key: k,
value: v,
left: Lazy(RBTree(k, v)),
right: Lazy(RBTree(k, v)),
) -> RBTree(k, v) {
let left_tree = force(left)
let right_tree = force(right)
case color, left_tree, right_tree {
// Случай 1: красный левый parent с красными child
Black, Node(Red, lk, lv, ll, lr), r ->
case force(ll) {
Node(Red, llk, llv, lll, llr) ->
Node(
Red,
lk,
lv,
delay(fn() { Node(Black, llk, llv, lll, llr) }),
delay(fn() { Node(Black, key, value, lr, Value(r)) }),
)
_ ->
case force(lr) {
Node(Red, lrk, lrv, lrl, lrr) ->
Node(
Red,
lrk,
lrv,
delay(fn() { Node(Black, lk, lv, ll, lrl) }),
delay(fn() { Node(Black, key, value, lrr, Value(r)) }),
)
_ -> Node(color, key, value, left, right)
}
}
// Случай 3: красный правый parent с красным левым child
Black, l, Node(Red, rk, rv, rl, rr) ->
case force(rl) {
Node(Red, rlk, rlv, rll, rlr) ->
Node(
Red,
rlk,
rlv,
delay(fn() { Node(Black, key, value, Value(l), rll) }),
delay(fn() { Node(Black, rk, rv, rlr, rr) }),
)
_ ->
case force(rr) {
Node(Red, rrk, rrv, rrl, rrr) ->
Node(
Red,
rk,
rv,
delay(fn() { Node(Black, key, value, Value(l), rl) }),
delay(fn() { Node(Black, rrk, rrv, rrl, rrr) }),
)
_ -> Node(color, key, value, left, right)
}
}
// Базовый случай - балансировка не нужна
_, _, _ -> Node(color, key, value, left, right)
}
}
/// Вставляет элемент в дерево с сохранением инвариантов красно-чёрного дерева
/// Время выполнения: O(log n)
/// compare - функция сравнения ключей, должна возвращать order.Lt, order.Eq или order.Gt
pub fn insert(
tree: RBTree(k, v),
key: k,
value: v,
compare: fn(k, k) -> Order,
) -> RBTree(k, v) {
let result = insert_helper(tree, key, value, compare)
make_black(result)
// Гарантируем, что корень чёрный
}
fn insert_helper(
tree: RBTree(k, v),
key: k,
value: v,
compare: fn(k, k) -> Order,
) -> RBTree(k, v) {
case tree {
Empty -> Node(Red, key, value, Value(Empty), Value(Empty))
Node(color, k, v, left, right) ->
case compare(key, k) {
order.Lt ->
balance(
color,
k,
v,
delay(fn() { insert_helper(force(left), key, value, compare) }),
right,
)
order.Gt ->
balance(
color,
k,
v,
left,
delay(fn() { insert_helper(force(right), key, value, compare) }),
)
order.Eq -> Node(color, key, value, left, right)
}
}
}
/// Поиск элемента в дереве
pub fn lookup(
tree: RBTree(k, v),
key: k,
compare: fn(k, k) -> Order,
) -> Option(v) {
case tree {
Empty -> None
Node(_, k, v, left, right) ->
case compare(key, k) {
order.Lt -> lookup(force(left), key, compare)
order.Gt -> lookup(force(right), key, compare)
order.Eq -> Some(v)
}
}
}
/// Находит минимальный элемент в дереве
fn find_min(tree: RBTree(k, v)) -> Option(#(k, v)) {
case tree {
Empty -> None
Node(_, key, value, left, _) ->
case force(left) {
Empty -> Some(#(key, value))
_ -> find_min(force(left))
}
}
}
/// Удаляет минимальный элемент из дерева
fn delete_min(tree: RBTree(k, v)) -> RBTree(k, v) {
case tree {
Empty -> Empty
Node(color, key, value, left, right) ->
case force(left) {
Empty -> force(right)
_ ->
Node(
color,
key,
value,
delay(fn() { delete_min(force(left)) }),
right,
)
}
}
}
pub fn delete(
tree: RBTree(k, v),
key: k,
compare: fn(k, k) -> Order,
) -> RBTree(k, v) {
case tree {
Empty -> Empty
Node(_, k, v, left, right) ->
case compare(key, k) {
order.Lt ->
Node(
Black,
k,
v,
delay(fn() { delete(force(left), key, compare) }),
right,
)
order.Gt ->
Node(
Black,
k,
v,
left,
delay(fn() { delete(force(right), key, compare) }),
)
order.Eq ->
case force(left), force(right) {
Empty, Empty -> Empty
_, Empty -> force(left)
Empty, _ -> force(right)
_, _ ->
case find_min(force(right)) {
None -> force(left)
Some(#(min_key, min_value)) ->
Node(
Black,
min_key,
min_value,
left,
delay(fn() { delete_min(force(right)) }),
)
}
}
}
}
}
/// Фильтрация элементов дерева
pub fn filter(
tree: RBTree(k, v),
predicate: fn(k, v) -> Bool,
compare: fn(k, k) -> Order,
) -> RBTree(k, v) {
fold_left(tree, empty(), fn(acc, key, value) {
case predicate(key, value) {
True -> insert(acc, key, value, compare)
False -> acc
}
})
}
/// Отображение значений в дереве
pub fn map(tree: RBTree(k, v), f: fn(v) -> w) -> RBTree(k, w) {
case tree {
Empty -> Empty
Node(color, key, value, left, right) ->
Node(
color,
key,
f(value),
delay(fn() { map(force(left), f) }),
delay(fn() { map(force(right), f) }),
)
}
}
/// Левая свёртка дерева
pub fn fold_left(tree: RBTree(k, v), acc: a, f: fn(a, k, v) -> a) -> a {
case tree {
Empty -> acc
Node(_, key, value, left, right) -> {
let left_acc = fold_left(force(left), acc, f)
let current_acc = f(left_acc, key, value)
fold_left(force(right), current_acc, f)
}
}
}
/// Правая свёртка дерева
pub fn fold_right(tree: RBTree(k, v), acc: a, f: fn(k, v, a) -> a) -> a {
case tree {
Empty -> acc
Node(_, key, value, left, right) -> {
let right_acc = fold_right(force(right), acc, f)
let current_acc = f(key, value, right_acc)
fold_right(force(left), current_acc, f)
}
}
}
/// Объединение двух деревьев (операция моноида)
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) {
insert(acc, key, value, compare)
})
}
/// Нейтральный элемент моноида (пустое дерево)
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,
) -> RBTree(k, v) {
list
|> list_fold_left(empty(), fn(acc, pair) {
let #(key, value) = pair
insert(acc, key, value, compare)
})
}
/// Вспомогательная функция для свёртки списка
fn list_fold_left(list: List(a), acc: b, f: fn(b, a) -> b) -> b {
case list {
[] -> acc
[head, ..tail] -> list_fold_left(tail, f(acc, head), f)
}
}
/// Эффективная проверка равенства двух деревьев
/// Сравнивает деревья структурно без преобразования в списки
/// Время выполнения: O(min(n, m)) где n, m - размеры деревьев
pub fn equal(
tree1: RBTree(k, v),
tree2: RBTree(k, v),
key_compare: fn(k, k) -> Bool,
value_compare: fn(v, v) -> Bool,
) -> Bool {
equal_helper(tree1, tree2, key_compare, value_compare)
}
/// Вспомогательная функция для структурного сравнения деревьев
/// Использует короткое замыкание при первом несовпадении
fn equal_helper(
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)
}
}
}
/// Проверка, содержится ли ключ в дереве
pub fn contains(tree: RBTree(k, v), key: k, compare: fn(k, k) -> Order) -> Bool {
case lookup(tree, key, compare) {
Some(_) -> True
None -> False
}
}
/// Получение всех ключей дерева
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) из-за необходимости итерации
pub fn semantic_equal(
tree1: RBTree(k, v),
tree2: RBTree(k, v),
key_order: fn(k, k) -> Order,
value_equal: fn(v, v) -> Bool,
) -> Bool {
// Быстрая проверка размеров
case size(tree1) == size(tree2) {
False -> False
True -> {
// Проверяем, что все элементы из tree1 есть в tree2 с теми же значениями
fold_left(tree1, True, fn(acc, key, value) {
acc &&
case lookup(tree2, key, key_order) {
Some(other_value) -> value_equal(value, other_value)
None -> False
}
})
}
}
}
/// Сравнение с игнорированием цветов узлов (только структура и данные)
/// Полезно когда важна только логическая структура дерева поиска
pub fn structure_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(_, 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)
}
}
}

373
lab2/test/lab2_test.gleam Normal file
View File

@@ -0,0 +1,373 @@
import gleam/int
import gleam/list
import gleam/option
import gleam/order
import gleam/string
import gleeunit
import gleeunit/should
import lab2
pub fn main() {
gleeunit.main()
}
// Вспомогательные функции для тестов
fn int_compare(a: Int, b: Int) -> order.Order {
int.compare(a, b)
}
fn int_equal(a: Int, b: Int) -> Bool {
a == b
}
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()
|> lab2.insert(5, "five", int_compare)
lab2.is_empty(tree) |> should.be_false
lab2.size(tree) |> should.equal(1)
lab2.lookup(tree, 5, int_compare) |> should.equal(option.Some("five"))
lab2.lookup(tree, 10, int_compare) |> should.equal(option.None)
}
/// Тест множественных вставок
pub fn multiple_insert_test() {
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)
|> lab2.insert(9, "nine", int_compare)
lab2.size(tree) |> should.equal(5)
lab2.lookup(tree, 1, int_compare) |> should.equal(option.Some("one"))
lab2.lookup(tree, 3, int_compare) |> should.equal(option.Some("three"))
lab2.lookup(tree, 5, int_compare) |> should.equal(option.Some("five"))
lab2.lookup(tree, 7, int_compare) |> should.equal(option.Some("seven"))
lab2.lookup(tree, 9, int_compare) |> should.equal(option.Some("nine"))
}
/// Тест удаления элементов
pub fn delete_test() {
let tree =
lab2.empty()
|> lab2.insert(5, "five", int_compare)
|> lab2.insert(3, "three", int_compare)
|> lab2.insert(7, "seven", int_compare)
|> lab2.delete(3, int_compare)
lab2.size(tree) |> should.equal(2)
lab2.lookup(tree, 3, int_compare) |> should.equal(option.None)
lab2.lookup(tree, 5, int_compare) |> should.equal(option.Some("five"))
lab2.lookup(tree, 7, int_compare) |> should.equal(option.Some("seven"))
}
/// Тест фильтрации
pub fn filter_test() {
let tree =
lab2.empty()
|> lab2.insert(1, "one", int_compare)
|> lab2.insert(2, "two", int_compare)
|> lab2.insert(3, "three", int_compare)
|> lab2.insert(4, "four", int_compare)
|> lab2.insert(5, "five", int_compare)
let filtered = lab2.filter(tree, fn(key, _) { key % 2 == 0 }, int_compare)
lab2.size(filtered) |> should.equal(2)
lab2.contains(filtered, 2, int_compare) |> should.be_true
lab2.contains(filtered, 4, int_compare) |> should.be_true
lab2.contains(filtered, 1, int_compare) |> should.be_false
}
/// Тест отображения
pub fn map_test() {
let tree =
lab2.empty()
|> lab2.insert(1, 10, int_compare)
|> lab2.insert(2, 20, int_compare)
|> lab2.insert(3, 30, int_compare)
let mapped = lab2.map(tree, fn(value) { value * 2 })
lab2.lookup(mapped, 1, int_compare) |> should.equal(option.Some(20))
lab2.lookup(mapped, 2, int_compare) |> should.equal(option.Some(40))
lab2.lookup(mapped, 3, int_compare) |> should.equal(option.Some(60))
}
/// Тест левой свёртки
pub fn fold_left_test() {
let tree =
lab2.empty()
|> lab2.insert(1, 10, int_compare)
|> lab2.insert(2, 20, int_compare)
|> lab2.insert(3, 30, int_compare)
let sum = lab2.fold_left(tree, 0, fn(acc, _, value) { acc + value })
sum |> should.equal(60)
}
/// Тест правой свёртки
pub fn fold_right_test() {
let tree =
lab2.empty()
|> lab2.insert(1, "a", int_compare)
|> lab2.insert(2, "b", int_compare)
|> lab2.insert(3, "c", int_compare)
let result = lab2.fold_right(tree, "", fn(_, value, acc) { value <> acc })
// Порядок может отличаться в зависимости от структуры дерева
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)
let result_list = lab2.to_list(tree)
lab2.size(tree) |> should.equal(3)
list.length(result_list) |> should.equal(3)
lab2.contains(tree, 1, int_compare) |> should.be_true
lab2.contains(tree, 2, int_compare) |> should.be_true
lab2.contains(tree, 3, int_compare) |> should.be_true
}
// Тесты свойств моноида
/// Тест нейтрального элемента (левая единица)
pub fn monoid_left_identity_test() {
let tree =
lab2.empty()
|> lab2.insert(1, "one", int_compare)
|> lab2.insert(2, "two", int_compare)
let result = lab2.concat(lab2.mempty(), tree, int_compare)
lab2.equal(tree, result, int_equal, string_equal) |> should.be_true
}
/// Тест нейтрального элемента (правая единица)
pub fn monoid_right_identity_test() {
let tree =
lab2.empty()
|> lab2.insert(1, "one", int_compare)
|> lab2.insert(2, "two", int_compare)
let result = lab2.concat(tree, lab2.mempty(), int_compare)
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)
let tree3 = lab2.empty() |> lab2.insert(3, "three", int_compare)
let left_assoc =
lab2.concat(lab2.concat(tree1, tree2, int_compare), tree3, int_compare)
let right_assoc =
lab2.concat(tree1, lab2.concat(tree2, tree3, int_compare), int_compare)
// Проверяем, что оба дерева содержат одинаковые элементы
lab2.size(left_assoc) |> should.equal(3)
lab2.size(right_assoc) |> should.equal(3)
lab2.contains(left_assoc, 1, int_compare) |> should.be_true
lab2.contains(left_assoc, 2, int_compare) |> should.be_true
lab2.contains(left_assoc, 3, int_compare) |> should.be_true
lab2.contains(right_assoc, 1, int_compare) |> should.be_true
lab2.contains(right_assoc, 2, int_compare) |> should.be_true
lab2.contains(right_assoc, 3, int_compare) |> should.be_true
}
/// Тест инвариантов красно-чёрного дерева (упрощённая проверка)
pub fn red_black_invariant_test() {
// Создаём большое дерево и проверяем, что поиск работает корректно
let tree =
lab2.from_list(
[
#(1, "one"),
#(2, "two"),
#(3, "three"),
#(4, "four"),
#(5, "five"),
#(6, "six"),
#(7, "seven"),
#(8, "eight"),
#(9, "nine"),
#(10, "ten"),
],
int_compare,
)
lab2.size(tree) |> should.equal(10)
// Проверяем, что все элементы можно найти
let all_found =
list.all([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], fn(key) {
lab2.contains(tree, key, int_compare)
})
all_found |> should.be_true
}
/// Тест обновления существующего ключа
pub fn update_existing_key_test() {
let tree =
lab2.empty()
|> lab2.insert(1, "old", int_compare)
|> lab2.insert(1, "new", int_compare)
lab2.size(tree) |> should.equal(1)
lab2.lookup(tree, 1, int_compare) |> should.equal(option.Some("new"))
}
/// Тест свойства: insert -> lookup должен возвращать вставленное значение
pub fn insert_lookup_property_test() {
let test_cases = [
#(1, "one"),
#(42, "forty-two"),
#(0, "zero"),
#(-5, "minus-five"),
]
list.each(test_cases, fn(test_case) {
let #(key, value) = test_case
let tree = lab2.empty() |> lab2.insert(key, value, int_compare)
lab2.lookup(tree, key, int_compare) |> should.equal(option.Some(value))
})
}
/// Тест свойства: size увеличивается при вставке новых элементов
pub fn insert_size_property_test() {
let tree = lab2.empty()
lab2.size(tree) |> should.equal(0)
let tree1 = lab2.insert(tree, 1, "one", int_compare)
lab2.size(tree1) |> should.equal(1)
let tree2 = lab2.insert(tree1, 2, "two", int_compare)
lab2.size(tree2) |> should.equal(2)
let tree3 = lab2.insert(tree2, 3, "three", int_compare)
lab2.size(tree3) |> should.equal(3)
// Вставка существующего ключа не должна увеличивать размер
let tree4 = lab2.insert(tree3, 2, "two-updated", int_compare)
lab2.size(tree4) |> should.equal(3)
}
/// Тест свойства: filter сохраняет порядок и структуру
pub fn filter_property_test() {
let original =
lab2.from_list(
[#(1, "a"), #(2, "b"), #(3, "c"), #(4, "d"), #(5, "e")],
int_compare,
)
// Фильтр, который пропускает все элементы
let all_filter = lab2.filter(original, fn(_, _) { True }, int_compare)
lab2.equal(original, all_filter, int_equal, string_equal) |> should.be_true
// Фильтр, который не пропускает ничего
let none_filter = lab2.filter(original, fn(_, _) { False }, int_compare)
lab2.is_empty(none_filter) |> should.be_true
// Частичный фильтр
let partial_filter =
lab2.filter(original, fn(key, _) { key > 3 }, int_compare)
lab2.size(partial_filter) |> should.equal(2)
lab2.contains(partial_filter, 4, int_compare) |> should.be_true
lab2.contains(partial_filter, 5, int_compare) |> should.be_true
}
/// Тест свойства: map сохраняет структуру дерева
pub fn map_property_test() {
let original = lab2.from_list([#(1, 10), #(2, 20), #(3, 30)], int_compare)
// Тождественное отображение
let identity_map = lab2.map(original, fn(x) { x })
lab2.equal(original, identity_map, int_equal, int_equal) |> should.be_true
// Отображение с изменением
let doubled = lab2.map(original, fn(x) { x * 2 })
lab2.size(doubled) |> should.equal(lab2.size(original))
lab2.lookup(doubled, 1, int_compare) |> should.equal(option.Some(20))
lab2.lookup(doubled, 2, int_compare) |> should.equal(option.Some(40))
lab2.lookup(doubled, 3, int_compare) |> should.equal(option.Some(60))
}
/// Тест свойства: 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)
// Сумма всех значений
let sum_left = lab2.fold_left(tree, 0, fn(acc, _, value) { acc + value })
let sum_right = lab2.fold_right(tree, 0, fn(_, value, acc) { acc + value })
sum_left |> should.equal(15)
sum_right |> should.equal(15)
// Количество элементов
let count_left = lab2.fold_left(tree, 0, fn(acc, _, _) { acc + 1 })
let count_right = lab2.fold_right(tree, 0, fn(_, _, acc) { acc + 1 })
count_left |> should.equal(5)
count_right |> should.equal(5)
}
/// Тест свойства: 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)
let result_list = lab2.to_list(tree)
let restored_tree = lab2.from_list(result_list, int_compare)
lab2.size(tree) |> should.equal(lab2.size(restored_tree))
lab2.equal(tree, restored_tree, int_equal, string_equal) |> should.be_true
// Проверяем, что все элементы присутствуют
list.each(original_list, fn(item) {
let #(key, value) = item
lab2.lookup(restored_tree, key, int_compare)
|> should.equal(option.Some(value))
})
}
/// Тест инвариантов моноида (коммутативность для деревьев с разными ключами)
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)
let concat1 = lab2.concat(tree1, tree2, int_compare)
let concat2 = lab2.concat(tree2, tree1, int_compare)
// Для деревьев с разными ключами конкатенация должна быть коммутативной
lab2.size(concat1) |> should.equal(4)
lab2.size(concat2) |> should.equal(4)
// Оба дерева должны содержать все элементы
[1, 2, 3, 4]
|> list.each(fn(key) {
lab2.contains(concat1, key, int_compare) |> should.be_true
lab2.contains(concat2, key, int_compare) |> should.be_true
})
}