diff --git a/.gitignore b/.gitignore index 600d2d3..0ad56a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode \ No newline at end of file +.vscode +.venv/ diff --git a/python/LinkedList/singly/list.py b/python/LinkedList/singly/list.py deleted file mode 100644 index f5c20a7..0000000 --- a/python/LinkedList/singly/list.py +++ /dev/null @@ -1,211 +0,0 @@ -class Node: - """ - Node class representing a single node in a singly linked list. - """ - - def __init__(self, data): - """ - Create a Node. - - :param data: The data to be stored in the node. - """ - self.data = data - self.next = None - - -class LinkedList: - """ - LinkedList class representing a singly linked list. - """ - - def __init__(self): - """ - Create an empty LinkedList. - """ - self.head = None - self.tail = None - self.length = 0 - self.node_map = {} # Dictionary for O(1) lookups - - def get_head(self): - """ - Get the head node of the list. - - :return: The head node or None if the list is empty. - """ - return self.head - - def get_tail(self): - """ - Get the tail node of the list. - - :return: The tail node or None if the list is empty. - """ - return self.tail - - def prepend(self, data): - """ - Prepend a node to the beginning of the list. - - :param data: The data to be stored in the new node. - """ - new_node = Node(data) - if not self.head: - self.head = new_node - self.tail = new_node - else: - new_node.next = self.head - self.head = new_node - self.node_map[data.id] = new_node - self.length += 1 - - def append(self, data): - """ - Append a node to the end of the list. - - :param data: The data to be stored in the new node. - """ - new_node = Node(data) - if not self.head: - self.head = new_node - self.tail = new_node - else: - self.tail.next = new_node - self.tail = new_node - self.node_map[data.id] = new_node - self.length += 1 - - def insert_between(self, data, before, after): - """ - Insert a node between two existing nodes in the list. - - :param data: The data to be stored in the new node. - :param before: The data of the node before which to - insert the new node. - :param after: The data of the node after which to insert the new node. - """ - new_node = Node(data) - curr = self.head - - while curr and curr.data != after: - curr = curr.next - - if curr and curr.next and curr.next.data == before: - new_node.next = curr.next - curr.next = new_node - self.node_map[data.id] = new_node - self.length += 1 - - def delete_head(self): - """ - Delete the head node of the list. - """ - if self.head: - self.node_map.pop(self.head.data.id, None) - self.head = self.head.next - if not self.head: - self.tail = None - self.length -= 1 - - def delete_tail(self): - """ - Delete the tail node of the list. - """ - if not self.head: - return - if not self.head.next: - self.node_map.pop(self.head.data.id, None) - self.head = None - self.tail = None - else: - prev_node = self.head - while prev_node.next and prev_node.next.next: - prev_node = prev_node.next - self.node_map.pop(prev_node.next.data.id, None) - prev_node.next = None - self.tail = prev_node - self.length -= 1 - - def delete_any(self, target): - """ - Delete any node from the list that matches the given target data. - - :param target: The target data to match for deletion. - """ - if not self.head: - return - - if self.head.data.id == target.id: - self.node_map.pop(self.head.data.id, None) - self.head = self.head.next - if not self.head: - self.tail = None - self.length -= 1 - else: - prev_node = self.head - curr = self.head.next - - while curr and curr.data.id != target.id: - prev_node = curr - curr = curr.next - - if curr: - prev_node.next = curr.next - if not curr.next: - self.tail = prev_node - self.node_map.pop(curr.data.id, None) - self.length -= 1 - - def size(self): - """ - Get the size of the list. - - :return: The number of nodes in the list. - """ - return self.length - - def reverse_list(self): - """ - Reverse the linked list in place. - """ - prev = None - curr = self.head - next_node = None - - self.tail = self.head - - while curr: - next_node = curr.next - curr.next = prev - prev = curr - curr = next_node - self.head = prev - - def find(self, id): - """ - Find a node in the list by its id. - - :param id: The id of the node to find. - :return: The node with the given id or None if not found. - """ - return self.node_map.get(id, None) - - def clear(self): - """ - Clear the list. - """ - self.head = None - self.tail = None - self.length = 0 - self.node_map = {} - - def print_list(self): - """ - Print the list data in a readable format. - """ - curr = self.head - result = [] - while curr: - result.append(str(curr.data)) - curr = curr.next - print(" -> ".join(result)) diff --git a/python/LinkedList/singly/list_test.py b/python/LinkedList/singly/list_test.py deleted file mode 100644 index be6821f..0000000 --- a/python/LinkedList/singly/list_test.py +++ /dev/null @@ -1,126 +0,0 @@ -import unittest -from list import LinkedList - - -class TestLinkedList(unittest.TestCase): - - class TestData: - def __init__(self, id, value): - self.id = id - self.value = value - - def __str__(self): - return f"({self.id}: {self.value})" - - def setUp(self): - self.linked_list = LinkedList() - - def test_prepend(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - self.linked_list.prepend(data1) - self.linked_list.prepend(data2) - self.assertEqual(self.linked_list.get_head().data, data2) - self.assertEqual(self.linked_list.get_tail().data, data1) - self.assertEqual(self.linked_list.size(), 2) - - def test_append(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - self.linked_list.append(data1) - self.linked_list.append(data2) - self.assertEqual(self.linked_list.get_head().data, data1) - self.assertEqual(self.linked_list.get_tail().data, data2) - self.assertEqual(self.linked_list.size(), 2) - - def test_insert_between(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - data3 = self.TestData(3, "data3") - self.linked_list.append(data1) - self.linked_list.append(data3) - self.linked_list.insert_between(data2, before=data3, after=data1) - self.assertEqual(self.linked_list.size(), 3) - self.assertEqual(self.linked_list.get_head().next.data, data2) - self.assertEqual(self.linked_list.get_head().next.next.data, data3) - - def test_delete_head(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - self.linked_list.append(data1) - self.linked_list.append(data2) - self.linked_list.delete_head() - self.assertEqual(self.linked_list.get_head().data, data2) - self.assertEqual(self.linked_list.size(), 1) - self.linked_list.delete_head() - self.assertIsNone(self.linked_list.get_head()) - self.assertIsNone(self.linked_list.get_tail()) - self.assertEqual(self.linked_list.size(), 0) - - def test_delete_tail(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - self.linked_list.append(data1) - self.linked_list.append(data2) - self.linked_list.delete_tail() - self.assertEqual(self.linked_list.get_tail().data, data1) - self.assertEqual(self.linked_list.size(), 1) - self.linked_list.delete_tail() - self.assertIsNone(self.linked_list.get_tail()) - self.assertIsNone(self.linked_list.get_head()) - self.assertEqual(self.linked_list.size(), 0) - - def test_delete_any(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - data3 = self.TestData(3, "data3") - self.linked_list.append(data1) - self.linked_list.append(data2) - self.linked_list.append(data3) - self.linked_list.delete_any(data2) - self.assertEqual(self.linked_list.size(), 2) - self.assertEqual(self.linked_list.get_head().next.data, data3) - self.linked_list.delete_any(data1) - self.assertEqual(self.linked_list.size(), 1) - self.assertEqual(self.linked_list.get_head().data, data3) - self.linked_list.delete_any(data3) - self.assertEqual(self.linked_list.size(), 0) - self.assertIsNone(self.linked_list.get_head()) - self.assertIsNone(self.linked_list.get_tail()) - - def test_reverse_list(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - data3 = self.TestData(3, "data3") - self.linked_list.append(data1) - self.linked_list.append(data2) - self.linked_list.append(data3) - self.linked_list.reverse_list() - self.assertEqual(self.linked_list.get_head().data, data3) - self.assertEqual(self.linked_list.get_head().next.data, data2) - self.assertEqual(self.linked_list.get_head().next.next.data, data1) - self.assertEqual(self.linked_list.get_tail().data, data1) - - def test_find(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - self.linked_list.append(data1) - self.linked_list.append(data2) - self.assertEqual(self.linked_list.find(1), self.linked_list.get_head()) - self.assertEqual(self.linked_list.find(2), self.linked_list.get_tail()) - self.assertIsNone(self.linked_list.find(3)) - - def test_clear(self): - data1 = self.TestData(1, "data1") - data2 = self.TestData(2, "data2") - self.linked_list.append(data1) - self.linked_list.append(data2) - self.linked_list.clear() - self.assertIsNone(self.linked_list.get_head()) - self.assertIsNone(self.linked_list.get_tail()) - self.assertEqual(self.linked_list.size(), 0) - self.assertEqual(self.linked_list.node_map, {}) - - -if __name__ == "__main__": - unittest.main() diff --git a/python/cleanup.sh b/python/cleanup.sh new file mode 100755 index 0000000..9399f1a --- /dev/null +++ b/python/cleanup.sh @@ -0,0 +1,4 @@ +# !/bin/bash + +find . -type d -name "__pycache__" -exec rm -r {} + +find . -type d -name ".pytest_cache" -exec rm -r {} + \ No newline at end of file diff --git a/python/src/__init__.py b/python/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/src/linkedlist/__init__.py b/python/src/linkedlist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/LinkedList/doubly/list.py b/python/src/linkedlist/doubly/list.py similarity index 100% rename from python/LinkedList/doubly/list.py rename to python/src/linkedlist/doubly/list.py diff --git a/python/src/linkedlist/singly/__init__.py b/python/src/linkedlist/singly/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/src/linkedlist/singly/list.py b/python/src/linkedlist/singly/list.py new file mode 100644 index 0000000..59accef --- /dev/null +++ b/python/src/linkedlist/singly/list.py @@ -0,0 +1,206 @@ +import os +from typing import Dict, Optional, Any + +from utils.logger import Logger + +class Node: + def __init__(self, data: Any): + self.data = data + self.next: Optional["Node"] = None + self._id = os.urandom(16).hex() + + def __repr__(self) -> str: + return f"Node({self._id}: {self.data})" + +class LinkedList: + def __init__(self): + self.head: Optional[Node] = None + self.tail: Optional[Node] = None + self.length: int = 0 + # dictionary to store nodes by their IDs for faster lookup and deletion + self.node_map: Dict[str, Node] = {} + + def get_head(self) -> Optional[Node]: + """Get the head node of the list.""" + return self.head + + def get_tail(self) -> Optional[Node]: + """Get the tail node of the list.""" + return self.tail + + def prepend(self, data: Any) -> None: + """Prepend a node to the start of the list.""" + + new_node = Node(data) + new_node.next = self.head + self.head = new_node + + if not self.tail: + self.tail = new_node + + self.node_map[new_node._id] = new_node.data + self.length += 1 + Logger.info(f"Prepend node: {data}") + + def append(self, data): + """Append a node to the end of the list.""" + + new_node = Node(data) + if not self.head: + self.head = new_node + self.tail = new_node + else: + assert self.tail is not None, "Tail node is not set" + + self.tail.next = new_node + self.tail = new_node + + self.node_map[new_node._id] = new_node.data + self.length += 1 + Logger.info(f"Append node: {data}") + + def insert_between(self, data, before, after): + """ + Insert a node between two existing nodes in the list. + + :param data: The data to be stored in the new node. + :param before: The data of the node before which to + insert the new node. + :param after: The data of the node after which to insert the new node. + """ + if not self.head: + raise Exception("List is empty, nothing to insert") + + new_node = Node(data) + curr = self.head + + while curr and curr.data != after: + curr = curr.next + + if curr and curr.next and curr.next.data == before: + new_node.next = curr.next + curr.next = new_node + self.node_map[new_node._id] = new_node.data + self.length += 1 + + def delete_head(self): + """Delete the head node of the list.""" + + if not self.head: + raise Exception("List is empty, nothing to delete") + self.node_map.pop(self.head._id, None) + self.head = self.head.next + if not self.head: + self.tail = None + self.length -= 1 + + def delete_tail(self): + """ + Delete the tail node of the list. + """ + if not self.head: + raise Exception("List is empty, nothing to delete") + + if not self.head.next or self.head == self.tail: + self.node_map.pop(self.head._id, None) + self.head = self.tail = None + else: + prev_node = self.head + while prev_node.next != self.tail: + prev_node = prev_node.next + self.node_map.pop(self.tail._id, None) + self.tail = prev_node + self.tail.next = None + + self.length -= 1 + + def delete_any(self, target: Node): + """Delete any node from the list that matches the given target data.""" + if not isinstance(target, Node): + raise ValueError("Target must be a Node instance") + + if target._id not in self.node_map: + raise Exception(f"Node with ID {target._id} not found in the list") + + if self.head._id == target._id: + self.delete_head() + return + else: + prev_node: Node = self.head + curr: Node = self.head.next + + while curr and curr._id != target._id: + prev_node = curr + curr = curr.next + + if curr: + prev_node.next = curr.next + if not curr.next: + self.tail = prev_node + self.node_map.pop(curr._id, None) + self.length -= 1 + else: + raise Exception(f"Node with ID {target._id} not found in the list") + + def size(self): + """Get the number of nodes in the list.""" + + return self.length + + def reverse_list(self): + """ + Reverse the linked list in place. + """ + + prev = None + curr = self.head + next_node = None + + self.tail = self.head + + while curr: + next_node = curr.next + curr.next = prev + prev = curr + curr = next_node + + self.head = prev + + def find(self, node: Node) -> Optional[Node]: + """ + Find a node in the list by its id. + + :param id: The id of the node to find. + :return: The node with the given id or None if not found. + """ + return self.node_map.get(node._id, None) + + def clear(self): + """ + Clear the list. + """ + self.head = None + self.tail = None + self.length = 0 + self.node_map.clear() + + def __len__(self) -> int: + return self.length + + def __iter__(self): + current = self.head + while current: + yield current + current = current.next + + def __repr__(self) -> str: + nodes = [str(node) for node in self] + return " -> ".join(nodes) + + def print_list(self): + """ + Print the list data in a readable format. Since we have a + custom __iter__ and __repr__ methods, this method is not necessary. Simply + use a print statement to print the list data. + """ + print(self) \ No newline at end of file diff --git a/python/src/utils/__init__.py b/python/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/src/utils/logger.py b/python/src/utils/logger.py new file mode 100644 index 0000000..1d06171 --- /dev/null +++ b/python/src/utils/logger.py @@ -0,0 +1,46 @@ +import sys +import time +from datetime import datetime +from colorama import init, Fore, Style + +init(autoreset=True) + +class Logger: + LEVELS = { + "INFO": Fore.CYAN, + "DEBUG": Fore.BLUE, + "WARNING": Fore.YELLOW, + "ERROR": Fore.RED, + "SUCCESS": Fore.GREEN, + } + + @staticmethod + def log(level: str, message: str): + if level not in Logger.LEVELS: + raise ValueError(f"Invalid log level: {level}") + + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + formatted_mssg = ( + f"{Style.BRIGHT}[{timestamp}] {Logger.LEVELS[level]}[{level}] {Style.RESET_ALL}{message}" + ) + print(formatted_mssg, file=sys.stdout) + + @staticmethod + def info(message: str): + Logger.log("INFO", message) + + @staticmethod + def debug(message: str): + Logger.log("DEBUG", message) + + @staticmethod + def warning(message: str): + Logger.log("WARNING", message) + + @staticmethod + def error(message: str): + Logger.log("ERROR", message) + + @staticmethod + def success(message: str): + Logger.log("SUCCESS", message) \ No newline at end of file diff --git a/python/tests/conftest.py b/python/tests/conftest.py new file mode 100644 index 0000000..c149a93 --- /dev/null +++ b/python/tests/conftest.py @@ -0,0 +1,18 @@ +import os +import sys +import subprocess +from src.utils.logger import Logger + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +def pytest_sessionfinish(session, exitstatus): + cleanup_script = os.path.abspath(os.path.join(os.path.dirname(__file__), "../cleanup.sh")) + if os.path.exists(cleanup_script): + Logger.log("INFO", f"Running cleanup script: {cleanup_script}") + try: + subprocess.run(["bash", cleanup_script], check=True) + Logger.log("INFO", "Cleanup script completed successfully") + except subprocess.CalledProcessError as e: + Logger.error(f"Error running cleanup script: {e}") + else: + Logger.error(f"Cleanup script not found: {cleanup_script}") \ No newline at end of file diff --git a/python/tests/linkedlist/__init__.py b/python/tests/linkedlist/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/linkedlist/singly/__init__.py b/python/tests/linkedlist/singly/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/linkedlist/singly/test_singly.py b/python/tests/linkedlist/singly/test_singly.py new file mode 100644 index 0000000..2294a79 --- /dev/null +++ b/python/tests/linkedlist/singly/test_singly.py @@ -0,0 +1,41 @@ +from src.linkedlist.singly.list import LinkedList + +import pytest + +NUM_ELEMENTS = 100_000 + + +@pytest.fixture +def linked_list_append_fixture() -> LinkedList: + ll = LinkedList() + + for i in range(NUM_ELEMENTS): + ll.append(f"node({i})") + + return ll + + +@pytest.fixture +def linked_list_prepend_fixture() -> LinkedList: + ll = LinkedList() + + for i in range(NUM_ELEMENTS): + ll.prepend(f"node({i})") + + return ll + + +def test_append(linked_list_append_fixture): + ll = linked_list_append_fixture + + assert len(ll) == NUM_ELEMENTS + assert ll.get_tail().data == f"node({NUM_ELEMENTS-1})" + assert ll.get_head().data == "node(0)" + + +def test_prepend(linked_list_prepend_fixture): + ll = linked_list_prepend_fixture + + assert len(ll) == NUM_ELEMENTS + assert ll.get_tail().data == f"node({NUM_ELEMENTS-1})" + assert ll.get_head().data == f"node({NUM_ELEMENTS-99999})" \ No newline at end of file