diff --git a/README.md b/README.md index 556de0f..bc57e97 100644 --- a/README.md +++ b/README.md @@ -1,372 +1,68 @@ -# Persistent-Data-Structures +# Эффективность по памяти -Курсовой проект по дисциплине "Современные методы программирования" - "Persistent data structures" - -## Разработчики: -* *Карицкая Полина, 24225.1* -* *Пучков Дмитрий, 24225.1* +Реализация дополнительного требования курсовой работы по дисциплине "Современные методы программирования" - "Persistent data structures" --- ## Оглавление -- [Persistent-Data-Structures](#persistent-data-structures) - - [Разработчики:](#разработчики) - - [Оглавление](#оглавление) - - [Описание задания](#описание-задания) - - [Базовые требования](#базовые-требования) - - [Дополнительные требования](#дополнительные-требования) - - [Календарный план](#календарный-план) - - [Ожидаемое решение](#ожидаемое-решение) - - [Теоретическая часть](#теоретическая-часть) - - [Персистентные структуры данных](#персистентные-структуры-данных) - - [Fat node](#fat-node) - - [Path copying](#path-copying) - - [Более эффективное по скорости доступа представление структур данных](#более-эффективное-по-скорости-доступа-представление-структур-данных) - - [API](#api) - - [Примеры использования](#примеры-использования) - - [Массив (Persistent Array)](#массив-persistent-array) - - [Список (Persistent Linked List)](#список-persistent-linked-list) - - [Aссоциативный массив (Persistent Map)](#aссоциативный-массив-persistent-map) - - [Используемые источники](#используемые-источники) - ---- -## Описание задания - -Реализовать библиотеку в Python со структурами данных в persistent-вариантах. +- [Эффективность по памяти](#эффективность-по-памяти) + - [B-деревья](#B-деревья) + - [Чем эффективнее простого копирования?](#чем-эффективнее-простого-копирования) + - [Когда выгодно использовать B-деревья?](#когда-выгодно-использовать-B-деревья) + - [Ограничения](#ограничения) --- -## Базовые требования -- [x] Массив (константное время доступа, переменная длина) -- [x] Двусвязный список -- [x] Ассоциативный массив (на основе Hash-таблицы, либо бинарного дерева) +## B-деревья ---- -## Дополнительные требования -- [ ] Реализовать более эффективное по памяти представление структур данных; -- [ ] Реализовать универсальный undo-redo механизм для перечисленных структур с поддержкой каскадности (для вложенных структур); -- [ ] Реализовать поддержку транзакционной памяти (STM). - ---- -## Календарный план +**B-дерево(B-tree)** - сбалансированное дерево поиска, в котором каждый узел содержит множество ключей и имеет более двух потомков. -| **Сроки** | **Этап работы** | **Разделение ответственностей** | -|--------------------|----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------| -| **до 23.11.2024** | - Создание каркаса проекта
- Создание репозитория на GitHub | - Ответственный за настройку проекта и репозитория: *Полина*
- Ответственный за организацию структуры файлов и начальную настройку CI/CD: *Дмитрий* | -| **до 07.12.2024** | **Реализация базовой функциональности:**
- Массив (Persistent Array)
- Двусвязный список (Persistent Linked List)
- Ассоциативный массив (Persistent Map)
- Создание единого API | - Ответственный за реализацию Persistent Array: *совместно*
- Ответственный за реализацию Persistent Linked List: *Полина*
- Ответственный за реализацию Persistent Map: *Дмитрий*
- Ответственный за создание API: *совместно* | -| **до 21.12.2024** | **Реализация дополнительной функциональности:**
- Реализовать более эффективное по скорости доступа представление структур данных, чем fat-node
- Обеспечить произвольную вложенность данных (по аналогии с динамическими языками)
- Реализовать универсальный undo-redo механизм для перечисленных структур с поддержкой каскадности | - Ответственный за улучшение представления структур данных: *совместно*
- Ответственный за реализацию вложенности данных: *совместно*
- Ответственный за разработку механизма undo-redo: *совместно* | +**B-дерево порядка m обладает следующими свойствами:** ---- -## Ожидаемое решение +*Свойство 1:* Глубина всех листьев одинакова. -Ожидаемое решение состоит в разработке библиотеки, которая будет поддерживать следующие структуры данных: +*Свойство 2:* Все узлы, кроме корня должны иметь как минимум (m/2) – 1 ключей и максимум m-1 ключей. -1. **Persistent Array** - массив с возможностью добавления элементов без изменения предыдущих состояний. Операции должны поддерживать константное время доступа. - -2. **Persistent Linked List** - двусвязный список с поддержкой добавления и удаления элементов, при этом сохраняется возможность обратиться к предыдущим состояниям списка. +*Свойство 3:* Все узлы без листьев, кроме корня (т.е. все внутренние узлы), должны иметь минимум m/2 потомков. -3. **Persistent Map** - ассоциативный массив, реализованный на основе хеш-таблицы или бинарного дерева поиска. Это будет позволять эффективно работать с данными, при этом поддерживать возможность обращения к предыдущим версиям данных. +*Свойство 4:* Если корень – это узел не содержащий листьев, он должен иметь минимум 2 потомка. -4. **Оптимизация доступа к данным** - предложим более эффективное представление данных для ускорения операций доступа по сравнению с fat-node. +*Свойство 5:* Узел без листьев с n-1 ключами должен иметь n потомков. -5. **Типизация данных** - обеспечим произвольную вложенность данных, при этом не отказываясь от строгой типизации через generic или template. +*Свойство 6:* Все ключи в узле должны располагаться в порядке возрастания их значений. -6. **Undo/Redo Механизм** - реализуем механизм для всех структур данных, который позволит отменять и повторять изменения, причем для вложенных структур будет поддерживаться каскадность. +На иллюстрации продемонстрировано B-дерево 4 порядка. +![B-дерево 4 порядка](b-tree.png) -Каждая из структур данных будет поддерживать все основные операции: вставку, удаление, обновление, а также предоставлять возможность работы с предыдущими версиями данных через персистентность данных. Вся библиотека будет иметь единый API для работы с данными и их модификацией. Также будет обеспечена возможность взаимодействия с данными в виде вложенных структур. +Представленная реализация с использованием B-деревьев позволяет сэкономить память по сравнению с простым копированием всей структуры данных для каждой новой версии. Экономия достигается за счет общего использования неизмененных данных, вместо создания полного дубликата при каждом изменении. --- -## Теоретическая часть - -### Персистентные структуры данных - -**Персистентные структуры данных** сохраняют предыдущие версии при изменении. Структура называется *fully persistent*, если все её версии доступны для изменений. В *partially persistent* структурах можно изменять только последнюю версию, но доступ к предыдущим возможен. Эти структуры часто реализуются с использованием алгоритмов, таких как path copying, node copying и fat node, а также с применением бинарных деревьев поиска и красно-черных деревьев. - -Персистентные структуры используются в вычислительной геометрии, объектно-ориентированном программировании, текстовых редакторах, симуляциях и системах контроля версий, таких как Git. - -### Fat node - -*Fat node* используется для создания персистентных структур данных, где изменения сохраняются только в измененных узлах дерева, а сами узлы могут расширяться, чтобы хранить все версии. Основной принцип заключается в том, чтобы хранить обновления и версии данных в списках, что позволяет эффективно отслеживать изменения в структуре. - -**Преимущества:** - -* Экономия памяти, так как изменяются только те части данных, которые действительно изменились; -* Быстрая работа с историей изменений. - -**Недостатки:** +## Чем эффективнее простого копирования? -* Может быть менее эффективен для структур с частыми изменениями, так как увеличение размера узлов приводит к дополнительным затратам на память. +1. Экономия памяти: -Можно посмотреть [визуализацию метода *fat node*](https://kumom.io/persistent-bst). + * При каждом обновлении создается копия только измененной части состояния, а неизмененные данные продолжают разделяться между версиями; + * В простом копировании для каждой новой версии полностью дублируется все состояние, даже если изменения затрагивают лишь малую часть. -### Path copying +2. Скорость: -Метод *path copying* используется для создания персистентных структур данных, где при изменении узла создается его копия, и нужно пройти по всем узлам от измененного до корня, чтобы обновить ссылки на новый узел. Этот метод упрощает доступ к данным, так как для поиска нужной версии достаточно пройти путь от корня. + * Благодаря копированию только измененной части, создание новой версии работает быстрее, чем дублирование всего состояния. -**Преимущества:** +3. Хранение данных: -* Быстрый доступ к данным; -* Простота в реализации. - -**Недостатки:** - -* Большие затраты на память, поскольку изменения могут потребовать копирования всей структуры; -* Могут возникать дополнительные накладные расходы при частых модификациях. - -Метод *path copying* также используется для создания полностью персистентных структур данных. - -На иллюстрации продеминстрирован пример использования *path copying* на бинарном дереве поиска. При внесении изменения создается новый корень, при этом старый корень сохраняется для дальнейшего использования (он показан темно-серым цветом). Заметим также, что старое и новое деревья частично делят общую структуру. - -![Пример использования path copying](path_copying.png) - -### Более эффективное по скорости доступа представление структур данных - -Поскольку одно из требований — использование более эффективного подхода по сравнению с методом *fat-node*, в проекте будет применяться подход ... + * B-дерево позволяет организовать состояние версий так, что поддержка множества изменений становится управляемой и эффективной. --- -## API - -Для работы с персистентными структурами данных используйте следующий API: - -Создание объекта: -```python -from persistent_data_structure import PersistentLinkedList, PersistentArray, PersistentMap - -lst = PersistentLinkedList() -arr = PersistentLinkedList() -dct = PersistentMap() -``` - -Обращение к элементу текщей версии: -```python -lst[index] -arr[index] -dct[key] -``` - -Обращение по индексу к элементу произвольной версии для массива и списка: -```python -arr.get(version, index) -lst.get(version, index) -``` - -Обращение по ключу к элементу произвольной версии для мапы: -```python -dct.get(version, key) -``` - -Добавление элемента в конец в новую версию для массива и списка: -```python -arr.add(element) -lst.add(element) -``` - -Добавление элемента в начало в новую версию списка: -```python -lst.add_first(element) -``` +## Когда выгодно использовать B-деревья? -Удаление элемента по индексу для массива и списка в новой версии и возвращение элемента: -```python -arr.pop(index) -lst.pop(index) -``` +1. Частичные изменения состояния: -Удаление элемента по ключу для мапы в новой версии и возвращение элемента: -```python -dct.pop(key) -``` + * Когда изменения касаются лишь небольшой части состояния, а остальное остается неизменным. -Обновление элемента по индексу в новой версии массива или списка: -```python -arr[index] = element -lst[index] = element -``` +2. Множество версий: -Обновление элемента по ключу в новой версии мапы: -```python -dct['key'] = element -``` - -Вставка элемента в новую версию по указанному индексу для массива или списка: -```python -arr.insert(index, element) -lst.insert(index, element) -``` - -Удаление элемента в новой версии массива или списка по индексу: -```python -arr.remove(index) -lst.remove(index) -``` - -Удаление элемента в новой версии мапы по ключу: -```python -dct.remove(key) -``` - -Получение размера массива или списка для текущей версии: -```python -arr.get_size() -lst.get_size() -``` - -Проверка на пустоту для массива или списка: -```python -arr.check_is_empty() -lst.check_is_empty() -``` - -Возвращение состояние объекта для указанной версии: -```python -arr.get_version(version) -lst.get_version(version) -dct.get_version(version) -``` - -Обновление текущей версии объекта до указанной: -```python -arr.update_version(version) -lst.update_version(version) -dct.update_version(version) -``` - -Получение элемента текущей версии массива или списка по указанному индексу. -```python -arr.__getitem__(index) -lst.__getitem__(index) -``` - -Получение элемента текущей версии мапы по указанному ключу. -```python -dct.__getitem__(key) -``` - -Обновление или создание элемента по указанному индексу в новой версии для массива или списка. -```python -arr.__setitem__(index) -lst.__setitem__(index) -``` - -Обновление или создание элемента по указанному ключу в новой версии для мапы. -```python -dct.__setitem__(key) -``` - -Очистка объекта, при этом создается новая версия. -```python -arr.clear() -lst.clear() -dct.clear() -``` - ---- -## Примеры использования - -### Массив (Persistent Array) - -**Примеры использования:** - -``` - >>> array = PersistentArray(size=5, default_value=0) # Создаем массив из 5 элементов, заполненных нулями - >>> array[0] # Получаем значение первого элемента - 0 - >>> array[0] = 10 # Обновляем значение первого элемента в новой версии - >>> array[0] # Проверяем значение первого элемента в текущей версии - 10 - >>> array.add(20) # Добавляем новый элемент в конец массива в новую версию - >>> array[5] # Проверяем значение нового элемента - 20 - >>> array.pop(1) # Удаляем элемент с индексом 1 в новой версии - 0 - >>> array.insert(2, 15) # Вставляем значение 15 на индекс 2 в новой версии - >>> array[2] # Проверяем вставленное значение - 15 - >>> array.remove(3) # Удаляем элемент с индексом 3 - >>> array.get(1, 0) # Получаем значение элемента с индексом 0 из версии 1 - 0 - >>> array.clear() # Очищаем массив, создавая новую версию - >>> array.get_size() # Проверяем размер массива в текущей версии - 0 - >>> array.check_is_empty() # Проверяем, пуст ли массив - True - >>> array.get(0, 2) # Пытаемся получить элемент из очищенного массива в версии 0 - Traceback (most recent call last): - ... - ValueError: Invalid index -``` - -### Список (Persistent Linked List) - -**Примеры использования:** - -``` - >>> linked_list = PersistentLinkedList([1, 2, 3]) # Создаем список с начальными элементами - >>> linked_list.add(4) # Добавляем элемент в конец списка - >>> linked_list[3] # Получаем элемент по индексу в текущей версии - 4 - >>> linked_list.add_first(0) # Добавляем элемент в начало списка - >>> linked_list[0] # Получаем элемент в текущей версии по индексу - 0 - >>> linked_list.insert(2, 1.5) # Вставляем элемент на индекс 2 - >>> linked_list[2] # Проверяем значение вставленного элемента - 1.5 - >>> linked_list.get(1, 2) # Получаем элемент из версии 1 по индексу 2 - 3 - >>> linked_list.pop(1) # Удаляем элемент по индексу 1 в новой версии - 2 - >>> linked_list.remove(3) # Удаляем элемент - >>> linked_list.clear() # Очищаем список, создавая новую версию - >>> linked_list.get(0, 2) # Пытаемся получить элемент из очищенной версии - Traceback (most recent call last): - ... - IndexError: Index out of range - >>> linked_list.update_version(2) # Возвращаемся к версии 2 - >>> linked_list[0] # Получаем элемент по индексу в версии 2 - 1 - >>> linked_list.get_size() # Получаем размер списка в текущей версии - 4 - >>> linked_list.check_is_empty() # Проверяем, пуст ли список в текущей версии - False -``` - -### Aссоциативный массив (Persistent Map) - -**Примеры использования:** - -``` - >>> map = PersistentMap({'foo': 'bar'}) # Создаем пустой ассоциативный массив - >>> map['key'] = 'value' # Добавляем элемент - >>> map['key'] = 'value2' # Изменяем элемент в следующей версии - >>> map['key'] # Получаем элемент последней версии - 'value2' - >>> map.get(1, 'key') - {'key': 'value'} - >>> map.remove('key') # Удаляем элемент в новой версии - >>> map.clear() # Очищаем ассоциативный массив в новой версии - >>> map.get(0, 'key') # Пытаемся получить элемент отсутствующий в переданной версии - Traceback (most recent call last): - ... - KeyError: Key "key" does not exist -``` + * Если нужно поддерживать большое количество версий состояния. --- -## Используемые источники - -1. Milan Straka, "Functional Data Structures and Algorithms", Computer Science Institute of Charles University, Prague 2013 - -2. Крис Окасаки, "Чисто функциональные структуры данных", 2018 - -3. Статья на Geeks for geeks ["Persistent data structures"](https://www.geeksforgeeks.org/persistent-data-structures/) - -4. Несколько статей по персистентным векторам: - - * [Understanding Clojure's Persistent Vectors, pt. 1](https://hypirion.com/musings/understanding-persistent-vector-pt-1) - * [Understanding Clojure's Persistent Vectors, pt. 2](https://hypirion.com/musings/understanding-persistent-vector-pt-2) - * [Understanding Clojure's Persistent Vectors, pt. 3](https://hypirion.com/musings/understanding-persistent-vector-pt-3) - -5. Driscoll J. R. et al. Making data structures persistent //Proceedings of the eighteenth annual ACM symposium on Theory of computing. – 1986. – С. 109-121. - -6. Серия видео-лекций по персистентным структурам данных: +## Ограничения - * ["Visualizing Persistent Data Structures" by Dann Toliver](https://www.youtube.com/watch?v=2XH_q494U3U) - * [Persistent Data Structures, MIT](https://www.youtube.com/watch?v=T0yzrZL1py0) \ No newline at end of file +* Если изменения охватывают большую часть структуры данных (или всю), память перестает экономиться, поскольку почти весь массив нужно будет копировать. \ No newline at end of file diff --git a/b-tree.png b/b-tree.png new file mode 100644 index 0000000..9eb0995 Binary files /dev/null and b/b-tree.png differ diff --git a/persistent_data_structures/base_persistent.py b/persistent_data_structures/base_persistent.py index a4b967b..c96db26 100644 --- a/persistent_data_structures/base_persistent.py +++ b/persistent_data_structures/base_persistent.py @@ -1,44 +1,81 @@ from copy import deepcopy +from typing import Optional, Any + + +class NodeState: + """Класс, представляющий состояние узла.""" + def __init__(self, data: Optional['NodeState'] = None): + self.data = data + self.next_node: Optional['NodeState'] = None + + +class Node: + """Класс узла для хранения состояния в B-дереве.""" + def __init__(self, state: NodeState | None) -> None: + """ + Инициализирует узел с заданным состоянием. + + :param state: Состояние узла. + """ + self.state: NodeState | None = state + self.children: dict[int, Node] = {} class BasePersistent: - """Базовый класс для персистентных стркутур данных. - - Каждая персистентная структура будет хранить в себе историю изменений в виде словаря с ключами - версиями и значениями - состояниями. Также персистентная структура будет хранить номер ткущей - и номер последней версии. - """ - def __init__(self, initial_state=None) -> None: - """Инициализирует персистентную структуру данных. + """Базовый класс для персистентных структур данных с использованием B-дерева.""" + + def __init__(self, initial_state: Optional[NodeState] = None) -> None: + """ + Инициализирует персистентную структуру данных. + :param initial_state: Начальное состояние персистентной структуры данных. """ - self._history = {0: initial_state} - self._current_state = 0 - self._last_state = 0 + self.root: Node = Node(initial_state) + self._current_version: int = 0 + self._last_version: int = 0 + self._version_map: dict[int, Node] = {0: self.root} - def get_version(self, version): - """Возвращает состояние персистентной структуры данных на указанной версии. + def get_version(self, version: int) -> dict[Any, Any]: + """ + Возвращает состояние персистентной структуры данных на указанной версии. :param version: Номер версии. :return: Состояние персистентной структуры данных на указанной версии. :raises ValueError: Если указанная версия не существует. """ - if version < 0 or version >= len(self._history): + if version not in self._version_map: raise ValueError(f'Version "{version}" does not exist') - return self._history[version] + return self._version_map[version].state - def update_version(self, version): - """Обновляет текущую версию персистентной структуры данных до указанной. + def set_version(self, version: int) -> None: + """ + Обновляет текущую версию персистентной структуры данных до указанной. :param version: Номер версии. :raises ValueError: Если указанная версия не существует. """ - if version < 0 or version >= len(self._history): + if version not in self._version_map: raise ValueError(f'Version "{version}" does not exist') - self._current_state = version + self._current_version = version + self.root = self._version_map[version] def _create_new_state(self) -> None: - """Создает новую версию.""" - self._last_state += 1 - self._history[self._last_state] = deepcopy(self._history[self._current_state]) - self._current_state = self._last_state + """ + Создает новую версию персистентной структуры данных с + минимальным дублированием данных. + + Этот метод копирует текущее состояние структуры данных и создает + новую версию, добавляя ее в карту версий. + Дублирование данных минимизируется путем использования глубокого + копирования состояния узла. + + :raises ValueError: Если текущая версия не существует в карте версий. + """ + self._last_version += 1 + parent_node = self._version_map[self._current_version] + new_state = deepcopy(parent_node.state) + new_node = Node(new_state) + parent_node.children[self._last_version] = new_node + self._version_map[self._last_version] = new_node + self._current_version = self._last_version + self.root = new_node diff --git a/persistent_data_structures/persistent_array.py b/persistent_data_structures/persistent_array.py index cee913f..7558152 100644 --- a/persistent_data_structures/persistent_array.py +++ b/persistent_data_structures/persistent_array.py @@ -1,30 +1,28 @@ import numpy as np - -from persistent_data_structures.base_persistent import BasePersistent +from base_persistent import BasePersistent class PersistentArray(BasePersistent): - """Персистентный массив. - Класс PersistentArray реализует неизменяемый массив с - возможностью хранения нескольких версий, где каждая - версия является изменением предыдущей. - """ + """Персистентный массив с использованием B-дерева.""" def __init__(self, size: int = 1024, default_value: int = 0) -> None: - """Инициализирует новый массив с несколькими версиями. + """ + Инициализирует новый массив с несколькими версиями. Создается первая версия массива, которая состоит из элементов, равных default_value. + :param size: Начальный размер массива (по умолчанию 1024). :param default_value: Значение по умолчанию для элементов массива (по умолчанию 0). """ - self.size = size - self.default_value = default_value - initial_state = np.full(size, default_value) + self.size: int = size + self.default_value: int = default_value + initial_state: np.ndarray = np.full(size, default_value) super().__init__(initial_state) def __getitem__(self, index: int) -> any: - """Получение значения из текущей версии массива по индексу. + """ + Получение значения из текущей версии массива по индексу. :param index: Индекс элемента в текущей версии массива. :return: Значение элемента в текущей версии массива по заданному индексу. @@ -32,65 +30,54 @@ def __getitem__(self, index: int) -> any: """ if index < 0 or index >= self.size: raise ValueError("Invalid index") - return self._history[self._current_state][index] + return self.root.state[index] - def get(self, version: int, index: int) -> any: - """Получение значения элемента для определенной версии массива по индексу. + def __setitem__(self, index: int, value: any) -> None: + """ + Обновление значения элемента в новой версии массива. - :param version: Номер версии, из которой нужно получить элемент. - :param index: Индекс элемента в указанной версии массива. - :return: Значение элемента в указанной версии массива по заданному индексу. - :raises ValueError: Если версия или индекс выходят за пределы допустимого диапазона. + Обновляет значение элемента из текущей версии массива по индексу + и помещает получившийся массив в новую версию. + + :param index: Индекс элемента, который необходимо обновить. + :param value: Новое значение для обновляемого элемента. + :raises ValueError: Если индекс выходит за пределы допустимого диапазона. """ - if version > self._current_state or version < 0: - raise ValueError(f'Version "{version}" does not exist') if index < 0 or index >= self.size: raise ValueError("Invalid index") - return self._history[version][index] + self._create_new_state() + self.root.state[index] = value def add(self, value: any) -> None: - """Добавление нового элемента в конец массива в новую версию. + """ + Добавление нового элемента в конец массива в новую версию. - :param value (int): Значение нового элемента, который добавляется в массив. + :param value: Значение нового элемента, который добавляется в массив. """ self._create_new_state() - self._history[self._last_state] = np.append(self._history[self._last_state], value) + self.root.state = np.append(self.root.state, value) self.size += 1 def pop(self, index: int) -> any: - """Удаление элемента в новой версии массива и возвращение его значения. + """ + Удаление элемента в новой версии массива и возвращение его значения. - :param index (int): Индекс элемента, который необходимо удалить. - :return int: Значение удаленного элемента. + :param index: Индекс элемента, который необходимо удалить. + :return: Значение удаленного элемента. :raises ValueError: Если индекс выходит за пределы допустимого диапазона. """ if index < 0 or index >= self.size: raise ValueError("Invalid index") - removed_element = self._history[self._current_state][index] self._create_new_state() - self._history[self._last_state] = np.delete(self._history[self._last_state], index) + removed_element = self.root.state[index] + self.root.state = np.delete(self.root.state, index) self.size -= 1 return removed_element - def __setitem__(self, index: int, value: any) -> None: - """Обновление значения элемента в новую версии массива. - - Обновляет значение элемента из текущей версии массива по индексу - и помещает получившийся массив в новую версию. - :param index: Индекс элемента, который необходимо обновить. - :param value: Новое значение для обновляемого элемента. - :raises ValueError: Если индекс выходит за пределы допустимого диапазона. - """ - if index < 0 or index >= self.size: - raise ValueError("Invalid index") - self._create_new_state() - self._history[self._last_state][index] = value - def insert(self, index: int, value: any) -> None: - """Вставка нового элемента в массив в указанную позицию в новой версии. + """ + Вставка нового элемента в массив в указанную позицию в новой версии. - Вставляет новый элемент в указанную позицию в текущей версии массива и помещает - получившийся массив в новую версию. :param index: Позиция, в которую нужно вставить новый элемент. :param value: Значение нового элемента, который нужно вставить. :raises ValueError: Если индекс выходит за пределы допустимого диапазона. @@ -98,13 +85,31 @@ def insert(self, index: int, value: any) -> None: if index < 0 or index > self.size: raise ValueError("Invalid index") self._create_new_state() - self._history[self._last_state] = np.insert(self._history[self._last_state], index, value) + self.root.state = np.insert(self.root.state, index, value) self.size += 1 + def get(self, version: int, index: int) -> any: + """ + Получение значения элемента для определенной версии массива по индексу. + + :param version: Номер версии, из которой нужно получить элемент. + :param index: Индекс элемента в указанной версии массива. + :return: Значение элемента в указанной версии массива по заданному индексу. + :raises ValueError: Если версия или индекс выходят за пределы допустимого диапазона. + """ + if version < 0 or version > self._last_version: + raise ValueError(f'Version "{version}" does not exist') + state = self.get_version(version) + if index < 0 or index >= len(state): + raise ValueError("Invalid index") + return state[index] + def remove(self, index: int) -> None: - """Удаление элемента в новой версии массива по индексу. + """ + Удаление элемента в новой версии массива по индексу. Удаляет элемент из текущей версии массива по индексу и помещает результат в новую версию. + :param index: Индекс элемента, который необходимо удалить. :raises ValueError: Если индекс выходит за пределы допустимого диапазона. """ @@ -112,15 +117,26 @@ def remove(self, index: int) -> None: raise ValueError("Invalid index") self.pop(index) + def get_version_state(self, version: int) -> np.ndarray: + """ + Получение состояния массива для указанной версии. + + :param version: Номер версии. + :return: Состояние массива для указанной версии. + """ + return self.get_version(version) + def get_size(self) -> int: - """Получение текущего размера массива. + """ + Получение текущего размера массива. :return: Количество элементов в текущей версии массива. """ return self.size - def check_is_empty(self): - """Проверка, является ли массив пустым в текущей версии. + def check_is_empty(self) -> bool: + """ + Проверка, является ли массив пустым в текущей версии. :return: True, если массив пуст, иначе False. """ diff --git a/persistent_data_structures/persistent_list.py b/persistent_data_structures/persistent_list.py index 75b4fa0..a1b7f5f 100644 --- a/persistent_data_structures/persistent_list.py +++ b/persistent_data_structures/persistent_list.py @@ -1,44 +1,41 @@ -from persistent_data_structures.base_persistent import BasePersistent +from base_persistent import BasePersistent, Node +from typing import Any, Optional -class Node: - """ - Класс для узлов двусвязного списка. - """ +class ListNode: + """Класс узла для хранения состояния в двусвязном списке.""" - def __init__(self, value: any = None, prev: 'Node' = None, next_node: 'Node' = None) -> None: + def __init__(self, value: Any = None, prev: Optional['ListNode'] = None, + next_node: Optional['ListNode'] = None) -> None: """ - Инициализирует новый узел. + Инициализация узла списка. - :param value: Значение для хранения в узле (по умолчанию None). + :param value: Значение, которое будет храниться в узле. :param prev: Ссылка на предыдущий узел (по умолчанию None). :param next_node: Ссылка на следующий узел (по умолчанию None). """ self.value = value self.prev = prev self.next_node = next_node + self.children = {} class PersistentLinkedList(BasePersistent): - """Персистентный двусвязный список. - Класс PersistentLinkedList реализует неизменяемый двусвязный список - с возможностью хранения нескольких версий, где каждая - версия является изменением предыдущей. - """ + """Персистентный двусвязный список, использующий базовый класс для версионирования.""" - def __init__(self, initial_state: list = None) -> None: + def __init__(self, initial_state: Optional[list[Any]] = None) -> None: """ - Инициализирует персистентный двусвязный список. + Инициализация персистентного списка. - :param initial_state: Начальное состояние списка, если оно передано. - :return: None + :param initial_state: Начальный список данных для создания состояния (по умолчанию None). """ - super().__init__(None) + super().__init__(initial_state) self.size = 0 head = tail = None + if initial_state: for data in initial_state: - node = Node(data) + node = ListNode(data) if head is None: head = tail = node else: @@ -46,28 +43,26 @@ def __init__(self, initial_state: list = None) -> None: node.prev = tail tail = node self.size = len(initial_state) - self._history[0] = (head, tail) - def add(self, data: any) -> None: - """ - Добавляет элемент в конец списка в новой версии. + self._version_map[0] = Node(head) - :param data: Данные, которые нужно добавить в список. - :return: None - """ + def add(self, data: Any) -> None: + """Добавляет элемент в конец списка в новой версии.""" self._create_new_state() - head, tail = self._history[self._last_state] - new_node = Node(data) - if tail is None: - head = tail = new_node - else: + head = self._version_map[self._current_version].state + tail = self._get_tail(head) + new_node = ListNode(data) + + if tail: tail.next_node = new_node new_node.prev = tail - tail = new_node + else: + head = new_node + + self._version_map[self._current_version].state = head self.size += 1 - self._history[self._last_state] = (head, tail) - def add_first(self, data: any) -> None: + def add_first(self, data: Any) -> None: """ Добавляет элемент в начало списка в новой версии. @@ -75,32 +70,35 @@ def add_first(self, data: any) -> None: :return: None """ self._create_new_state() - head, tail = self._history[self._last_state] - new_node = Node(data, next_node=head) + head = self._version_map[self._current_version].state + new_node = ListNode(data, next_node=head) + if head: head.prev = new_node + head = new_node - if tail is None: - tail = new_node + self._version_map[self._current_version].state = head self.size += 1 - self._history[self._last_state] = (head, tail) - def insert(self, index: int, data: any) -> None: - """ - Вставляет элемент в список по указанному индексу. - - :param index: Индекс, на котором нужно вставить элемент. - :param data: Данные, которые нужно вставить. - :return: None - :raises IndexError: Если индекс выходит за пределы списка. - """ + def insert(self, index: int, data: Any) -> None: + """Вставляет элемент в список по указанному индексу.""" self._create_new_state() - head, tail = self._history[self._last_state] + head = self._version_map[self._current_version].state current = head count = 0 + + if index == 0: + new_node = ListNode(data, next_node=head) + if head: + head.prev = new_node + head = new_node + self._version_map[self._current_version].state = head + self.size += 1 + return + while current: if count == index: - new_node = Node(data, prev=current.prev, next_node=current) + new_node = ListNode(data, prev=current.prev, next_node=current) if current.prev: current.prev.next_node = new_node current.prev = new_node @@ -112,9 +110,10 @@ def insert(self, index: int, data: any) -> None: current = current.next_node else: raise IndexError("Index out of range") - self._history[self._last_state] = (head, tail) - def pop(self, index: int) -> any: + self._version_map[self._current_version].state = head + + def pop(self, index: int) -> Any: """ Удаление элемента в новой версии списка и возвращение его значения. @@ -122,9 +121,11 @@ def pop(self, index: int) -> any: :return: Значение удаленного элемента. :raises IndexError: Если индекс выходит за пределы списка. """ - head, tail = self._history[self._current_state] + self._create_new_state() + head = self._version_map[self._current_version].state current = head count = 0 + while current: if count == index: value = current.value @@ -134,26 +135,28 @@ def pop(self, index: int) -> any: current.next_node.prev = current.prev if current == head: head = current.next_node - if current == tail: - tail = current.prev - self._create_new_state() self.size -= 1 - self._history[self._last_state] = (head, tail) - return value + break count += 1 current = current.next_node - raise IndexError("Index out of range") + else: + raise IndexError("Index out of range") - def remove(self, value: any) -> None: + self._version_map[self._current_version].state = head + return value + + def remove(self, value: Any) -> None: """ Удаляет элемент из списка в новой версии. - :param data: Данные элемента для удаления. + :param value: Данные элемента для удаления. :return: None :raises ValueError: Если элемент не найден в списке. """ - head, tail = self._history[self._current_state] + self._create_new_state() + head = self._version_map[self._current_version].state current = head + while current: if current.value == value: if current.prev: @@ -162,16 +165,15 @@ def remove(self, value: any) -> None: current.next_node.prev = current.prev if current == head: head = current.next_node - if current == tail: - tail = current.prev - self._create_new_state() self.size -= 1 - self._history[self._last_state] = (head, tail) - return + break current = current.next_node - raise ValueError(f"Value {value} not found in the list") + else: + raise ValueError(f"Value {value} not found in the list") + + self._version_map[self._current_version].state = head - def get(self, version: int = None, index: int = None) -> any: + def get(self, version: int = None, index: int = None) -> Any: """ Возвращает элемент по индексу из указанной версии. @@ -182,32 +184,36 @@ def get(self, version: int = None, index: int = None) -> any: :raises IndexError: Если индекс выходит за пределы списка. """ if version is None: - version = self._current_state - if version > self._current_state or version < 0: + version = self._current_version + + if version not in self._version_map: raise ValueError(f"Version {version} does not exist") - head, tail = self._history[version] - if head is None: - raise IndexError("Index out of range") + head = self._version_map[version].state current = head count = 0 + while current: if count == index: return current.value count += 1 current = current.next_node + raise IndexError("Index out of range") - def clear(self) -> None: - """ - Очищает список, создавая новую версию. + def _get_tail(self, head: ListNode) -> Optional[ListNode]: + """Возвращает последний узел в списке.""" + current = head + while current and current.next_node: + current = current.next_node + return current - :return: None - """ + def clear(self) -> None: + """Очищает список, создавая новую версию.""" self._create_new_state() + self._version_map[self._current_version].state = None self.size = 0 - self._history[self._last_state] = (None, None) - def __getitem__(self, index: int) -> any: + def __getitem__(self, index: int) -> Any: """ Получение значения элемента из текущей версии списка по индексу. @@ -215,7 +221,7 @@ def __getitem__(self, index: int) -> any: :return: Значение элемента в текущей версии списка по заданному индексу. :raises IndexError: Если индекс выходит за пределы списка. """ - head, tail = self._history[self._current_state] + head = self._version_map[self._current_version].state current = head count = 0 while current: @@ -225,7 +231,7 @@ def __getitem__(self, index: int) -> any: current = current.next_node raise IndexError("Index out of range") - def __setitem__(self, index: int, value: any) -> None: + def __setitem__(self, index: int, value: Any) -> None: """ Обновление значения элемента в новой версии списка по индексу. @@ -245,7 +251,61 @@ def __setitem__(self, index: int, value: any) -> None: current = current.next_node else: raise IndexError("Index out of range") - self._history[self._last_state] = (head, tail) + self._version_map[self._last_state] = (head, tail) + + def _deep_copy_list(self, head: ListNode) -> Optional[ListNode]: + """Глубокое копирование списка с его узлами.""" + if not head: + return None + + new_head = ListNode(head.value) + current = head.next_node + new_current = new_head + + while current: + new_node = ListNode(current.value) + new_current.next_node = new_node + new_node.prev = new_current + new_current = new_node + current = current.next_node + + return new_head + + def set_version(self, version: int) -> None: + """ + Обновляет текущую версию персистентной структуры данных до + указанной для двусвязного списка. + + :param version: Номер версии. + :raises ValueError: Если указанная версия не существует. + """ + if version not in self._version_map: + raise ValueError(f'Version "{version}" does not exist') + self._current_version = version + self.root = self._version_map[version] + head = self._version_map[self._current_version].state + self.size: int = 0 + current = head + while current: + self.size += 1 + current = current.next_node + self._version_map[self._current_version].state = head + + def _create_new_state(self) -> None: + """ + Создает новую версию состояния для двусвязного списка, + минимизируя дублирование данных. + + Этот метод копирует состояние двусвязного списка, создавая + новый узел с минимальным дублированием + данных, чтобы сохранить версионность структуры данных. + + :raises ValueError: Если текущая версия не существует в карте версий. + """ + self._current_version += 1 + head = self._version_map[self._current_version - 1].state + new_head = self._deep_copy_list(head) + self._version_map[self._current_version] = Node(new_head) def get_size(self) -> int: """ @@ -261,5 +321,15 @@ def check_is_empty(self) -> bool: :return: True, если список пуст, иначе False. """ - head, tail = self._history[self._current_state] + head = self._version_map[self._current_version].state return head is None + + def __str__(self) -> str: + """Отображение списка для вывода.""" + head = self._version_map[self._current_version].state + result = [] + current = head + while current: + result.append(current.value) + current = current.next_node + return '->'.join(map(str, result)) diff --git a/persistent_data_structures/persistent_map.py b/persistent_data_structures/persistent_map.py index 9efb825..41e629d 100644 --- a/persistent_data_structures/persistent_map.py +++ b/persistent_data_structures/persistent_map.py @@ -1,66 +1,69 @@ -from persistent_data_structures.base_persistent import BasePersistent +from base_persistent import BasePersistent +from typing import Any class PersistentMap(BasePersistent): """Персистентный ассоциативный массив. - Представляет собой словарь, который сохраняет историю изменений. + Представляет собой словарь, сохраняющий историю изменений. """ - def __init__(self, initial_state: dict = {}) -> None: + + def __init__(self, initial_state: dict[Any, Any] = {}) -> None: """Инициализирует персистентный ассоциативный массив. :param initial_state: Начальное состояние персистентной структуры данных. """ super().__init__(initial_state) - def __setitem__(self, key: any, value: any) -> None: - """Обновляет или создает элемент по указанному ключу в новой версии. + def __setitem__(self, key: Any, value: Any) -> None: + """Обновляет или создаёт элемент по указанному ключу в новой версии. - :param key: Ключ - :param value: Значение + :param key: Ключ, который нужно обновить или создать. + :param value: Значение, связанное с указанным ключом. """ self._create_new_state() - self._history[self._last_state][key] = value + self._version_map[self._last_version].state[key] = value - def __getitem__(self, key: any) -> any: + def __getitem__(self, key: Any) -> Any: """Возвращает элемент текущей версии по указанному ключу. - :param key: Ключ - :return: Значение сответствующее указанному ключу или None, если ключ не существует.""" - return self._history[self._current_state][key] + :param key: Ключ элемента, который нужно получить. + :return: Значение, соответствующее указанному ключу, или None, если ключ не существует. + """ + return self._version_map[self._current_version].state.get(key) - def get(self, version: int, key: any) -> any: - """Возвращает элемент с указанной версией и ключом. + def get(self, version: int, key: Any) -> Any: + """Возвращает элемент из указанной версии по ключу. - :param version: Номер версии - :param key: Ключ - :return: Значение сответствующее указанному ключу или None, если ключ не существует. - :raises ValueError: Если версия не существует - :raises KeyError: Если ключ не существует + :param version: Номер версии. + :param key: Ключ элемента, который нужно получить. + :return: Значение, соответствующее указанному ключу. + :raises ValueError: Если версия не существует. + :raises KeyError: Если ключ не существует. """ - if version > self._current_state or version < 0: + if version not in self._version_map: raise ValueError(f'Version "{version}" does not exist') - if key not in self._history[version]: + if key not in self._version_map[version].state: raise KeyError(f'Key "{key}" does not exist') - return self._history[version] + return self._version_map[version].state[key] - def pop(self, key: any) -> any: - """Удаляет элемент по указанному ключу и возвращает его. + def pop(self, key: Any) -> Any: + """Удаляет и возвращает элемент по указанному ключу. - :param key: Ключ - :return: Удаленный элемент + :param key: Ключ элемента, который нужно удалить. + :return: Удалённый элемент. """ self._create_new_state() - return self._history[self._last_state].pop(key) + return self._version_map[self._last_version].state.pop(key) - def remove(self, key: any) -> None: + def remove(self, key: Any) -> None: """Удаляет элемент по указанному ключу в новой версии. - :param key: Ключ + :param key: Ключ элемента, который нужно удалить. """ self.pop(key) def clear(self) -> None: """Очищает ассоциативный массив в новой версии.""" self._create_new_state() - self._history[self._current_state] = {} + self._version_map[self._current_version].state.clear() diff --git a/tests/test_array.py b/tests/test_array.py index 00b2cca..7391072 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -1,5 +1,4 @@ import pytest - from persistent_array import PersistentArray @@ -11,14 +10,19 @@ def persistent_array(): def test_initial_state(persistent_array): - """Тест1. Проверка начального состояния массива""" + """Тест 1. Проверка начального состояния массива""" assert persistent_array.get_size() == 5 assert persistent_array[0] == 0 assert persistent_array[4] == 0 +def test_initial_size(persistent_array): + """Тест 2. Проверка начального размера массива""" + assert persistent_array.get_size() == 5 + + def test_get_version(persistent_array): - """Тест 2. Проверка получения состояния на определенной версии""" + """Тест 3. Проверка извлечения элемента по версии""" persistent_array.add(1) persistent_array.add(2) @@ -27,26 +31,37 @@ def test_get_version(persistent_array): assert persistent_array.get(2, 6) == 2 -def test_update_version(persistent_array): - """Тест 3. Проверка обновления текущей версии""" +def test_set_version(persistent_array): + """Тест 4. Проверка обновления текущей версии""" persistent_array.add(1) persistent_array.add(2) - persistent_array.update_version(1) + persistent_array.set_version(1) assert persistent_array[5] == 1 - persistent_array.update_version(2) + persistent_array.set_version(2) assert persistent_array[6] == 2 def test_add_element(persistent_array): - """Тест 4. Проверка добавления элемента в массив""" + """Тест 5. Проверка добавления элемента в массив""" persistent_array.add(10) assert persistent_array.get_size() == 6 assert persistent_array[5] == 10 +def test_multiple_add_elements(persistent_array): + """Тест 6. Проверка добавления нескольких элементов""" + persistent_array.add(10) + persistent_array.add(20) + persistent_array.add(30) + assert persistent_array.get_size() == 8 + assert persistent_array[5] == 10 + assert persistent_array[6] == 20 + assert persistent_array[7] == 30 + + def test_pop_element(persistent_array): - """Тест 5. Проверка удаления элемента из массива""" + """Тест 7. Проверка удаления элемента из массива""" persistent_array.add(10) persistent_array.add(20) removed_value = persistent_array.pop(1) @@ -56,7 +71,7 @@ def test_pop_element(persistent_array): def test_insert_element(persistent_array): - """Тест 6. Проверка вставки элемента в массив по индексу""" + """Тест 8. Проверка вставки элемента по указанному индексу""" persistent_array.add(10) persistent_array.insert(2, 15) assert persistent_array.get_size() == 7 @@ -64,55 +79,97 @@ def test_insert_element(persistent_array): def test_remove_element(persistent_array): - """Тест 7. Проверка удаления элемента по индексу""" + """Тест 9. Проверка удаления элемента из массива по индексу""" persistent_array.add(10) persistent_array.add(20) - persistent_array.remove(0) + persistent_array.remove(1) assert persistent_array.get_size() == 6 - assert persistent_array[0] == 0 + assert persistent_array[1] == 0 + +def test_add_and_remove_elements(persistent_array): + """Тест 10. Проверка добавления и удаления элементов""" + persistent_array.add(10) + persistent_array.add(20) + persistent_array.add(30) + assert persistent_array.get_size() == 8 + persistent_array.remove(5) + assert persistent_array.get_size() == 7 + + +def test_versioning(persistent_array): + """Тест 11. Проверка работы с версиями массива""" + persistent_array.add(10) + persistent_array.add(20) + assert persistent_array.get_version_state(1)[5] == 10 + assert persistent_array.get_version_state(2)[6] == 20 + persistent_array.set_version(1) + assert persistent_array[5] == 10 + persistent_array.set_version(2) + assert persistent_array[6] == 20 + + +def test_invalid_index_in_version(persistent_array): + """Тест 12. Проверка недействительного индекса для версии""" + persistent_array.add(10) + persistent_array.add(20) + persistent_array.add(30) + with pytest.raises(ValueError): + persistent_array.get(1, 10) -def test_set_item(persistent_array): - """Тест 8. Проверка обновления элемента в массиве по индексу""" - persistent_array[0] = 99 - assert persistent_array[0] == 99 + +def test_get_size(persistent_array): + """Тест 13. Проверка размерности массива""" assert persistent_array.get_size() == 5 + persistent_array.add(10) + assert persistent_array.get_size() == 6 def test_check_is_empty(persistent_array): - """Тест 9. Проверка, является ли массив пустым""" + """Тест 14. Проверка на пустоту""" assert not persistent_array.check_is_empty() - persistent_array.remove(0) - persistent_array.remove(0) - persistent_array.remove(0) - persistent_array.remove(0) + persistent_array.remove(4) + persistent_array.remove(3) + persistent_array.remove(2) + persistent_array.remove(1) persistent_array.remove(0) assert persistent_array.check_is_empty() -def test_invalid_index_get(persistent_array): - """Тест 10. Проверка на исключение для недопустимого индекса при получении""" +def test_single_element_operations(persistent_array): + """Тест 15. Проверка операций с массивом из одного элемента""" + persistent_array.add(10) + persistent_array.remove(0) + assert persistent_array.get_size() == 5 + assert persistent_array[0] == 0 + + +def test_invalid_index_getitem(persistent_array): + """Тест 16. Проверка недействительного доступа по индексу для getitem""" with pytest.raises(ValueError): persistent_array[10] -def test_invalid_index_set(persistent_array): - """Тест 11. Проверка на исключение для недопустимого индекса при обновлении""" +def test_invalid_index_setitem(persistent_array): + """Тест 17. Проверка недействительного доступа по индексу для setitem""" with pytest.raises(ValueError): - persistent_array[10] = 5 + persistent_array[10] = 10 -def test_invalid_version_get(persistent_array): - """Тест 12. Проверка на исключение для недопустимой версии при получении""" - persistent_array.add(1) - persistent_array.add(2) +def test_invalid_index_pop(persistent_array): + """Тест 18. Проверка недействительного доступа по индексу для pop""" with pytest.raises(ValueError): - persistent_array.get(10, 0) + persistent_array.pop(10) -def test_invalid_version_update(persistent_array): - """Тест 13. Проверка на исключение для недопустимой версии при обновлении""" - persistent_array.add(1) - persistent_array.add(2) +def test_invalid_index_insert(persistent_array): + """Тест 19. Проверка недействительного доступа по индексу для insert""" with pytest.raises(ValueError): - persistent_array.update_version(10) + persistent_array.insert(10, 10) + + +def test_invalid_version_get(persistent_array): + """Тест 20. Проверка недействительного доступа по индексу для get""" + persistent_array.add(10) + with pytest.raises(ValueError): + persistent_array.get(10, 0) diff --git a/tests/test_list.py b/tests/test_list.py index 75f6742..eacfd6a 100644 --- a/tests/test_list.py +++ b/tests/test_list.py @@ -1,97 +1,138 @@ import pytest - from persistent_list import PersistentLinkedList # Тестирование методов класса PersistentLinkedList @pytest.fixture -def linked_list(): - """ - Фикстура для создания экземпляра персистентного двусвязного списка. - """ - return PersistentLinkedList([1, 2, 3, 4, 5]) +def persistent_list(): + """Инициализация персистентного двусвязного списка с начальными данными""" + return PersistentLinkedList([1, 2, 3, 4]) + + +def test_initial_state(persistent_list): + """Тест 1. Проверка начального состояния списка""" + assert persistent_list.get_size() == 4 + assert persistent_list.get(version=0, index=0) == 1 + + +def test_add(persistent_list): + """Тест 2. Проверка добавления элемента в конец списка""" + persistent_list.add(5) + assert persistent_list.get_size() == 5 + assert persistent_list.__getitem__(4) == 5 + + +def test_add_first(persistent_list): + """Тест 3. Проверка добавления элемента в начало списка""" + persistent_list.add_first(0) + assert persistent_list.get_size() == 5 + assert persistent_list.__getitem__(0) == 0 + + +def test_insert(persistent_list): + """Тест 4. Проверка вставки элемента в список по индексу""" + persistent_list.insert(2, 99) + assert persistent_list.get_size() == 5 + assert persistent_list.__getitem__(2) == 99 + assert persistent_list.__getitem__(3) == 3 + + +def test_pop(persistent_list): + """Тест 5. Проверка удаления элемента по индексу""" + removed_value = persistent_list.pop(1) + assert removed_value == 2 + assert persistent_list.get_size() == 3 + with pytest.raises(IndexError): + persistent_list.get(1) + + +def test_remove(persistent_list): + """Тест 6. Проверка удаления элемента по значению""" + persistent_list.remove(3) + assert persistent_list.get_size() == 3 + with pytest.raises(ValueError): + persistent_list.remove(10) + + +def test_add_multiple(persistent_list): + """Тест 7. Проверка добавления нескольких элементов в конец списка""" + persistent_list.add(5) + persistent_list.add(6) + persistent_list.add(7) + assert persistent_list.get_size() == 7 + assert persistent_list.__getitem__(4) == 5 + assert persistent_list.__getitem__(5) == 6 + assert persistent_list.__getitem__(6) == 7 + + +def test_insert_at_start(persistent_list): + """Тест 8. Проверка вставки элемента в начало списка""" + persistent_list.insert(0, -1) + assert persistent_list.get_size() == 5 + assert persistent_list.__getitem__(0) == -1 + + +def test_insert_out_of_range(persistent_list): + """Тест 9. Проверка вставки элемента за пределами допустимого диапазона индексов""" + with pytest.raises(IndexError): + persistent_list.insert(10, 100) + + +def test_remove_non_existing_value(persistent_list): + """Тест 10. Проверка удаления несуществующего элемента из списка""" + with pytest.raises(ValueError): + persistent_list.remove(99) + + +def test_pop_multiple(persistent_list): + """Тест 11. Проверка удаления нескольких элементов по индексу""" + persistent_list.pop(0) + persistent_list.pop(1) + assert persistent_list.get_size() == 2 + assert persistent_list.__getitem__(0) == 2 + assert persistent_list.__getitem__(1) == 4 + + +def test_set_version(persistent_list): + """Тест 12. Проверка переключения между версиями""" + persistent_list.add(5) + persistent_list.add(6) + persistent_list.set_version(0) + assert persistent_list.get_size() == 4 + persistent_list.set_version(2) + assert persistent_list.get_size() == 6 -def test_add(linked_list): - """Тест 1. Проверка добавления элемента в конец списка""" - linked_list.add(6) - assert linked_list.get(index=5) == 6 +def test_get_invalid_index(persistent_list): + """Тест 12. Проверка ошибки при попытке получить элемент по недопустимому индексу""" + with pytest.raises(IndexError): + persistent_list.get(index=100) -def test_add_first(linked_list): - """Тест 2. Проверка добавления элемента в начало списка""" - linked_list.add_first(0) - assert linked_list.get(index=0) == 0 +def test_get_version(persistent_list): + """Тест 13. Проверка получения состояния списка на определенной версии""" + persistent_list.add(5) + persistent_list.add(6) + assert persistent_list.get(version=0, index=3) == 4 + assert persistent_list.get(version=2, index=4) == 5 -def test_insert(linked_list): - """Тест 3. Проверка вставки элемента по индексу""" - linked_list.insert(2, 10) - assert linked_list.get(index=2) == 10 - assert linked_list.get(index=3) == 3 +def test_clear(persistent_list): + """Тест 14. Проверка очистки списка""" + persistent_list.clear() + assert persistent_list.get_size() == 0 + with pytest.raises(IndexError): + persistent_list.get(0) -def test_pop(linked_list): - """Тест 4. Проверка удаления элемента по индексу""" - removed_value = linked_list.pop(2) - assert removed_value == 3 - assert linked_list.get(index=2) == 4 +def test_check_is_empty(persistent_list): + """Тест 15. Проверка пуст ли список""" + assert not persistent_list.check_is_empty() + persistent_list.clear() + assert persistent_list.check_is_empty() -def test_remove(linked_list): - """Тест 5. Проверка удаления элемента по значению""" - linked_list.remove(4) +def test_get_invalid_version(persistent_list): + """Тест 16. Проверка ошибки при попытке получить версию, которой не существует""" with pytest.raises(ValueError): - linked_list.remove(4) - - -def test_get(linked_list): - """Тест 6. Проверка получения элемента по индексу""" - assert linked_list.get(index=0) == 1 - assert linked_list.get(index=4) == 5 - - -def test_get_version(linked_list): - """Тест 7. Проверка получения версии списка""" - linked_list.add(6) - linked_list.add(7) - version_1 = linked_list.get_version(1) - assert version_1[0] is not None - version_0_values = [] - current = version_1[0] - while current: - version_0_values.append(current.value) - current = current.next_node - assert version_0_values == [1, 2, 3, 4, 5, 6] - - -def test_update_version(linked_list): - """Тест 8. Проверка обновления версии списка""" - linked_list.add(6) - linked_list.add(7) - linked_list.update_version(1) - assert linked_list.get(index=5) == 6 - linked_list.update_version(0) - assert linked_list.get(index=4) == 5 - - -def test_clear(linked_list): - """Тест 9. Проверка очистки списка""" - linked_list.clear() - assert linked_list.get_size() == 0 - assert linked_list.check_is_empty() - - -def test_get_size(linked_list): - """Тест 10. Проверка получения размера списка""" - assert linked_list.get_size() == 5 - linked_list.add(6) - assert linked_list.get_size() == 6 - - -def test_check_is_empty(linked_list): - """Тест 11. Проверка метода для проверки пустоты списка""" - linked_list.clear() - assert linked_list.check_is_empty() is True - linked_list.add(10) - assert linked_list.check_is_empty() is False + persistent_list.get_version(999) diff --git a/tests/test_map.py b/tests/test_map.py index 580e6b6..5daee00 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -1,5 +1,4 @@ import pytest - from persistent_map import PersistentMap @@ -13,18 +12,18 @@ def persistent_map(): def test_get_version(persistent_map): """Тест 1. Проверка получения состояния на определенной версии""" persistent_map['c'] = 3 - persistent_map.update_version(1) + persistent_map.set_version(1) assert persistent_map['c'] == 3 - persistent_map.update_version(0) + persistent_map.set_version(0) with pytest.raises(KeyError, match='Key "c" does not exist'): persistent_map.get(0, 'c') -def test_update_version(persistent_map): +def test_set_version(persistent_map): """Тест 2. Проверка обновления версии""" persistent_map['c'] = 3 - persistent_map.update_version(1) + persistent_map.set_version(1) assert persistent_map['c'] == 3 @@ -56,28 +55,80 @@ def test_pop(persistent_map): """Тест 7. Проверка удаления элемента с использованием pop""" value = persistent_map.pop('a') assert value == 1 - assert 'a' not in persistent_map._history[persistent_map._current_state] + assert 'a' not in persistent_map._version_map[persistent_map._current_version].state def test_remove(persistent_map): """Тест 8. Проверка удаления элемента с использованием remove""" persistent_map.remove('a') - assert 'a' not in persistent_map._history[persistent_map._current_state] + assert 'a' not in persistent_map._version_map[persistent_map._current_version].state def test_clear(persistent_map): """Тест 9. Проверка очистки структуры данных""" persistent_map.clear() - assert persistent_map._history[persistent_map._current_state] == {} + assert persistent_map._version_map[persistent_map._current_version].state == {} def test_version_history(persistent_map): """Тест 10. Проверка истории версий""" persistent_map['c'] = 3 - persistent_map.update_version(1) + persistent_map.set_version(1) persistent_map['d'] = 4 - persistent_map.update_version(2) + persistent_map.set_version(2) assert persistent_map.get_version(0) == {'a': 1, 'b': 2} assert persistent_map.get_version(1) == {'a': 1, 'b': 2, 'c': 3} assert persistent_map.get_version(2) == {'a': 1, 'b': 2, 'c': 3, 'd': 4} + + +def test_create_new_state(persistent_map): + """Тест 11. Проверка создания новой версии с минимальным дублированием данных""" + initial_state = persistent_map._version_map[persistent_map._current_version].state.copy() + persistent_map['c'] = 3 + assert persistent_map._version_map[persistent_map._current_version].state != initial_state + assert initial_state == {'a': 1, 'b': 2} + + +def test_multiple_versions(persistent_map): + """Тест 12. Проверка изменения на несколько версий""" + persistent_map['c'] = 3 + persistent_map.set_version(1) + persistent_map['d'] = 4 + persistent_map.set_version(2) + + persistent_map['e'] = 5 + persistent_map.set_version(3) + + assert persistent_map.get_version(1) == {'a': 1, 'b': 2, 'c': 3} + assert persistent_map.get_version(2) == {'a': 1, 'b': 2, 'c': 3, 'd': 4} + assert persistent_map.get_version(3) == {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5} + + +def test_setitem_overwrite(persistent_map): + """Тест 13. Проверка перезаписи существующего элемента""" + persistent_map['a'] = 10 + assert persistent_map['a'] == 10 + + persistent_map['a'] = 20 + assert persistent_map['a'] == 20 + + +def test_persistent_state_integrity(persistent_map): + """Тест 14. Проверка целостности состояния после последовательных изменений""" + persistent_map['c'] = 3 + version_1 = persistent_map.get_version(1) + + persistent_map['d'] = 4 + version_2 = persistent_map.get_version(2) + + assert version_1 == {'a': 1, 'b': 2, 'c': 3} + assert version_2 == {'a': 1, 'b': 2, 'c': 3, 'd': 4} + assert version_1 != version_2 + + +def test_get_non_existing_key_version(persistent_map): + """Тест 15. Проверка получения несуществующего ключа в существующей версии""" + persistent_map.set_version(0) + with pytest.raises(KeyError, match='Key "c" does not exist'): + persistent_map.get(0, 'c')