From 436a9ed124a5d2733a148faabff16d1c361e6148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Mercado?= Date: Wed, 9 Oct 2024 13:16:32 +0200 Subject: [PATCH 01/13] docs: check adjacency list in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 344e3a5..500ee1a 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,4 @@ A Python library for data structures. This library provides more than 20 advance - [x] Trie - [x] Max Heap - [x] Graph (Adjacency Matrix) -- [ ] Graph (Adjacency List) +- [x] Graph (Adjacency List) From 3fbd826a200720eca5468954a18ef09b5638f6cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20P=C3=A9rez=20Mercado?= Date: Wed, 9 Oct 2024 13:22:25 +0200 Subject: [PATCH 02/13] enhancement: add base class for linked lists (#6) Create a base class for linked lists, with an abstract method _link defined in the derived classes SinglyLinkedList and DoublyLinkedList --- pystrukts/list/doubly_linked_list.py | 439 +++++---------------------- pystrukts/list/linked_list.py | 355 ++++++++++++++++++++++ pystrukts/list/singly_linked_list.py | 418 +++++-------------------- tests/test_doubly_linked_list.py | 58 ++-- tests/test_singly_linked_list.py | 36 +-- 5 files changed, 547 insertions(+), 759 deletions(-) create mode 100644 pystrukts/list/linked_list.py diff --git a/pystrukts/list/doubly_linked_list.py b/pystrukts/list/doubly_linked_list.py index 09294c8..c3b1635 100644 --- a/pystrukts/list/doubly_linked_list.py +++ b/pystrukts/list/doubly_linked_list.py @@ -1,364 +1,75 @@ -# pylint: disable=duplicate-code -''' -Doubly Linked List Module. - -This module implements a Doubly Linked List and Node data structures. -''' - -from dataclasses import dataclass - -@dataclass -class DoublyLinkedNode: - ''' - DoublyLinkedNode Class. - - Attributes: - data (any): The data stored in the node. - prev (DoublyLinkedNode): The previous node in the list. - next (DoublyLinkedNode): The next node in the list. - - Methods: - `__str__()`: Return the string representation of the node. - ''' - - data: any = None - prev: 'DoublyLinkedNode' = None - next: 'DoublyLinkedNode' = None - - def __str__(self): - return f"DoublyLinkedNode({self.data}){' <-> ' + str(self.next) if self.next else ''}" - -@dataclass -class DoublyLinkedList: - ''' - DoublyLinkedList Class. - - Attributes: - __head (DoublyLinkedNode): The head of the list. - __tail (DoublyLinkedNode): The tail of the list. - __length (int): The length of the list. - __circular (bool): Whether the list is circular. - - Methods: - `__str__()`: Return the string representation of the list. - `__len__()`: Return the length of the list. - `__iter__()`: Return an iterator for the list. - `__next__()`: Return the next element in the list. - `iscircular()`: Check if the list is circular. - `insert(data: any, index: int)`: Insert data at the specified index. - `append(data: any)`: Append data to the end of the list. - `appendleft(data: any)`: Append data to the beginning of the list. - `remove(index: int)`: Remove data at the specified index. - `pop()`: Remove data from the end of the list. - `popleft()`: Remove data from the beginning of the list. - `get(index: int)`: Get data at the specified index. - `index(data: any)`: Get the index of the specified data. - `clear()`: Clear the list. - ''' - - __head: 'DoublyLinkedNode' = None - __tail: 'DoublyLinkedNode' = None - __length: int = 0 - __circular: bool = False - - def __init__(self, circular: bool = False): - ''' - Initialize the DoublyLinkedList. - - Args: - circular (bool): Whether the list is circular. - - Returns: - out (DoublyLinkedList): The DoublyLinkedList instance. - ''' - - self.__head = None - self.__tail = None - self.__length = 0 - self.__circular = circular - - def __str__(self): - return f"DoublyLinkedList({self.__head})" - - def __len__(self): - return self.__length - - def __iter__(self): - ''' - Return an iterator for the list. - - Returns: - out (Iterator): The iterator for the list. - - Yields: - out (any): The data in the list. - ''' - - current_node = self.__head - while current_node is not None: - yield current_node.data - current_node = current_node.next - - def iscircular(self): - ''' - Check if the list is circular. - - Returns: - out (bool): Whether the list is circular. - ''' - - return self.__circular - - def insert(self, data: any, index: int): - ''' - Insert data at the specified index. - - Args: - data (any): The data to be inserted. - index (int): The index to insert the data. - - Raises: - IndexError: If the index is out of bounds. - ''' - - if index < 0: - index = self.__length + index + 1 - - if index < 0 or index > self.__length: - raise IndexError("Index out of bounds") - - if index == 0: - self.appendleft(data) - return - - if index == self.__length: - self.append(data) - return - - new_node = DoublyLinkedNode(data) - - current_node = self.__head - for _ in range(index - 1): - current_node = current_node.next - - new_node.next = current_node.next - current_node.next.prev = new_node - current_node.next = new_node - new_node.prev = current_node - - self.__length += 1 - - def append(self, data: any): - ''' - Append data to the end of the list. - - Args: - data (any): The data to be appended. - ''' - - new_node = DoublyLinkedNode(data) - - if self.__length == 0: - self.__head = new_node - self.__tail = new_node - - else: - self.__tail.next = new_node - new_node.prev = self.__tail - self.__tail = new_node - - if self.__circular: - self.__tail.next = self.__head - self.__head.prev = self.__tail - - self.__length += 1 - - def appendleft(self, data: any): - ''' - Append data to the beginning of the list. - - Args: - data (any): The data to be appended. - ''' - - new_node = DoublyLinkedNode(data) - - if self.__length == 0: - self.__head = new_node - self.__tail = new_node - - else: - new_node.next = self.__head - self.__head.prev = new_node - self.__head = new_node - - if self.__circular: - self.__tail.next = self.__head - self.__head.prev = self.__tail - - self.__length += 1 - - def remove(self, index: int): - ''' - Remove data at the specified index. - - Args: - index (int): The index to remove the data. - - Returns: - out (any): The removed data. - - Raises: - IndexError: If the index is out of bounds. - ''' - - if index < 0: - index = self.__length + index - - if index < 0 or index >= self.__length: - raise IndexError("Index out of bounds") - - if index == 0: - return self.popleft() - - if index == self.__length - 1: - return self.pop() - - current_node = self.__head - for _ in range(index - 1): - current_node = current_node.next - - removed_node = current_node.next - current_node.next = removed_node.next - current_node.next.prev = current_node - removed_node.next = None - removed_node.prev = None - - self.__length -= 1 - - return removed_node.data - - def pop(self): - ''' - Extract data from the end of the list. - - Returns: - out (any): The extracted data. - - Raises: - IndexError: If the list is empty. - ''' - - if self.__length == 0: - raise IndexError("Empty list") - - if self.__length == 1: - removed_node = self.__head - self.__head = None - self.__tail = None - - else: - removed_node = self.__tail - self.__tail = self.__tail.prev - self.__tail.next = removed_node.next - - if self.__circular: - self.__head.prev = self.__tail - - self.__length -= 1 - - return removed_node.data - - def popleft(self): - ''' - Extract data from the beginning of the list. - - Returns: - out (any): The extracted data. - - Raises: - IndexError: If the list is empty. - ''' - - if self.__length == 0: - raise IndexError("Empty list") - - removed_node = self.__head - - if self.__length == 1: - self.__head = None - self.__tail = None - - else: - self.__head = self.__head.next - self.__head.prev = removed_node.prev - - if self.__circular: - self.__tail.next = self.__head - - self.__length -= 1 - - return removed_node.data - - def get(self, index: int): - ''' - Get data at the specified index. - - Args: - index (int): The index to get the data. - - Returns: - out (any): The data at the specified index. - - Raises: - IndexError: If the index is out of bounds. - ''' - - if index < 0: - index = self.__length + index - - if index < 0 or index >= self.__length: - raise IndexError("Index out of bounds") - - if index < self.__length // 2: - current_node = self.__head - for _ in range(index): - current_node = current_node.next - else: - current_node = self.__tail - for _ in range(self.__length - index - 1): - current_node = current_node.prev - - return current_node.data - - def index(self, data: any): - ''' - Get the index of the first occurrence of the specified data. - - Args: - data (any): The data to search for. - - Returns: - out (int): The index of the specified data. - - Raises: - ValueError: If the data is not found. - ''' - - current_node = self.__head - for i in range(self.__length): - if current_node.data == data: - return i - - current_node = current_node.next - - raise ValueError("Data not found") - - def clear(self): - ''' - Remove all data from the list. - ''' - - self.__head = None - self.__tail = None - self.__length = 0 +''' +Doubly Linked List Module. + +This module implements a Doubly Linked List and Node data structures. +''' + +from dataclasses import dataclass +from .linked_list import LinkedList + +@dataclass +class DoublyLinkedNode: + ''' + DoublyLinkedNode Class. + + Attributes: + data (any): The data stored in the node. + prev (DoublyLinkedNode): The previous node in the list. + next (DoublyLinkedNode): The next node in the list. + + Methods: + `__str__()`: Return the string representation of the node. + ''' + + data: any = None + prev: 'DoublyLinkedNode' = None + next: 'DoublyLinkedNode' = None + + def __str__(self): + return f"DoublyLinkedNode({self.data}){' <-> ' + str(self.next) if self.next else ''}" + +@dataclass +class DoublyLinkedList(LinkedList[DoublyLinkedNode]): + ''' + DoublyLinkedList Class. + + Attributes: + __head (DoublyLinkedNode): The head of the list. + __tail (DoublyLinkedNode): The tail of the list. + __length (int): The length of the list. + __circular (bool): Whether the list is circular. + + Methods: + `__str__()`: Return the string representation of the list. + `__len__()`: Return the length of the list. + `__iter__()`: Return an iterator for the list. + `iscircular()`: Check if the list is circular. + `_link(node1: Type[T], node2: Type[T])`: Link two nodes. + `_preprocess_index(index: int)`: Preprocess the index. + `insert(data: any, index: int)`: Insert data at the specified index. + `append(data: any)`: Append data to the end of the list. + `appendleft(data: any)`: Append data to the beginning of the list. + `remove(index: int)`: Remove data at the specified index. + `pop()`: Remove data from the end of the list. + `popleft()`: Remove data from the beginning of the list. + `get(index: int)`: Get data at the specified index. + `index(data: any)`: Get the index of the specified data. + `clear()`: Clear the list. + ''' + + def __init__(self, circular: bool = False): + super().__init__(DoublyLinkedNode, circular) + + def _link(self, node1: DoublyLinkedNode, node2: DoublyLinkedNode): + ''' + Link two nodes. + + Args: + node1 (DoublyLinkedNode): The first node. + node2 (DoublyLinkedNode): The second node. + ''' + + node1.next = node2 + if node2: + node2.prev = node1 + \ No newline at end of file diff --git a/pystrukts/list/linked_list.py b/pystrukts/list/linked_list.py new file mode 100644 index 0000000..18824ca --- /dev/null +++ b/pystrukts/list/linked_list.py @@ -0,0 +1,355 @@ +''' +Base Linked List Module. + +This module implements a Linked List data structure. +''' + +from dataclasses import dataclass +from abc import ABC, abstractmethod +from typing import Type, TypeVar, Generic + +T = TypeVar('T') + +@dataclass +class LinkedList(ABC, Generic[T]): + ''' + LinkedList Class. + + Attributes: + __head (Type[T]): The head of the list. T is a node class. + __tail (Type[T]): The tail of the list. T is a node class. + __length (int): The length of the list. + __circular (bool): Whether the list is circular. + + Methods: + `__str__()`: Return the string representation of the list. + `__len__()`: Return the length of the list. + `__iter__()`: Return an iterator for the list. + `iscircular()`: Check if the list is circular. + `_link(node1: Type[T], node2: Type[T])`: Link two nodes. + `_preprocess_index(index: int)`: Preprocess the index. + `insert(data: any, index: int)`: Insert data at the specified index. + `append(data: any)`: Append data to the end of the list. + `appendleft(data: any)`: Append data to the beginning of the list. + `remove(index: int)`: Remove data at the specified index. + `pop()`: Remove data from the end of the list. + `popleft()`: Remove data from the beginning of the list. + `get(index: int)`: Get data at the specified index. + `index(data: any)`: Get the index of the specified data. + `clear()`: Clear the list. + ''' + + __head: Type[T] = None + __tail: Type[T] = None + __length: int = 0 + __circular: bool = False + + def __init__(self, cls: Type[T], circular: bool = False): + ''' + Initialize the LinkedList. + + Args: + circular (bool): Whether the list is circular. + ''' + + self.__cls = cls + self.__head = None + self.__tail = None + self.__length = 0 + self.__circular = circular + + def __str__(self): + return f"{self.__class__.__name__}({self.__head})" + + def __len__(self): + return self.__length + + def __iter__(self): + ''' + Return an iterator for the list. + + Returns: + out (Iterator): The iterator for the list. + + Yields: + out (any): The data in the list. + ''' + + current_node = self.__head + while current_node is not None: + yield current_node.data + current_node = current_node.next + + @abstractmethod + def _link(self, node1: T, node2: T): + ''' + Link two nodes. + + Args: + node1 (Type[T]): The first node. + node2 (Type[T]): The second node. + ''' + + + def iscircular(self): + ''' + Check if the list is circular. + + Returns: + out (bool): Whether the list is circular. + ''' + + return self.__circular + + def _preprocess_index(self, index: int, insert: bool = False): + ''' + Preprocess the index. + + Args: + index (int): The index to preprocess. + insert (bool): Whether the index is for insertion. + Returns: + out (int): The preprocessed index. + + Raises: + IndexError: If the index is out of bounds. + ''' + + ins = 1 if insert else 0 + + if index < 0: + index = self.__length + index + ins + + if index < 0 or ((1 - ins + index) > self.__length): + raise IndexError("Index out of bounds") + + return index + + def insert(self, data: any, index: int): + ''' + Insert data at the specified index. + + Args: + data (any): The data to be inserted. + index (int): The index to insert the data. + + Raises: + IndexError: If the index is out of bounds. + ''' + + index = self._preprocess_index(index, insert=True) + + if index == 0: + self.appendleft(data) + return + + if index == self.__length: + self.append(data) + return + + new_node = self.__cls(data) + + current_node = self.__head + for _ in range(index - 1): + current_node = current_node.next + + self._link(new_node, current_node.next) + self._link(current_node, new_node) + + self.__length += 1 + + def append(self, data: any): + ''' + Append data to the end of the list. + + Args: + data (any): The data to be appended. + ''' + + new_node = self.__cls(data) + + if self.__length == 0: + self.__head = new_node + self.__tail = new_node + + else: + self._link(self.__tail, new_node) + self.__tail = new_node + + if self.__circular: + self._link(self.__tail, self.__head) + + self.__length += 1 + + def appendleft(self, data: any): + ''' + Append data to the beginning of the list. + + Args: + data (any): The data to be appended. + ''' + + new_node = self.__cls(data) + + if self.__length == 0: + self.__head = new_node + self.__tail = new_node + + else: + self._link(new_node, self.__head) + self.__head = new_node + + if self.__circular: + self._link(self.__tail, self.__head) + + self.__length += 1 + + def remove(self, index: int): + ''' + Remove data at the specified index. + + Args: + index (int): The index to remove the data. + + Returns: + out (any): The removed data. + + Raises: + IndexError: If the index is out of bounds. + ''' + + index = self._preprocess_index(index) + + if index == 0: + return self.popleft() + + if index == self.__length - 1: + return self.pop() + + current_node = self.__head + for _ in range(index - 1): + current_node = current_node.next + + removed_node = current_node.next + self._link(current_node, removed_node.next) + removed_node.next = None + + self.__length -= 1 + + return removed_node.data + + def pop(self): + ''' + Extract data from the end of the list. + + Returns: + out (any): The extracted data. + + Raises: + IndexError: If the list is empty. + ''' + + if self.__length == 0: + raise IndexError("Empty list") + + if self.__length == 1: + removed_node = self.__head + self.__head = None + self.__tail = None + + else: + current_node = self.__head + while current_node.next != self.__tail: + current_node = current_node.next + + removed_node = self.__tail + self._link(current_node, removed_node.next) + self.__tail = current_node + + self.__length -= 1 + + return removed_node.data + + def popleft(self): + ''' + Extract data from the beginning of the list. + + Returns: + out (any): The extracted data. + + Raises: + IndexError: If the list is empty. + ''' + + if self.__length == 0: + raise IndexError("Empty list") + + removed_node = self.__head + + if self.__length == 1: + self.__head = None + self.__tail = None + + else: + self.__head = self.__head.next + + if self.__circular: + self._link(self.__tail, self.__head) + + self.__length -= 1 + + return removed_node.data + + def get(self, index: int): + ''' + Get data at the specified index. + + Args: + index (int): The index to get the data. + + Returns: + out (any): The data at the specified index. + + Raises: + IndexError: If the index is out of bounds. + ''' + + index = self._preprocess_index(index) + + current_node = self.__head + for _ in range(index): + current_node = current_node.next + + return current_node.data + + def index(self, data: any): + ''' + Get the index of the first occurrence of the specified data. + + Args: + data (any): The data to search for. + + Returns: + out (int): The index of the specified data. + + Raises: + ValueError: If the data is not found. + ''' + + current_node = self.__head + for i in range(self.__length): + if current_node.data == data: + return i + + current_node = current_node.next + + raise ValueError("Data not found") + + def clear(self): + ''' + Remove all data from the list. + ''' + + self.__head = None + self.__tail = None + self.__length = 0 diff --git a/pystrukts/list/singly_linked_list.py b/pystrukts/list/singly_linked_list.py index f5da7bc..5163e2b 100644 --- a/pystrukts/list/singly_linked_list.py +++ b/pystrukts/list/singly_linked_list.py @@ -1,348 +1,70 @@ -''' -Singly Linked List Module. - -This module implements a Singly Linked List and Node data structures. -''' - -from dataclasses import dataclass - -@dataclass -class SinglyLinkedNode: - ''' - SinglyLinkedNode Class. - - Attributes: - data (any): The data stored in the node. - next (SinglyLinkedNode): The next node in the list. - - Methods: - `__str__()`: Return the string representation of the node. - ''' - - data: any = None - next: 'SinglyLinkedNode' = None - - def __str__(self): - return f"SinglyLinkedNode({self.data}){' -> ' + str(self.next) if self.next else ''}" - -@dataclass -class SinglyLinkedList: - ''' - SinglyLinkedList Class. - - Attributes: - __head (SinglyLinkedNode): The head of the list. - __tail (SinglyLinkedNode): The tail of the list. - __length (int): The length of the list. - __circular (bool): Whether the list is circular. - - Methods: - `__str__()`: Return the string representation of the list. - `__len__()`: Return the length of the list. - `__iter__()`: Return an iterator for the list. - `__next__()`: Return the next element in the list. - `iscircular()`: Check if the list is circular. - `insert(data: any, index: int)`: Insert data at the specified index. - `append(data: any)`: Append data to the end of the list. - `appendleft(data: any)`: Append data to the beginning of the list. - `remove(index: int)`: Remove data at the specified index. - `pop()`: Remove data from the end of the list. - `popleft()`: Remove data from the beginning of the list. - `get(index: int)`: Get data at the specified index. - `index(data: any)`: Get the index of the specified data. - `clear()`: Clear the list. - ''' - - __head: 'SinglyLinkedNode' = None - __tail: 'SinglyLinkedNode' = None - __length: int = 0 - __circular: bool = False - - def __init__(self, circular: bool = False): - ''' - Initialize the SinglyLinkedList. - - Args: - circular (bool): Whether the list is circular. - - Returns: - out (SinglyLinkedList): The SinglyLinkedList instance. - ''' - - self.__head = None - self.__tail = None - self.__length = 0 - self.__circular = circular - - def __str__(self): - return f"SinglyLinkedList({self.__head})" - - def __len__(self): - return self.__length - - def __iter__(self): - ''' - Return an iterator for the list. - - Returns: - out (Iterator): The iterator for the list. - - Yields: - out (any): The data in the list. - ''' - - current_node = self.__head - while current_node is not None: - yield current_node.data - current_node = current_node.next - - def iscircular(self): - ''' - Check if the list is circular. - - Returns: - out (bool): Whether the list is circular. - ''' - - return self.__circular - - def insert(self, data: any, index: int): - ''' - Insert data at the specified index. - - Args: - data (any): The data to be inserted. - index (int): The index to insert the data. - - Raises: - IndexError: If the index is out of bounds. - ''' - - if index < 0: - index = self.__length + index + 1 - - if index < 0 or index > self.__length: - raise IndexError("Index out of bounds") - - if index == 0: - self.appendleft(data) - return - - if index == self.__length: - self.append(data) - return - - new_node = SinglyLinkedNode(data) - - current_node = self.__head - for _ in range(index - 1): - current_node = current_node.next - - new_node.next = current_node.next - current_node.next = new_node - - self.__length += 1 - - def append(self, data: any): - ''' - Append data to the end of the list. - - Args: - data (any): The data to be appended. - ''' - - new_node = SinglyLinkedNode(data) - - if self.__length == 0: - self.__head = new_node - self.__tail = new_node - - else: - self.__tail.next = new_node - self.__tail = new_node - - if self.__circular: - self.__tail.next = self.__head - - self.__length += 1 - - def appendleft(self, data: any): - ''' - Append data to the beginning of the list. - - Args: - data (any): The data to be appended. - ''' - - new_node = SinglyLinkedNode(data) - - if self.__length == 0: - self.__head = new_node - self.__tail = new_node - - else: - new_node.next = self.__head - self.__head = new_node - - if self.__circular: - self.__tail.next = self.__head - - self.__length += 1 - - def remove(self, index: int): - ''' - Remove data at the specified index. - - Args: - index (int): The index to remove the data. - - Returns: - out (any): The removed data. - - Raises: - IndexError: If the index is out of bounds. - ''' - - if index < 0: - index = self.__length + index - - if index < 0 or index >= self.__length: - raise IndexError("Index out of bounds") - - if index == 0: - return self.popleft() - - if index == self.__length - 1: - return self.pop() - - current_node = self.__head - for _ in range(index - 1): - current_node = current_node.next - - removed_node = current_node.next - current_node.next = removed_node.next - removed_node.next = None - - self.__length -= 1 - - return removed_node.data - - def pop(self): - ''' - Extract data from the end of the list. - - Returns: - out (any): The extracted data. - - Raises: - IndexError: If the list is empty. - ''' - - if self.__length == 0: - raise IndexError("Empty list") - - if self.__length == 1: - removed_node = self.__head - self.__head = None - self.__tail = None - - else: - current_node = self.__head - while current_node.next != self.__tail: - current_node = current_node.next - - removed_node = self.__tail - current_node.next = removed_node.next - self.__tail = current_node - - self.__length -= 1 - - return removed_node.data - - def popleft(self): - ''' - Extract data from the beginning of the list. - - Returns: - out (any): The extracted data. - - Raises: - IndexError: If the list is empty. - ''' - - if self.__length == 0: - raise IndexError("Empty list") - - removed_node = self.__head - - if self.__length == 1: - self.__head = None - self.__tail = None - - else: - self.__head = self.__head.next - - if self.__circular: - self.__tail.next = self.__head - - self.__length -= 1 - - return removed_node.data - - def get(self, index: int): - ''' - Get data at the specified index. - - Args: - index (int): The index to get the data. - - Returns: - out (any): The data at the specified index. - - Raises: - IndexError: If the index is out of bounds. - ''' - - if index < 0: - index = self.__length + index - - if index < 0 or index >= self.__length: - raise IndexError("Index out of bounds") - - current_node = self.__head - for _ in range(index): - current_node = current_node.next - - return current_node.data - - def index(self, data: any): - ''' - Get the index of the first occurrence of the specified data. - - Args: - data (any): The data to search for. - - Returns: - out (int): The index of the specified data. - - Raises: - ValueError: If the data is not found. - ''' - - current_node = self.__head - for i in range(self.__length): - if current_node.data == data: - return i - - current_node = current_node.next - - raise ValueError("Data not found") - - def clear(self): - ''' - Remove all data from the list. - ''' - - self.__head = None - self.__tail = None - self.__length = 0 +''' +Singly Linked List Module. + +This module implements a Singly Linked List and Node data structures. +''' + +from dataclasses import dataclass +from .linked_list import LinkedList + +@dataclass +class SinglyLinkedNode: + ''' + SinglyLinkedNode Class. + + Attributes: + data (any): The data stored in the node. + next (SinglyLinkedNode): The next node in the list. + + Methods: + `__str__()`: Return the string representation of the node. + ''' + + data: any = None + next: 'SinglyLinkedNode' = None + + def __str__(self): + return f"SinglyLinkedNode({self.data}){' -> ' + str(self.next) if self.next else ''}" + + +class SinglyLinkedList(LinkedList[SinglyLinkedNode]): + ''' + SinglyLinkedList Class. + + Attributes: + __head (SinglyLinkedNode): The head of the list. + __tail (SinglyLinkedNode): The tail of the list. + __length (int): The length of the list. + __circular (bool): Whether the list is circular. + + Methods: + `__str__()`: Return the string representation of the list. + `__len__()`: Return the length of the list. + `__iter__()`: Return an iterator for the list. + `iscircular()`: Check if the list is circular. + `_link(node1: Type[T], node2: Type[T])`: Link two nodes. + `_preprocess_index(index: int)`: Preprocess the index. + `insert(data: any, index: int)`: Insert data at the specified index. + `append(data: any)`: Append data to the end of the list. + `appendleft(data: any)`: Append data to the beginning of the list. + `remove(index: int)`: Remove data at the specified index. + `pop()`: Remove data from the end of the list. + `popleft()`: Remove data from the beginning of the list. + `get(index: int)`: Get data at the specified index. + `index(data: any)`: Get the index of the specified data. + `clear()`: Clear the list. + ''' + + def __init__(self, circular: bool = False): + super().__init__(SinglyLinkedNode, circular) + + def _link(self, node1: SinglyLinkedNode, node2: SinglyLinkedNode): + ''' + Link two nodes. + + Args: + node1 (SinglyLinkedNode): The first node. + node2 (SinglyLinkedNode): The second node. + ''' + + node1.next = node2 diff --git a/tests/test_doubly_linked_list.py b/tests/test_doubly_linked_list.py index d6bfaff..771e3a4 100644 --- a/tests/test_doubly_linked_list.py +++ b/tests/test_doubly_linked_list.py @@ -36,8 +36,8 @@ def test_list_initialization(): assert len(linked_list) == 0 assert linked_list.iscircular() == False - assert linked_list._DoublyLinkedList__head is None - assert linked_list._DoublyLinkedList__tail is None + assert linked_list._LinkedList__head is None + assert linked_list._LinkedList__tail is None def test_list_str(): linked_list = DoublyLinkedList() @@ -83,7 +83,7 @@ def test_list_append_multiple(): assert linked_list.get(0) == 1 assert linked_list.get(1) == 2 assert linked_list.get(2) == 3 - assert linked_list._DoublyLinkedList__tail.prev.prev == linked_list._DoublyLinkedList__head + assert linked_list._LinkedList__tail.prev.prev == linked_list._LinkedList__head def test_list_prepend(): linked_list = DoublyLinkedList() @@ -102,7 +102,7 @@ def test_list_prepend_multiple(): assert linked_list.get(0) == 3 assert linked_list.get(1) == 2 assert linked_list.get(2) == 1 - assert linked_list._DoublyLinkedList__tail.prev.prev == linked_list._DoublyLinkedList__head + assert linked_list._LinkedList__tail.prev.prev == linked_list._LinkedList__head def test_insert(): linked_list = DoublyLinkedList() @@ -116,7 +116,7 @@ def test_insert(): assert linked_list.get(1) == 2 assert linked_list.get(2) == 3 assert linked_list.get(3) == 4 - assert linked_list._DoublyLinkedList__tail.prev.prev.prev == linked_list._DoublyLinkedList__head + assert linked_list._LinkedList__tail.prev.prev.prev == linked_list._LinkedList__head def test_insert_start(): linked_list = DoublyLinkedList() @@ -175,8 +175,8 @@ def test_pop_last(): assert linked_list.pop() == 1 assert len(linked_list) == 0 - assert linked_list._DoublyLinkedList__head is None - assert linked_list._DoublyLinkedList__tail is None + assert linked_list._LinkedList__head is None + assert linked_list._LinkedList__tail is None def test_pop_empty(): linked_list = DoublyLinkedList() @@ -193,7 +193,7 @@ def test_popleft(): assert linked_list.popleft() == 1 assert len(linked_list) == 2 assert linked_list.get(0) == 2 - assert linked_list._DoublyLinkedList__head.data == 2 + assert linked_list._LinkedList__head.data == 2 def test_popleft_last(): linked_list = DoublyLinkedList() @@ -201,8 +201,8 @@ def test_popleft_last(): assert linked_list.popleft() == 1 assert len(linked_list) == 0 - assert linked_list._DoublyLinkedList__head is None - assert linked_list._DoublyLinkedList__tail is None + assert linked_list._LinkedList__head is None + assert linked_list._LinkedList__tail is None def test_popleft_empty(): linked_list = DoublyLinkedList() @@ -222,7 +222,7 @@ def test_remove(): assert linked_list.get(0) == 1 assert linked_list.get(1) == 2 assert linked_list.get(2) == 4 - assert linked_list._DoublyLinkedList__tail.prev.prev == linked_list._DoublyLinkedList__head + assert linked_list._LinkedList__tail.prev.prev == linked_list._LinkedList__head def test_remove_start(): linked_list = DoublyLinkedList() @@ -234,7 +234,7 @@ def test_remove_start(): assert len(linked_list) == 2 assert linked_list.get(0) == 2 assert linked_list.get(1) == 3 - assert linked_list._DoublyLinkedList__tail.prev == linked_list._DoublyLinkedList__head + assert linked_list._LinkedList__tail.prev == linked_list._LinkedList__head def test_remove_end(): linked_list = DoublyLinkedList() @@ -246,7 +246,7 @@ def test_remove_end(): assert len(linked_list) == 2 assert linked_list.get(0) == 1 assert linked_list.get(1) == 2 - assert linked_list._DoublyLinkedList__tail.prev == linked_list._DoublyLinkedList__head + assert linked_list._LinkedList__tail.prev == linked_list._LinkedList__head def test_remove_negative_index(): linked_list = DoublyLinkedList() @@ -341,10 +341,10 @@ def test_append_circular(): linked_list.append(2) linked_list.append(3) - assert linked_list._DoublyLinkedList__head.data == 1 - assert linked_list._DoublyLinkedList__tail.data == 3 - assert linked_list._DoublyLinkedList__tail.next == linked_list._DoublyLinkedList__head - assert linked_list._DoublyLinkedList__head.prev == linked_list._DoublyLinkedList__tail + assert linked_list._LinkedList__head.data == 1 + assert linked_list._LinkedList__tail.data == 3 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head + assert linked_list._LinkedList__head.prev == linked_list._LinkedList__tail def test_appendleft_circular(): linked_list = DoublyLinkedList(circular=True) @@ -352,10 +352,10 @@ def test_appendleft_circular(): linked_list.appendleft(2) linked_list.appendleft(3) - assert linked_list._DoublyLinkedList__head.data == 3 - assert linked_list._DoublyLinkedList__tail.data == 1 - assert linked_list._DoublyLinkedList__tail.next == linked_list._DoublyLinkedList__head - assert linked_list._DoublyLinkedList__head.prev == linked_list._DoublyLinkedList__tail + assert linked_list._LinkedList__head.data == 3 + assert linked_list._LinkedList__tail.data == 1 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head + assert linked_list._LinkedList__head.prev == linked_list._LinkedList__tail def test_pop_circular(): linked_list = DoublyLinkedList(circular=True) @@ -364,10 +364,10 @@ def test_pop_circular(): linked_list.append(3) assert linked_list.pop() == 3 - assert linked_list._DoublyLinkedList__head.data == 1 - assert linked_list._DoublyLinkedList__tail.data == 2 - assert linked_list._DoublyLinkedList__tail.next == linked_list._DoublyLinkedList__head - assert linked_list._DoublyLinkedList__head.prev == linked_list._DoublyLinkedList__tail + assert linked_list._LinkedList__head.data == 1 + assert linked_list._LinkedList__tail.data == 2 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head + assert linked_list._LinkedList__head.prev == linked_list._LinkedList__tail def test_popleft_circular(): linked_list = DoublyLinkedList(circular=True) @@ -376,7 +376,7 @@ def test_popleft_circular(): linked_list.append(3) assert linked_list.popleft() == 1 - assert linked_list._DoublyLinkedList__head.data == 2 - assert linked_list._DoublyLinkedList__tail.data == 3 - assert linked_list._DoublyLinkedList__tail.next == linked_list._DoublyLinkedList__head - assert linked_list._DoublyLinkedList__head.prev == linked_list._DoublyLinkedList__tail \ No newline at end of file + assert linked_list._LinkedList__head.data == 2 + assert linked_list._LinkedList__tail.data == 3 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head + assert linked_list._LinkedList__head.prev == linked_list._LinkedList__tail \ No newline at end of file diff --git a/tests/test_singly_linked_list.py b/tests/test_singly_linked_list.py index a40991e..791c411 100644 --- a/tests/test_singly_linked_list.py +++ b/tests/test_singly_linked_list.py @@ -34,8 +34,8 @@ def test_list_initialization(): assert len(linked_list) == 0 assert linked_list.iscircular() == False - assert linked_list._SinglyLinkedList__head is None - assert linked_list._SinglyLinkedList__tail is None + assert linked_list._LinkedList__head is None + assert linked_list._LinkedList__tail is None def test_list_str(): linked_list = SinglyLinkedList() @@ -170,8 +170,8 @@ def test_pop_last(): assert linked_list.pop() == 1 assert len(linked_list) == 0 - assert linked_list._SinglyLinkedList__head is None - assert linked_list._SinglyLinkedList__tail is None + assert linked_list._LinkedList__head is None + assert linked_list._LinkedList__tail is None def test_pop_empty(): linked_list = SinglyLinkedList() @@ -195,8 +195,8 @@ def test_popleft_last(): assert linked_list.popleft() == 1 assert len(linked_list) == 0 - assert linked_list._SinglyLinkedList__head is None - assert linked_list._SinglyLinkedList__tail is None + assert linked_list._LinkedList__head is None + assert linked_list._LinkedList__tail is None def test_popleft_empty(): linked_list = SinglyLinkedList() @@ -332,9 +332,9 @@ def test_append_circular(): linked_list.append(2) linked_list.append(3) - assert linked_list._SinglyLinkedList__head.data == 1 - assert linked_list._SinglyLinkedList__tail.data == 3 - assert linked_list._SinglyLinkedList__tail.next == linked_list._SinglyLinkedList__head + assert linked_list._LinkedList__head.data == 1 + assert linked_list._LinkedList__tail.data == 3 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head def test_appendleft_circular(): linked_list = SinglyLinkedList(circular=True) @@ -342,9 +342,9 @@ def test_appendleft_circular(): linked_list.appendleft(2) linked_list.appendleft(3) - assert linked_list._SinglyLinkedList__head.data == 3 - assert linked_list._SinglyLinkedList__tail.data == 1 - assert linked_list._SinglyLinkedList__tail.next == linked_list._SinglyLinkedList__head + assert linked_list._LinkedList__head.data == 3 + assert linked_list._LinkedList__tail.data == 1 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head def test_pop_circular(): linked_list = SinglyLinkedList(circular=True) @@ -353,9 +353,9 @@ def test_pop_circular(): linked_list.append(3) assert linked_list.pop() == 3 - assert linked_list._SinglyLinkedList__head.data == 1 - assert linked_list._SinglyLinkedList__tail.data == 2 - assert linked_list._SinglyLinkedList__tail.next == linked_list._SinglyLinkedList__head + assert linked_list._LinkedList__head.data == 1 + assert linked_list._LinkedList__tail.data == 2 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head def test_popleft_circular(): linked_list = SinglyLinkedList(circular=True) @@ -364,6 +364,6 @@ def test_popleft_circular(): linked_list.append(3) assert linked_list.popleft() == 1 - assert linked_list._SinglyLinkedList__head.data == 2 - assert linked_list._SinglyLinkedList__tail.data == 3 - assert linked_list._SinglyLinkedList__tail.next == linked_list._SinglyLinkedList__head \ No newline at end of file + assert linked_list._LinkedList__head.data == 2 + assert linked_list._LinkedList__tail.data == 3 + assert linked_list._LinkedList__tail.next == linked_list._LinkedList__head \ No newline at end of file From 8e1f3bccee7833465fc634c9bd51177cdc6c70f9 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Mon, 14 Oct 2024 11:14:25 +0200 Subject: [PATCH 03/13] docs: change typing Change from any function to typing.Any --- pystrukts/list/doubly_linked_list.py | 13 +++++----- pystrukts/list/linked_list.py | 36 ++++++++++++++-------------- pystrukts/list/singly_linked_list.py | 13 +++++----- pystrukts/max_heap.py | 11 +++++---- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/pystrukts/list/doubly_linked_list.py b/pystrukts/list/doubly_linked_list.py index c3b1635..0318d81 100644 --- a/pystrukts/list/doubly_linked_list.py +++ b/pystrukts/list/doubly_linked_list.py @@ -5,6 +5,7 @@ ''' from dataclasses import dataclass +from typing import Any from .linked_list import LinkedList @dataclass @@ -13,7 +14,7 @@ class DoublyLinkedNode: DoublyLinkedNode Class. Attributes: - data (any): The data stored in the node. + data (Any): The data stored in the node. prev (DoublyLinkedNode): The previous node in the list. next (DoublyLinkedNode): The next node in the list. @@ -21,7 +22,7 @@ class DoublyLinkedNode: `__str__()`: Return the string representation of the node. ''' - data: any = None + data: Any = None prev: 'DoublyLinkedNode' = None next: 'DoublyLinkedNode' = None @@ -46,14 +47,14 @@ class DoublyLinkedList(LinkedList[DoublyLinkedNode]): `iscircular()`: Check if the list is circular. `_link(node1: Type[T], node2: Type[T])`: Link two nodes. `_preprocess_index(index: int)`: Preprocess the index. - `insert(data: any, index: int)`: Insert data at the specified index. - `append(data: any)`: Append data to the end of the list. - `appendleft(data: any)`: Append data to the beginning of the list. + `insert(data: Any, index: int)`: Insert data at the specified index. + `append(data: Any)`: Append data to the end of the list. + `appendleft(data: Any)`: Append data to the beginning of the list. `remove(index: int)`: Remove data at the specified index. `pop()`: Remove data from the end of the list. `popleft()`: Remove data from the beginning of the list. `get(index: int)`: Get data at the specified index. - `index(data: any)`: Get the index of the specified data. + `index(data: Any)`: Get the index of the specified data. `clear()`: Clear the list. ''' diff --git a/pystrukts/list/linked_list.py b/pystrukts/list/linked_list.py index 18824ca..5d831b7 100644 --- a/pystrukts/list/linked_list.py +++ b/pystrukts/list/linked_list.py @@ -6,7 +6,7 @@ from dataclasses import dataclass from abc import ABC, abstractmethod -from typing import Type, TypeVar, Generic +from typing import Type, TypeVar, Generic, Any T = TypeVar('T') @@ -28,14 +28,14 @@ class LinkedList(ABC, Generic[T]): `iscircular()`: Check if the list is circular. `_link(node1: Type[T], node2: Type[T])`: Link two nodes. `_preprocess_index(index: int)`: Preprocess the index. - `insert(data: any, index: int)`: Insert data at the specified index. - `append(data: any)`: Append data to the end of the list. - `appendleft(data: any)`: Append data to the beginning of the list. + `insert(data: Any, index: int)`: Insert data at the specified index. + `append(data: Any)`: Append data to the end of the list. + `appendleft(data: Any)`: Append data to the beginning of the list. `remove(index: int)`: Remove data at the specified index. `pop()`: Remove data from the end of the list. `popleft()`: Remove data from the beginning of the list. `get(index: int)`: Get data at the specified index. - `index(data: any)`: Get the index of the specified data. + `index(data: Any)`: Get the index of the specified data. `clear()`: Clear the list. ''' @@ -72,7 +72,7 @@ def __iter__(self): out (Iterator): The iterator for the list. Yields: - out (any): The data in the list. + out (Any): The data in the list. ''' current_node = self.__head @@ -125,12 +125,12 @@ def _preprocess_index(self, index: int, insert: bool = False): return index - def insert(self, data: any, index: int): + def insert(self, data: Any, index: int): ''' Insert data at the specified index. Args: - data (any): The data to be inserted. + data (Any): The data to be inserted. index (int): The index to insert the data. Raises: @@ -158,12 +158,12 @@ def insert(self, data: any, index: int): self.__length += 1 - def append(self, data: any): + def append(self, data: Any): ''' Append data to the end of the list. Args: - data (any): The data to be appended. + data (Any): The data to be appended. ''' new_node = self.__cls(data) @@ -181,12 +181,12 @@ def append(self, data: any): self.__length += 1 - def appendleft(self, data: any): + def appendleft(self, data: Any): ''' Append data to the beginning of the list. Args: - data (any): The data to be appended. + data (Any): The data to be appended. ''' new_node = self.__cls(data) @@ -212,7 +212,7 @@ def remove(self, index: int): index (int): The index to remove the data. Returns: - out (any): The removed data. + out (Any): The removed data. Raises: IndexError: If the index is out of bounds. @@ -243,7 +243,7 @@ def pop(self): Extract data from the end of the list. Returns: - out (any): The extracted data. + out (Any): The extracted data. Raises: IndexError: If the list is empty. @@ -275,7 +275,7 @@ def popleft(self): Extract data from the beginning of the list. Returns: - out (any): The extracted data. + out (Any): The extracted data. Raises: IndexError: If the list is empty. @@ -308,7 +308,7 @@ def get(self, index: int): index (int): The index to get the data. Returns: - out (any): The data at the specified index. + out (Any): The data at the specified index. Raises: IndexError: If the index is out of bounds. @@ -322,12 +322,12 @@ def get(self, index: int): return current_node.data - def index(self, data: any): + def index(self, data: Any): ''' Get the index of the first occurrence of the specified data. Args: - data (any): The data to search for. + data (Any): The data to search for. Returns: out (int): The index of the specified data. diff --git a/pystrukts/list/singly_linked_list.py b/pystrukts/list/singly_linked_list.py index 5163e2b..3592ab3 100644 --- a/pystrukts/list/singly_linked_list.py +++ b/pystrukts/list/singly_linked_list.py @@ -5,6 +5,7 @@ ''' from dataclasses import dataclass +from typing import Any from .linked_list import LinkedList @dataclass @@ -13,14 +14,14 @@ class SinglyLinkedNode: SinglyLinkedNode Class. Attributes: - data (any): The data stored in the node. + data (Any): The data stored in the node. next (SinglyLinkedNode): The next node in the list. Methods: `__str__()`: Return the string representation of the node. ''' - data: any = None + data: Any = None next: 'SinglyLinkedNode' = None def __str__(self): @@ -44,14 +45,14 @@ class SinglyLinkedList(LinkedList[SinglyLinkedNode]): `iscircular()`: Check if the list is circular. `_link(node1: Type[T], node2: Type[T])`: Link two nodes. `_preprocess_index(index: int)`: Preprocess the index. - `insert(data: any, index: int)`: Insert data at the specified index. - `append(data: any)`: Append data to the end of the list. - `appendleft(data: any)`: Append data to the beginning of the list. + `insert(data: Any, index: int)`: Insert data at the specified index. + `append(data: Any)`: Append data to the end of the list. + `appendleft(data: Any)`: Append data to the beginning of the list. `remove(index: int)`: Remove data at the specified index. `pop()`: Remove data from the end of the list. `popleft()`: Remove data from the beginning of the list. `get(index: int)`: Get data at the specified index. - `index(data: any)`: Get the index of the specified data. + `index(data: Any)`: Get the index of the specified data. `clear()`: Clear the list. ''' diff --git a/pystrukts/max_heap.py b/pystrukts/max_heap.py index 6591843..d9f0205 100644 --- a/pystrukts/max_heap.py +++ b/pystrukts/max_heap.py @@ -6,6 +6,7 @@ ''' from dataclasses import field, dataclass +from typing import Any import heapq @dataclass @@ -19,7 +20,7 @@ class MaxHeap: Methods: `__post_init__()`: Creates the heap from the data. `__str__()`: Return the string representation of the heap. - `push(value: any)`: Insert a value in the heap. + `push(value: Any)`: Insert a value in the heap. `pop()`: Extract the maximum value from the heap. `peek()`: Peek the maximum value in the heap. ''' @@ -36,12 +37,12 @@ def __post_init__(self): def __str__(self): return f"MaxHeap({[-val for val in self.data]})" - def push(self, value: any): + def push(self, value: Any): ''' Insert a value in the heap. Args: - value (any): The value to be inserted. + value (Any): The value to be inserted. Raises: TypeError: If the value is not comparable. @@ -54,7 +55,7 @@ def pop(self): Extract the maximum value from the heap. Returns: - out (any): The maximum value in the heap. + out (Any): The maximum value in the heap. Raises: IndexError: If the heap is empty. @@ -66,6 +67,6 @@ def peek(self): Peek the maximum value in the heap. Returns: - out (any): The maximum value in the heap or `None` if the heap is empty. + out (Any): The maximum value in the heap or `None` if the heap is empty. ''' return -self.data[0] From 7c62cc74a3f917001a7c698feeaa6186eb3c2b86 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Mon, 14 Oct 2024 12:04:58 +0200 Subject: [PATCH 04/13] docs: add installation cmd, organize readme Add section for the pip command and structure the modules by type. --- README.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 500ee1a..044caec 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,25 @@ -A Python library for data structures. This library provides more than 20 advanced data structures not available in the Python standard library, including: +A Python library for data structures. This library provides more than 20 advanced data structures not available in the Python standard library. +## Installation + +```bash +pip install pystrukts +``` + +## Data Structures +### Lists +- [x] Singly Linked Node - [x] Singly Linked List +- [x] Doubly Linked Node - [x] Doubly Linked List - [x] Circular Linked List (Single, Double) +- [ ] Skip List + +### Trees +- [ ] Binary Tree - [ ] Generic Tree - [ ] Binary Search Tree - [ ] Ternary Search Tree @@ -24,9 +38,12 @@ A Python library for data structures. This library provides more than 20 advance - [ ] Interval Tree - [ ] Segment Tree - [ ] Fenwick Tree -- [x] Disjoint Set -- [ ] Skip List - [x] Trie + +### Graphs +- [x] Adjacency Matrix +- [x] Adjacency List + +### Others +- [x] Disjoint Set (Union Find) - [x] Max Heap -- [x] Graph (Adjacency Matrix) -- [x] Graph (Adjacency List) From 8a18c03dd9cff854624dbe462ec6996a100951d1 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Mon, 14 Oct 2024 12:06:01 +0200 Subject: [PATCH 05/13] docs: add dynamic fields in pyproject.toml Fetch description, authors, urls and keywords dynamically. --- pyproject.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 002c0d3..b17b659 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,9 @@ build-backend = "setuptools.build_meta" [project] name = "pystrukts" -description = "Advanced data structures for Python." readme = "README.md" requires-python = ">=3.8" -dynamic = ["version"] +dynamic = ["version", "description", "authors", "urls", "keywords"] license = { file = "LICENSE" } classifiers = [ "License :: OSI Approved :: Apache Software License", @@ -22,10 +21,6 @@ classifiers = [ dependencies = [ ] -authors = [ - { name = "Rubén Pérez Mercado", email = "rubenpermerc@gmail.com" } -] - [tool.setuptools_scm] write_to = "pystrukts/_version.py" From 899661efa17bc383576a8ed22ed7c275884be563 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Mon, 14 Oct 2024 19:46:40 +0200 Subject: [PATCH 06/13] feat: add generic tree Add generic tree implementation and tests --- README.md | 2 +- pystrukts/tree/generic_tree.py | 94 +++++++++++++++++++++++ tests/test_generic_tree.py | 135 +++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 pystrukts/tree/generic_tree.py create mode 100644 tests/test_generic_tree.py diff --git a/README.md b/README.md index 044caec..27c497d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ pip install pystrukts ### Trees - [ ] Binary Tree -- [ ] Generic Tree +- [x] Generic Tree - [ ] Binary Search Tree - [ ] Ternary Search Tree - [ ] Suffix Tree diff --git a/pystrukts/tree/generic_tree.py b/pystrukts/tree/generic_tree.py new file mode 100644 index 0000000..fec09f9 --- /dev/null +++ b/pystrukts/tree/generic_tree.py @@ -0,0 +1,94 @@ +''' +Generic tree module. + +This module contains the implementation of a generic tree with an optional number of children. +''' + +from dataclasses import dataclass +from warnings import warn +from typing import Any + +@dataclass +class GenericTree: + ''' + Generic tree class. + + This class represents a generic tree with an optional number of children. + ''' + def __init__(self, data: Any, max_children: int = 0): + ''' + Initializes the tree. If max_children is less than 2, there are no restrictions on the number of children. + + Args: + data (Any): The data of the root node. + max_children (int): The maximum number of children the node can have. + ''' + self.data = data + self.children = [] + + if max_children == 2: + warn("Binary trees are better represented using the BinaryTree class.") + + self.max_children = int(max_children) if max_children > 1 else None + + def n_children(self) -> int: + ''' + Returns the number of children of the node. + + Returns: + int: The number of children of the node. + ''' + return len(self.children) + + def add_child(self, data: Any) -> None: + ''' + Adds a child to the node. + + Args: + child (GenericTree): The child to be added. + ''' + + if self.max_children is not None and self.n_children() >= self.max_children: + warn(f"Child not appended as node already has {self.max_children} children.") + else: + new_node = GenericTree(data, self.max_children) + self.children.append(new_node) + + def remove_child(self, index: int) -> None: + ''' + Removes a child from the node. + + Args: + index (int): The index of the child to be removed. + ''' + self.children.pop(index) + + def get_children(self) -> list: + ''' + Returns the children of the node. + + Returns: + list: The children of the node. + ''' + return self.children.copy() + + def get_child(self, index: int) -> 'GenericTree': + ''' + Returns a child of the node. + + Args: + index (int): The index of the child to be returned. + + Returns: + GenericTree: The child of the node. + ''' + return self.children[index] + + def is_leaf(self) -> bool: + ''' + Returns whether the node is a leaf or not. + + Returns: + bool: True if the node is a leaf, False otherwise. + ''' + return self.n_children() == 0 diff --git a/tests/test_generic_tree.py b/tests/test_generic_tree.py new file mode 100644 index 0000000..ee083bb --- /dev/null +++ b/tests/test_generic_tree.py @@ -0,0 +1,135 @@ +import pytest +from pystrukts.tree import GenericTree + +def test_initialization(): + tree = GenericTree(1) + assert tree.data == 1 + assert tree.max_children is None + assert tree.n_children() == 0 + + tree2 = GenericTree(2, 3) + assert tree2.data == 2 + assert tree2.max_children == 3 + assert tree2.n_children() == 0 + + with pytest.warns(UserWarning): + tree3 = GenericTree(3, 2) + assert tree3.data == 3 + assert tree3.max_children == 2 + assert tree3.n_children() == 0 + +def test_initialization_error(): + with pytest.raises(TypeError): + GenericTree() + +def test_add_child(): + tree = GenericTree(1, 3) + tree.add_child(2) + assert tree.n_children() == 1 + assert tree.get_child(0).data == 2 + + tree.add_child(3) + assert tree.n_children() == 2 + assert tree.get_child(1).data == 3 + + tree.add_child(4) + assert tree.n_children() == 3 + assert tree.get_child(2).data == 4 + + with pytest.warns(UserWarning): + tree.add_child(4) + assert tree.n_children() == 3 + + tree.get_child(0).add_child(5) + assert tree.get_child(0).n_children() == 1 + assert tree.get_child(0).get_children()[0].data == 5 + +def test_remove_child(): + tree = GenericTree(1, 3) + tree.add_child(2) + tree.add_child(3) + tree.add_child(4) + + tree.remove_child(1) + assert tree.n_children() == 2 + assert tree.get_child(1).data == 4 + + tree.remove_child(0) + assert tree.n_children() == 1 + assert tree.get_child(0).data == 4 + + tree.remove_child(0) + assert tree.n_children() == 0 + +def test_get_child(): + tree = GenericTree(1, 3) + tree.add_child(2) + tree.add_child(3) + tree.add_child(4) + + assert tree.get_child(0).data == 2 + assert tree.get_child(1).data == 3 + assert tree.get_child(2).data == 4 + +def test_negative_index(): + tree = GenericTree(1, 3) + tree.add_child(2) + tree.add_child(3) + tree.add_child(4) + + tree.remove_child(-1) + assert tree.n_children() == 2 + assert tree.get_child(-1).data == 3 + +def test_remove_out_of_bounds(): + tree = GenericTree(1, 3) + tree.add_child(2) + tree.add_child(3) + tree.add_child(4) + + with pytest.raises(IndexError): + tree.remove_child(3) + + with pytest.raises(IndexError): + tree.remove_child(-4) + +def test_get_child_out_of_bounds(): + tree = GenericTree(1, 3) + tree.add_child(2) + tree.add_child(3) + tree.add_child(4) + + with pytest.raises(IndexError): + tree.get_child(3) + + with pytest.raises(IndexError): + tree.get_child(-4) + +def test_get_children(): + tree = GenericTree(1, 3) + tree.add_child(2) + tree.add_child(3) + tree.add_child(4) + + children = tree.get_children() + assert children[0].data == 2 + assert children[1].data == 3 + assert children[2].data == 4 + +def test_is_leaf(): + tree = GenericTree(1, 3) + assert tree.is_leaf() == True + + tree.add_child(2) + assert tree.is_leaf() == False + + tree.get_child(0).add_child(3) + assert tree.get_child(0).is_leaf() == False + assert tree.is_leaf() == False + + tree.get_child(0).remove_child(0) + assert tree.get_child(0).is_leaf() == True + assert tree.is_leaf() == False + + tree.remove_child(0) + assert tree.is_leaf() == True \ No newline at end of file From 361048dd8d0107d9281627aa9e836e560abc1040 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Mon, 14 Oct 2024 19:51:35 +0200 Subject: [PATCH 07/13] fix: linting generic tree --- pystrukts/tree/generic_tree.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pystrukts/tree/generic_tree.py b/pystrukts/tree/generic_tree.py index fec09f9..4a9cf95 100644 --- a/pystrukts/tree/generic_tree.py +++ b/pystrukts/tree/generic_tree.py @@ -17,7 +17,8 @@ class GenericTree: ''' def __init__(self, data: Any, max_children: int = 0): ''' - Initializes the tree. If max_children is less than 2, there are no restrictions on the number of children. + Initializes the tree. If max_children is less than 2, + there are no restrictions on the number of children. Args: data (Any): The data of the root node. @@ -71,7 +72,7 @@ def get_children(self) -> list: list: The children of the node. ''' return self.children.copy() - + def get_child(self, index: int) -> 'GenericTree': ''' Returns a child of the node. From 945ef3493f2f0a8c5f4394fd75f31180a3675226 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Tue, 15 Oct 2024 11:44:24 +0200 Subject: [PATCH 08/13] feat: binary tree Add binary tree implementation and tests --- README.md | 2 +- pystrukts/tree/binary_tree.py | 87 +++++++++++++++++++++++++++++++++++ tests/test_binary_tree.py | 55 ++++++++++++++++++++++ 3 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 pystrukts/tree/binary_tree.py create mode 100644 tests/test_binary_tree.py diff --git a/README.md b/README.md index 27c497d..8fc211d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ pip install pystrukts - [ ] Skip List ### Trees -- [ ] Binary Tree +- [x] Binary Tree - [x] Generic Tree - [ ] Binary Search Tree - [ ] Ternary Search Tree diff --git a/pystrukts/tree/binary_tree.py b/pystrukts/tree/binary_tree.py new file mode 100644 index 0000000..5966c59 --- /dev/null +++ b/pystrukts/tree/binary_tree.py @@ -0,0 +1,87 @@ +''' +Binary Tree module. + +This module contains the implementation of a binary tree. +''' + +from typing import Any + +class BinaryTree(): + ''' + Binary tree class. + + This class represents a binary tree. + ''' + def __init__(self, data: Any): + ''' + Initializes the tree. + + Args: + data (Any): The data of the root node. + ''' + self.data = data + self.__left = None + self.__right = None + + @property + def left(self) -> 'BinaryTree': + ''' + Returns the left child of the node. + + Returns: + BinaryTree: The left child of the node. + ''' + return self.__left + + @left.setter + def left(self, left: 'BinaryTree') -> None: + ''' + Sets the left child of the node. + + Args: + left (BinaryTree): The left child of the node. + ''' + self.__left = left + + @left.deleter + def left(self) -> None: + ''' + Deletes the left child of the node. + ''' + self.__left = None + + @property + def right(self) -> 'BinaryTree': + ''' + Returns the right child of the node. + + Returns: + BinaryTree: The right child of the node. + ''' + return self.__right + + @right.setter + def right(self, right: 'BinaryTree') -> None: + ''' + Sets the right child of the node. + + Args: + right (BinaryTree): The right child of the node. + ''' + self.__right = right + + @right.deleter + def right(self) -> None: + ''' + Deletes the right child of the node. + ''' + self.__right = None + + def is_leaf(self) -> bool: + ''' + Checks whether the node is a leaf. + + Returns: + bool: True if the node is a leaf, False otherwise. + ''' + return self.__left is None and self.__right is None diff --git a/tests/test_binary_tree.py b/tests/test_binary_tree.py new file mode 100644 index 0000000..1aa9b9d --- /dev/null +++ b/tests/test_binary_tree.py @@ -0,0 +1,55 @@ +import pytest +from pystrukts.tree import BinaryTree + +def test_initialization(): + tree = BinaryTree(1) + assert tree.data == 1 + assert tree.left is None + assert tree.right is None + + tree2 = BinaryTree(2) + assert tree2.data == 2 + assert tree2.left is None + assert tree2.right is None + +def test_initialization_error(): + with pytest.raises(TypeError): + BinaryTree() + +def test_add_child(): + tree = BinaryTree(1) + tree.left = BinaryTree(2) + tree.right = BinaryTree(3) + + assert tree.left.data == 2 + assert tree.right.data == 3 + + +def test_remove_child(): + tree = BinaryTree(1) + tree.left = BinaryTree(2) + tree.right = BinaryTree(3) + + del tree.left + assert tree.left is None + + del tree.right + assert tree.right is None + +def test_is_leaf(): + tree = BinaryTree(1) + assert tree.is_leaf() == True + + tree.left = BinaryTree(2) + assert tree.is_leaf() == False + + tree.left.left = BinaryTree(3) + assert tree.left.is_leaf() == False + assert tree.is_leaf() == False + + del tree.left.left + assert tree.left.is_leaf() == True + assert tree.is_leaf() == False + + tree.left = None + assert tree.is_leaf() == True \ No newline at end of file From e66ec01b2f963e592c347f2a18ab87a304d63b2a Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Tue, 15 Oct 2024 11:45:59 +0200 Subject: [PATCH 09/13] feat: binary search tree Add binary search tree implementation and tests --- README.md | 2 +- pystrukts/tree/__init__.py | 5 +- pystrukts/tree/binary_search_tree.py | 95 ++++++++++++++++++++ tests/test_binary_search_tree.py | 125 +++++++++++++++++++++++++++ 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 pystrukts/tree/binary_search_tree.py create mode 100644 tests/test_binary_search_tree.py diff --git a/README.md b/README.md index 8fc211d..42d1bf2 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ pip install pystrukts ### Trees - [x] Binary Tree - [x] Generic Tree -- [ ] Binary Search Tree +- [x] Binary Search Tree - [ ] Ternary Search Tree - [ ] Suffix Tree - [ ] AVL Tree diff --git a/pystrukts/tree/__init__.py b/pystrukts/tree/__init__.py index da7e1fa..97e3b96 100644 --- a/pystrukts/tree/__init__.py +++ b/pystrukts/tree/__init__.py @@ -1,3 +1,6 @@ # pylint: skip-file -from .trie import Trie, TrieNode \ No newline at end of file +from .trie import Trie, TrieNode +from .generic_tree import GenericTree +from .binary_tree import BinaryTree +from .binary_search_tree import BinarySearchTree \ No newline at end of file diff --git a/pystrukts/tree/binary_search_tree.py b/pystrukts/tree/binary_search_tree.py new file mode 100644 index 0000000..62e656d --- /dev/null +++ b/pystrukts/tree/binary_search_tree.py @@ -0,0 +1,95 @@ +''' +Binary Search Tree. + +This module contains the implementation of a binary search tree. +''' + +from typing import Any +from .binary_tree import BinaryTree + +class BinarySearchTree(BinaryTree): + ''' + Binary search tree class. + + This class represents a binary search tree. There are no duplicate nodes in the tree. + ''' + + def insert(self, data: Any) -> None: + ''' + Inserts a new node in the tree. + + Args: + data (Any): The data of the new node. + ''' + if data < self.data: + if self.left is None: + self.left = BinarySearchTree(data) + else: + self.left.insert(data) + elif data > self.data: + if self.right is None: + self.right = BinarySearchTree(data) + else: + self.right.insert(data) + + def search(self, data: Any) -> bool: + ''' + Searches for a node in the tree. + + Args: + data (Any): The data to be searched. + + Returns: + bool: True if the data is found, False otherwise. + ''' + if self.data == data: + return True + + child = self.left if data < self.data else self.right + + if child is None: + return False + + return child.search(data) + + def _find_successor(self) -> 'BinarySearchTree': + ''' + Finds the in-order successor of the node. + + Returns: + BinarySearchTree: The in-order successor of the node. + ''' + temp = self.right + + while temp.left is not None: + temp = temp.left + + return temp + + def delete(self, data: Any) -> 'BinarySearchTree': + ''' + Returns the root of the tree after deleting a node. + + Args: + data (Any): The data of the node to be deleted. + + Returns: + BinarySearchTree: The root of the tree after the deletion. + ''' + if data < self.data: + if self.left is not None: + self.left = self.left.delete(data) + elif data > self.data: + if self.right is not None: + self.right = self.right.delete(data) + else: + if self.left is None: + return self.right + if self.right is None: + return self.left + + temp = self._find_successor() + self.data = temp.data + self.right = self.right.delete(temp.data) + + return self diff --git a/tests/test_binary_search_tree.py b/tests/test_binary_search_tree.py new file mode 100644 index 0000000..eeb3750 --- /dev/null +++ b/tests/test_binary_search_tree.py @@ -0,0 +1,125 @@ +import pytest +from pystrukts.tree import BinarySearchTree + +def _inorder(tree: 'BinarySearchTree'): + if tree is None: + return [] + + return _inorder(tree.left) + [tree.data] + _inorder(tree.right) + + +def test_initialization(): + tree = BinarySearchTree(1) + assert tree.data == 1 + assert tree.left is None + assert tree.right is None + + tree2 = BinarySearchTree(2) + assert tree2.data == 2 + assert tree2.left is None + assert tree2.right is None + +def test_initialization_error(): + with pytest.raises(TypeError): + BinarySearchTree() + +def test_add_child(): + tree = BinarySearchTree(1) + tree.left = BinarySearchTree(2) + tree.right = BinarySearchTree(3) + + assert tree.left.data == 2 + assert tree.right.data == 3 + + +def test_remove_child(): + tree = BinarySearchTree(1) + tree.left = BinarySearchTree(2) + tree.right = BinarySearchTree(3) + + del tree.left + assert tree.left is None + + tree.right = None + assert tree.right is None + +def test_is_leaf(): + tree = BinarySearchTree(1) + assert tree.is_leaf() == True + + tree.left = BinarySearchTree(2) + assert tree.is_leaf() == False + + tree.left.left = BinarySearchTree(3) + assert tree.left.is_leaf() == False + assert tree.is_leaf() == False + + del tree.left.left + assert tree.left.is_leaf() == True + assert tree.is_leaf() == False + + tree.left = None + assert tree.is_leaf() == True + +def test_insert(): + tree = BinarySearchTree(1) + tree.insert(2) + tree.insert(3) + tree.insert(0) + tree.insert(-1) + tree.insert(5) + + assert _inorder(tree) == [-1, 0, 1, 2, 3, 5] + +def test_search(): + tree = BinarySearchTree(1) + tree.insert(2) + tree.insert(3) + tree.insert(0) + tree.insert(-1) + tree.insert(5) + + assert tree.search(1) == True + assert tree.search(2) == True + assert tree.search(3) == True + assert tree.search(0) == True + assert tree.search(-1) == True + assert tree.search(5) == True + assert tree.search(4) == False + assert tree.search(-2) == False + + tree.delete(1) + assert tree.search(1) == False + assert tree.search(2) == True + assert tree.search(3) == True + assert tree.search(0) == True + assert tree.search(-1) == True + assert tree.search(5) == True + +def test_delete(): + tree = BinarySearchTree(1) + tree.insert(5) + tree.insert(0) + tree.insert(-1) + tree.insert(4) + tree.insert(3) + + tree = tree.delete(1) + assert _inorder(tree) == [-1, 0, 3, 4, 5] + assert tree.data == 3 + + tree = tree.delete(0) + assert _inorder(tree) == [-1, 3, 4, 5] + + tree = tree.delete(5) + assert _inorder(tree) == [-1, 3, 4] + + tree = tree.delete(3) + assert _inorder(tree) == [-1, 4] + + tree = tree.delete(4) + assert _inorder(tree) == [-1] + + tree = tree.delete(-1) + assert _inorder(tree) == [] + From c90dc01de2a9155ebf647e0f21de790e54f6ca5c Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Tue, 15 Oct 2024 12:47:10 +0200 Subject: [PATCH 10/13] fix: merge conflict Remove duplicated lines created when merging main into dev --- pystrukts/list/doubly_linked_list.py | 12 ------------ pystrukts/list/singly_linked_list.py | 11 ----------- 2 files changed, 23 deletions(-) diff --git a/pystrukts/list/doubly_linked_list.py b/pystrukts/list/doubly_linked_list.py index cd01986..df1af3f 100644 --- a/pystrukts/list/doubly_linked_list.py +++ b/pystrukts/list/doubly_linked_list.py @@ -30,7 +30,6 @@ def __str__(self): return f"DoublyLinkedNode({self.data}){' <-> ' + str(self.next) if self.next else ''}" @dataclass -class DoublyLinkedList(LinkedList[DoublyLinkedNode]): class DoublyLinkedList(LinkedList[DoublyLinkedNode]): ''' DoublyLinkedList Class. @@ -62,9 +61,6 @@ class DoublyLinkedList(LinkedList[DoublyLinkedNode]): def __init__(self, circular: bool = False): super().__init__(DoublyLinkedNode, circular) - def _link(self, node1: DoublyLinkedNode, node2: DoublyLinkedNode): - super().__init__(DoublyLinkedNode, circular) - def _link(self, node1: DoublyLinkedNode, node2: DoublyLinkedNode): ''' Link two nodes. @@ -78,12 +74,4 @@ def _link(self, node1: DoublyLinkedNode, node2: DoublyLinkedNode): node1.next = node2 if node2: node2.prev = node1 - - node1 (DoublyLinkedNode): The first node. - node2 (DoublyLinkedNode): The second node. - ''' - - node1.next = node2 - if node2: - node2.prev = node1 \ No newline at end of file diff --git a/pystrukts/list/singly_linked_list.py b/pystrukts/list/singly_linked_list.py index 014c865..5c75e74 100644 --- a/pystrukts/list/singly_linked_list.py +++ b/pystrukts/list/singly_linked_list.py @@ -27,9 +27,6 @@ class SinglyLinkedNode: def __str__(self): return f"SinglyLinkedNode({self.data}){' -> ' + str(self.next) if self.next else ''}" - -class SinglyLinkedList(LinkedList[SinglyLinkedNode]): - class SinglyLinkedList(LinkedList[SinglyLinkedNode]): ''' SinglyLinkedList Class. @@ -61,9 +58,6 @@ class SinglyLinkedList(LinkedList[SinglyLinkedNode]): def __init__(self, circular: bool = False): super().__init__(SinglyLinkedNode, circular) - def _link(self, node1: SinglyLinkedNode, node2: SinglyLinkedNode): - super().__init__(SinglyLinkedNode, circular) - def _link(self, node1: SinglyLinkedNode, node2: SinglyLinkedNode): ''' Link two nodes. @@ -75,8 +69,3 @@ def _link(self, node1: SinglyLinkedNode, node2: SinglyLinkedNode): ''' node1.next = node2 - node1 (SinglyLinkedNode): The first node. - node2 (SinglyLinkedNode): The second node. - ''' - - node1.next = node2 From 9545dd36280d7c892a293e55f4236960d458beb8 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Tue, 15 Oct 2024 13:34:50 +0200 Subject: [PATCH 11/13] fix: pyproject.toml --- pyproject.toml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b17b659..58ac608 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,11 @@ build-backend = "setuptools.build_meta" [project] name = "pystrukts" +description = "Advanced data structures for Python." +keywords = ["data structures", "structures", "tree", "graph", "list", "heap", "trie", "union find", "pystrukts"] readme = "README.md" requires-python = ">=3.8" -dynamic = ["version", "description", "authors", "urls", "keywords"] +dynamic = ["version"] license = { file = "LICENSE" } classifiers = [ "License :: OSI Approved :: Apache Software License", @@ -21,6 +23,15 @@ classifiers = [ dependencies = [ ] +authors = [ + { name = "Rubén Pérez Mercado", email = "rubenpermerc@gmail.com" } +] + +[project.urls] +Repository = "https://github.com/rubenperezm/pystrukts" +Issues = "https://github.com/rubenperezm/pystrukts/issues" +Changelog = "https://github.com/rubenperezm/pystrukts/blob/main/CHANGELOG.md" + [tool.setuptools_scm] write_to = "pystrukts/_version.py" From 57ffc716fbfa9f3e4103e493388cf84f4a09032e Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Sat, 26 Oct 2024 11:37:26 +0200 Subject: [PATCH 12/13] feat: fenwick tree Add Fenwick tree implmentation and tests --- README.md | 17 +-- pystrukts/tree/__init__.py | 3 +- pystrukts/tree/avl.py | 0 pystrukts/tree/fenwick_tree.py | 193 +++++++++++++++++++++++++++++++++ tests/test_fenwick_tree.py | 36 ++++++ 5 files changed, 234 insertions(+), 15 deletions(-) create mode 100644 pystrukts/tree/avl.py create mode 100644 pystrukts/tree/fenwick_tree.py create mode 100644 tests/test_fenwick_tree.py diff --git a/README.md b/README.md index 42d1bf2..0dc2465 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ [![Build](https://github.com/rubenperezm/pystrukts/actions/workflows/test.yml/badge.svg)](https://github.com/rubenperezm/pystrukts/actions/workflows/test.yml/badge.svg) [![codecov](https://codecov.io/gh/rubenperezm/pystrukts/graph/badge.svg?token=OLV3EOPYFI)](https://codecov.io/gh/rubenperezm/pystrukts) [![PyPI Version](https://img.shields.io/pypi/v/pystrukts.svg)](https://pypi.org/project/pystrukts) - +[![Python Versions](https://img.shields.io/pypi/pyversions/pystrukts.svg)](https://pypi.org/project/pystrukts) -A Python library for data structures. This library provides more than 20 advanced data structures not available in the Python standard library. +A Python library for data structures. This library provides more than 10 advanced data structures not available in the Python standard library. ## Installation @@ -21,23 +21,12 @@ pip install pystrukts - [x] Doubly Linked Node - [x] Doubly Linked List - [x] Circular Linked List (Single, Double) -- [ ] Skip List ### Trees - [x] Binary Tree - [x] Generic Tree - [x] Binary Search Tree -- [ ] Ternary Search Tree -- [ ] Suffix Tree -- [ ] AVL Tree -- [ ] Red-Black Tree -- [ ] Splay Tree -- [ ] B-Tree -- [ ] B+ Tree -- [ ] R-Tree -- [ ] Interval Tree -- [ ] Segment Tree -- [ ] Fenwick Tree +- [x] Fenwick Trees (RUPQ, RURQ) - [x] Trie ### Graphs diff --git a/pystrukts/tree/__init__.py b/pystrukts/tree/__init__.py index 97e3b96..86de43a 100644 --- a/pystrukts/tree/__init__.py +++ b/pystrukts/tree/__init__.py @@ -3,4 +3,5 @@ from .trie import Trie, TrieNode from .generic_tree import GenericTree from .binary_tree import BinaryTree -from .binary_search_tree import BinarySearchTree \ No newline at end of file +from .binary_search_tree import BinarySearchTree +from .fenwick_tree import FenwickTree, RUPQ, RURQ \ No newline at end of file diff --git a/pystrukts/tree/avl.py b/pystrukts/tree/avl.py new file mode 100644 index 0000000..e69de29 diff --git a/pystrukts/tree/fenwick_tree.py b/pystrukts/tree/fenwick_tree.py new file mode 100644 index 0000000..0ff482d --- /dev/null +++ b/pystrukts/tree/fenwick_tree.py @@ -0,0 +1,193 @@ +''' +Fenwick Tree implementation in Python. + +This module contains the implementation of Fenwick Trees, as well as two extensions of it: +- Range Update Point Query (RUPQ) +- Range Update Range Query (RURQ) +''' + +from typing import List + +class FenwickTree: + ''' + Fenwick Tree implementation in Python. + + Attributes: + - n: int - the size of the Fenwick Tree + - ftree: list - the Fenwick Tree itself + + Methods: + - lsone(s: int) -> int: returns the least significant bit of s + - query(i: int, j: int) -> int: returns the sum of the elements in the range [i, j] + - update(i: int, v: int): updates the element at index i with value v + - select(k: int) -> int: returns the index of the k-th element in the Fenwick Tree + ''' + def __init__(self, f: List[int]): + self.n = len(f) + self.ftree = [0] * (self.n + 1) + + for i in range(1, self.n + 1): + self.ftree[i] += f[i - 1] + if i + self._lsone(i) <= self.n: + self.ftree[i + self._lsone(i)] += self.ftree[i] + + def _lsone(self, s: int) -> int: + ''' + Returns the least significant bit of s. + + Args: + s (int): The number to get the least significant bit from. + + Returns: + out (int): The least significant bit of s. + ''' + return s & -s + + def query(self, i: int, j: int) -> int: + ''' + Queries the Fenwick Tree for the sum of the elements in the range [i, j]. + + Args: + i (int): The lower bound of the range. + j (int): The upper bound of the range. + + Returns: + s (int): The sum of the elements in the range [i, j]. + ''' + if i > 1: + return self.query(1, j) - self.query(1, i - 1) + + s = 0 + while j > 0: + s += self.ftree[j] + j -= self._lsone(j) + + return s + + def update(self, i: int, v: int): + ''' + Updates the element at index i with value v. + + Args: + i (int): The index of the element to update. + v (int): The new value of the element. + ''' + + while i <= self.n: + self.ftree[i] += v + i += self._lsone(i) + + def select(self, k: int) -> int: + ''' + Returns the index of the k-th element in the Fenwick Tree. + + Args: + k (int): The index of the element to return. + + Returns: + i (int): The index of the k-th element in the Fenwick Tree. + ''' + + p = 1 + while p*2 <= self.n: + p *= 2 + + i = 0 + while p > 0: + if k > self.ftree[i + p]: + k -= self.ftree[i + p] + i += p + p //= 2 + + return i + 1 + +class RUPQ: + ''' + Range Update Point Query (RUPQ) implementation. + + This class extends the Fenwick Tree to support range updates and point queries. + + Attributes: + - ftree: FenwickTree - the Fenwick Tree used for the range updates + + Methods: + - query(i: int) -> int: returns the value of the element at index i + - update(i: int, j: int, v: int): updates the elements in the range [i, j] with value v + ''' + def __init__(self, n: int): + self.ftree = FenwickTree([0] * n) + + def query(self, i: int) -> int: + ''' + Queries the Fenwick Tree for the value of the element at index i. + + Args: + i (int): The index of the element to query. + + Returns: + s (int): The value of the element at index i. + ''' + + return self.ftree.query(1, i) + + def update(self, i: int, j: int, v: int): + ''' + Updates the elements in the range [i, j] with value v. + + Args: + i (int): The lower bound of the range. + j (int): The upper bound of the range. + v (int): The value to update the elements with. + ''' + + self.ftree.update(i, v) + self.ftree.update(j + 1, -v) + +class RURQ: + ''' + Range Update Range Query (RURQ) implementation. + + This class extends the Fenwick Tree to support range updates and range queries. + + Attributes: + - f: FenwickTree - the Fenwick Tree used for the range updates + - r: RUPQ - the RUPQ used for the range queries + + Methods: + - query(i: int, j: int) -> int: returns the sum of the elements in the range [i, j] + - update(i: int, j: int, v: int): updates the elements in the range [i, j] with value v + ''' + + def __init__(self, n: int): + self.ftree = FenwickTree([0] * n) + self.rupq = RUPQ(n) + + def query(self, i: int, j: int) -> int: + ''' + Queries the Fenwick Tree for the sum of the elements in the range [i, j]. + + Args: + i (int): The lower bound of the range. + j (int): The upper bound of the range. + + Returns: + s (int): The sum of the elements in the range [i, j]. + ''' + + if i > 1: + return self.query(1, j) - self.query(1, i - 1) + return self.rupq.query(j) * j - self.ftree.query(1, j) + + def update(self, i: int, j: int, v: int): + ''' + Updates the elements in the range [i, j] with value v. + + Args: + i (int): The lower bound of the range. + j (int): The upper bound of the range. + v (int): The value to update the elements with. + ''' + + self.rupq.update(i, j, v) + self.ftree.update(i, v * (i - 1)) + self.ftree.update(j + 1, -1 * v * j) diff --git a/tests/test_fenwick_tree.py b/tests/test_fenwick_tree.py new file mode 100644 index 0000000..ae58983 --- /dev/null +++ b/tests/test_fenwick_tree.py @@ -0,0 +1,36 @@ +import pytest +from pystrukts.tree import FenwickTree, RUPQ, RURQ + + +def test_initialization_ftree(): + f = [0, 1, 0, 1, 2, 3, 2, 1, 1, 0] + ft = FenwickTree(f) + assert ft.n == 10 + assert ft.ftree == [0, 0, 1, 0, 2, 2, 5, 2, 10, 1, 1] + assert ft.query(1, 6) == 7 + assert ft.query(1, 3) == 1 + assert ft.select(7) == 6 + ft.update(5, 1) + assert ft.query(1, 10) == 12 + +def test_rupq(): + r = RUPQ(10) + r.update(2, 9, 7) + r.update(6, 7, 3) + assert r.query(1) == 0 + assert r.query(2) == 7 + assert r.query(3) == 7 + assert r.query(4) == 7 + assert r.query(5) == 7 + assert r.query(6) == 10 + assert r.query(7) == 10 + assert r.query(8) == 7 + assert r.query(9) == 7 + assert r.query(10) == 0 + +def test_rurq(): + r = RURQ(10) + r.update(2, 9, 7) + r.update(6, 7, 3) + assert r.query(3, 5) == 21 + assert r.query(7, 8) == 17 \ No newline at end of file From 29b9d1c3fc6a51e0b63a480eec6ea0548361a3c8 Mon Sep 17 00:00:00 2001 From: rubenperezm Date: Sat, 26 Oct 2024 12:10:36 +0200 Subject: [PATCH 13/13] Delete avl file --- pystrukts/tree/avl.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 pystrukts/tree/avl.py diff --git a/pystrukts/tree/avl.py b/pystrukts/tree/avl.py deleted file mode 100644 index e69de29..0000000