pure-python-ds 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,3 @@
1
+ from .sorting import merge_sort
2
+ from .searching import binary_search
3
+ from .dp import fibonacci
@@ -0,0 +1,16 @@
1
+ from typing import Dict
2
+
3
+ def fibonacci(n: int, memo: Dict[int, int] = None) -> int:
4
+ """
5
+ Computes the nth Fibonacci number using Top-Down DP (Memoization).
6
+ O(n) time complexity vs O(2^n) recursive complexity.
7
+ """
8
+ if memo is None:
9
+ memo = {}
10
+ if n in memo:
11
+ return memo[n]
12
+ if n <= 1:
13
+ return n
14
+
15
+ memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo)
16
+ return memo[n]
@@ -0,0 +1,22 @@
1
+ from typing import List, TypeVar, Optional, Any
2
+
3
+ T = TypeVar('T')
4
+
5
+ def binary_search(arr: List[Any], target: Any) -> int:
6
+ """
7
+ Performs an iterative binary search on a sorted list.
8
+ Returns the index of the target if found, else -1.
9
+ """
10
+ low = 0
11
+ high = len(arr) - 1
12
+
13
+ while low <= high:
14
+ mid = (low + high) // 2
15
+ if arr[mid] == target:
16
+ return mid
17
+ elif arr[mid] < target:
18
+ low = mid + 1
19
+ else:
20
+ high = mid - 1
21
+
22
+ return -1
@@ -0,0 +1,36 @@
1
+ from typing import List, TypeVar
2
+
3
+ T = TypeVar('T')
4
+
5
+ def merge_sort(arr: List[T]) -> List[T]:
6
+ """
7
+ Perform an O(n log n) Merge Sort on a list.
8
+ Returns a new sorted list.
9
+ """
10
+ if len(arr) <= 1:
11
+ return arr
12
+
13
+ mid = len(arr) // 2
14
+ # Recursive split
15
+ left = merge_sort(arr[:mid])
16
+ right = merge_sort(arr[mid:])
17
+
18
+ return _merge(left, right)
19
+
20
+ def _merge(left: List[T], right: List[T]) -> List[T]:
21
+ """Helper method to merge two sorted lists into one."""
22
+ result = []
23
+ i = j = 0
24
+
25
+ while i < len(left) and j < len(right):
26
+ if left[i] < right[j]:
27
+ result.append(left[i])
28
+ i += 1
29
+ else:
30
+ result.append(right[j])
31
+ j += 1
32
+
33
+ # Add remaining elements
34
+ result.extend(left[i:])
35
+ result.extend(right[j:])
36
+ return result
@@ -0,0 +1,2 @@
1
+ from .graph import Graph
2
+ from .dsu import DSU
@@ -0,0 +1,22 @@
1
+ class DSU:
2
+ def __init__(self, n: int):
3
+ self.parent = list(range(n))
4
+ self.rank = [0] * n
5
+
6
+ def find(self, i: int) -> int:
7
+ if self.parent[i] == i:
8
+ return i
9
+ self.parent[i] = self.find(self.parent[i]) # Path Compression
10
+ return self.parent[i]
11
+
12
+ def union(self, i: int, j: int):
13
+ root_i = self.find(i)
14
+ root_j = self.find(j)
15
+ if root_i != root_j:
16
+ if self.rank[root_i] < self.rank[root_j]:
17
+ self.parent[root_i] = root_j
18
+ elif self.rank[root_i] > self.rank[root_j]:
19
+ self.parent[root_j] = root_i
20
+ else:
21
+ self.parent[root_i] = root_j
22
+ self.rank[root_j] += 1
@@ -0,0 +1,106 @@
1
+ from typing import Generic, TypeVar, Optional, Set, Dict, List, Tuple
2
+ import heapq # We'll use Python's built-in min-heap for O(log n) efficiency here
3
+
4
+ T = TypeVar('T')
5
+
6
+ class Graph(Generic[T]):
7
+ def __init__(self, directed: bool = False):
8
+ # Updated to store {vertex: {neighbor: weight}}
9
+ self._adj_list: Dict[T, Dict[T, float]] = {}
10
+ self.directed = directed
11
+
12
+ def add_vertex(self, vertex: T) -> None:
13
+ if vertex not in self._adj_list:
14
+ self._adj_list[vertex] = {}
15
+
16
+ def add_edge(self, v1: T, v2: T, weight: float = 1.0) -> None:
17
+ """Adds a weighted edge. Defaults to 1.0 if not specified."""
18
+ self.add_vertex(v1)
19
+ self.add_vertex(v2)
20
+ self._adj_list[v1][v2] = weight
21
+ if not self.directed:
22
+ self._adj_list[v2][v1] = weight
23
+
24
+ def dijkstra(self, start: T) -> Dict[T, float]:
25
+ """
26
+ Greedy algorithm to find the shortest distance from 'start' to all other nodes.
27
+ Returns a dictionary of {vertex: min_distance}.
28
+ """
29
+ # Distances from start to all other nodes are initially infinity
30
+ distances = {vertex: float('inf') for vertex in self._adj_list}
31
+ distances[start] = 0
32
+
33
+ # Priority Queue: (distance, vertex)
34
+ pq = [(0, start)]
35
+
36
+ while pq:
37
+ current_distance, current_vertex = heapq.heappop(pq)
38
+
39
+ # If we found a longer path than we already recorded, skip it
40
+ if current_distance > distances[current_vertex]:
41
+ continue
42
+
43
+ for neighbor, weight in self._adj_list[current_vertex].items():
44
+ distance = current_distance + weight
45
+
46
+ # If this path is shorter, update and push to PQ
47
+ if distance < distances[neighbor]:
48
+ distances[neighbor] = distance
49
+ heapq.heappush(pq, (distance, neighbor))
50
+
51
+ return distances
52
+ def kruskal_mst(self) -> List[Tuple[T, T, float]]:
53
+ """
54
+ Finds the Minimum Spanning Tree using the library's DSU module.
55
+ Returns a list of edges (v1, v2, weight) in the MST.
56
+ """
57
+ from pure_python_ds.graphs import DSU
58
+
59
+ edges = []
60
+ # Gather all unique edges
61
+ for u in self._adj_list:
62
+ for v, weight in self._adj_list[u].items():
63
+ if self.directed or (v, u, weight) not in edges:
64
+ edges.append((u, v, weight))
65
+
66
+ # Sort edges by weight
67
+ edges.sort(key=lambda x: x[2])
68
+
69
+ # Map vertices to integers for DSU
70
+ nodes = list(self._adj_list.keys())
71
+ node_to_idx = {node: i for i, node in enumerate(nodes)}
72
+
73
+ dsu = DSU(len(nodes))
74
+ mst = []
75
+
76
+ for u, v, weight in edges:
77
+ if dsu.find(node_to_idx[u]) != dsu.find(node_to_idx[v]):
78
+ dsu.union(node_to_idx[u], node_to_idx[v])
79
+ mst.append((u, v, weight))
80
+
81
+ return mst
82
+ def bellman_ford(self, start: T) -> Dict[T, float]:
83
+ """
84
+ Shortest path algorithm that handles negative weights.
85
+ Returns distances or raises ValueError if a negative cycle is found.
86
+ """
87
+ distances = {vertex: float('inf') for vertex in self._adj_list}
88
+ distances[start] = 0
89
+
90
+ # Relax edges |V| - 1 times
91
+ for _ in range(len(self._adj_list) - 1):
92
+ for u in self._adj_list:
93
+ for v, weight in self._adj_list[u].items():
94
+ if distances[u] + weight < distances[v]:
95
+ distances[v] = distances[u] + weight
96
+
97
+ # Check for negative cycles
98
+ for u in self._adj_list:
99
+ for v, weight in self._adj_list[u].items():
100
+ if distances[u] + weight < distances[v]:
101
+ raise ValueError("Graph contains a negative weight cycle")
102
+
103
+ return distances
104
+ def has_edge(self, u: T, v: T) -> bool:
105
+ """Returns True if there is an edge from u to v."""
106
+ return u in self._adj_list and v in self._adj_list[u]
@@ -0,0 +1,6 @@
1
+ from .singly_linked_list import SinglyLinkedList
2
+ from .doubly_linked_list import DoublyLinkedList
3
+ from .stack import Stack
4
+ from .queue import Queue
5
+ from .heap import MinHeap, MaxHeap
6
+ from .hash_table import HashTable
@@ -0,0 +1,91 @@
1
+ from typing import Generic, TypeVar, Optional, Generator
2
+ from pure_python_ds.nodes import ListNode
3
+
4
+ T = TypeVar('T')
5
+
6
+ class DoublyLinkedList(Generic[T]):
7
+ """A strictly typed, memory-optimized Doubly Linked List."""
8
+
9
+ def __init__(self):
10
+ self.head: Optional[ListNode[T]] = None
11
+ self.tail: Optional[ListNode[T]] = None
12
+ self._length: int = 0
13
+
14
+ def __len__(self) -> int:
15
+ return self._length
16
+
17
+ def append(self, value: T) -> None:
18
+ """Adds a node to the end in O(1) time."""
19
+ new_node = ListNode(value)
20
+ if not self.head or not self.tail:
21
+ self.head = new_node
22
+ self.tail = new_node
23
+ else:
24
+ self.tail.next = new_node
25
+ new_node.prev = self.tail
26
+ self.tail = new_node
27
+ self._length += 1
28
+
29
+ def prepend(self, value: T) -> None:
30
+ """Adds a node to the beginning in O(1) time."""
31
+ new_node = ListNode(value)
32
+ if not self.head or not self.tail:
33
+ self.head = new_node
34
+ self.tail = new_node
35
+ else:
36
+ new_node.next = self.head
37
+ self.head.prev = new_node
38
+ self.head = new_node
39
+ self._length += 1
40
+
41
+ def remove_tail(self) -> Optional[T]:
42
+ """Removes the last node in O(1) time (Impossible in Singly Linked Lists!)."""
43
+ if not self.tail:
44
+ return None
45
+
46
+ value = self.tail.value
47
+ if self.head is self.tail: # Only one element
48
+ self.head = None
49
+ self.tail = None
50
+ else:
51
+ self.tail = self.tail.prev
52
+ self.tail.next = None # Sever the tie
53
+
54
+ self._length -= 1
55
+ return value
56
+
57
+ def __iter__(self) -> Generator[T, None, None]:
58
+ """Forward generator traversal."""
59
+ current = self.head
60
+ while current:
61
+ yield current.value
62
+ current = current.next
63
+
64
+ def __str__(self) -> str:
65
+ """Visual representation showing bidirectional pointers."""
66
+ values = [str(val) for val in self]
67
+ return "None <- " + " <-> ".join(values) + " -> None" if values else "Empty List"
68
+ def remove(self, value: T):
69
+ """Removes the first occurrence of value from the list."""
70
+ current = self.head
71
+ while current:
72
+ if current.value == value:
73
+ # If it's the head
74
+ if current == self.head:
75
+ self.head = current.next
76
+ if self.head:
77
+ self.head.prev = None
78
+ else:
79
+ self.tail = None
80
+ # If it's the tail
81
+ elif current == self.tail:
82
+ self.tail = current.prev
83
+ if self.tail:
84
+ self.tail.next = None
85
+ # If it's in the middle
86
+ else:
87
+ current.prev.next = current.next
88
+ current.next.prev = current.prev
89
+ return True
90
+ current = current.next
91
+ return False
@@ -0,0 +1,33 @@
1
+ from typing import Any, List, Optional
2
+ from pure_python_ds.linear.singly_linked_list import SinglyLinkedList
3
+
4
+ class HashTable:
5
+ def __init__(self, size: int = 10):
6
+ self.size = size
7
+ self.table: List[Optional[SinglyLinkedList]] = [None] * size
8
+
9
+ def _hash(self, key: str) -> int:
10
+ return sum(ord(char) for char in key) % self.size
11
+
12
+ def set(self, key: str, value: Any):
13
+ index = self._hash(key)
14
+ if not self.table[index]:
15
+ self.table[index] = SinglyLinkedList()
16
+
17
+ # Check if key exists to update, else append
18
+ current = self.table[index].head
19
+ while current:
20
+ if current.value[0] == key:
21
+ current.value = (key, value)
22
+ return
23
+ current = current.next
24
+ self.table[index].append((key, value))
25
+
26
+ def get(self, key: str) -> Any:
27
+ index = self._hash(key)
28
+ if not self.table[index]: return None
29
+ current = self.table[index].head
30
+ while current:
31
+ if current.value[0] == key: return current.value[1]
32
+ current = current.next
33
+ return None
@@ -0,0 +1,102 @@
1
+ from typing import List, TypeVar, Generic, Optional
2
+
3
+ T = TypeVar('T')
4
+
5
+ class MinHeap(Generic[T]):
6
+ def __init__(self):
7
+ self.heap: List[T] = []
8
+
9
+ def push(self, val: T) -> None:
10
+ self.heap.append(val)
11
+ self._bubble_up(len(self.heap) - 1)
12
+
13
+ def pop(self) -> Optional[T]:
14
+ if len(self.heap) == 0: return None
15
+ if len(self.heap) == 1: return self.heap.pop()
16
+
17
+ root = self.heap[0]
18
+ self.heap[0] = self.heap.pop()
19
+ self._bubble_down(0)
20
+ return root
21
+
22
+ def _bubble_up(self, index: int):
23
+ parent = (index - 1) // 2
24
+ # Min-Heap check: Child < Parent
25
+ if index > 0 and self.heap[index] < self.heap[parent]:
26
+ self.heap[index], self.heap[parent] = self.heap[parent], self.heap[index]
27
+ self._bubble_up(parent)
28
+
29
+ def _bubble_down(self, index: int):
30
+ smallest = index
31
+ left, right = 2 * index + 1, 2 * index + 2
32
+
33
+ for child in [left, right]:
34
+ if child < len(self.heap) and self.heap[child] < self.heap[smallest]:
35
+ smallest = child
36
+
37
+ if smallest != index:
38
+ self.heap[index], self.heap[smallest] = self.heap[smallest], self.heap[index]
39
+ self._bubble_down(smallest)
40
+
41
+ @property
42
+ def size(self) -> int:
43
+ return len(self.heap)
44
+ @classmethod
45
+ def heapify(cls, data: List[T]) -> 'MinHeap[T]':
46
+ """Transforms an unsorted list into a MinHeap in O(n) time."""
47
+ instance = cls()
48
+ instance.heap = list(data)
49
+ for i in range(len(instance.heap) // 2 - 1, -1, -1):
50
+ instance._bubble_down(i)
51
+ return instance
52
+
53
+ class MaxHeap(Generic[T]):
54
+ """Native Max-Heap to avoid negating values for heapq-like logic."""
55
+ def __init__(self):
56
+ self.heap: List[T] = []
57
+
58
+ def push(self, val: T) -> None:
59
+ self.heap.append(val)
60
+ self._bubble_up(len(self.heap) - 1)
61
+
62
+ def pop(self) -> Optional[T]:
63
+ if len(self.heap) == 0: return None
64
+ if len(self.heap) == 1: return self.heap.pop()
65
+
66
+ root = self.heap[0]
67
+ self.heap[0] = self.heap.pop()
68
+ self._bubble_down(0)
69
+ return root
70
+
71
+ def _bubble_up(self, index: int):
72
+ parent = (index - 1) // 2
73
+ # Max-Heap check: Child > Parent
74
+ if index > 0 and self.heap[index] > self.heap[parent]:
75
+ self.heap[index], self.heap[parent] = self.heap[parent], self.heap[index]
76
+ self._bubble_up(parent)
77
+
78
+ def _bubble_down(self, index: int):
79
+ largest = index
80
+ left, right = 2 * index + 1, 2 * index + 2
81
+
82
+ for child in [left, right]:
83
+ # Max-Heap check: Child > Parent
84
+ if child < len(self.heap) and self.heap[child] > self.heap[largest]:
85
+ largest = child
86
+
87
+ if largest != index:
88
+ self.heap[index], self.heap[largest] = self.heap[largest], self.heap[index]
89
+ self._bubble_down(largest)
90
+
91
+ @property
92
+ def size(self) -> int:
93
+ return len(self.heap)
94
+
95
+ @classmethod
96
+ def heapify(cls, data: List[T]) -> 'MaxHeap[T]':
97
+ """Transforms an unsorted list into a MaxHeap in O(n) time."""
98
+ instance = cls()
99
+ instance.heap = list(data)
100
+ for i in range(len(instance.heap) // 2 - 1, -1, -1):
101
+ instance._bubble_down(i)
102
+ return instance
@@ -0,0 +1,24 @@
1
+ from typing import Generic, TypeVar, Optional
2
+ from pure_python_ds.linear.singly_linked_list import SinglyLinkedList
3
+
4
+ T = TypeVar('T')
5
+
6
+ class Queue(Generic[T]):
7
+ """A strictly typed, memory-optimized Queue (FIFO)."""
8
+
9
+ def __init__(self):
10
+ self._container = SinglyLinkedList[T]()
11
+
12
+ def __len__(self) -> int:
13
+ return len(self._container)
14
+
15
+ def enqueue(self, value: T) -> None:
16
+ """Adds an item to the back of the queue in O(1) time."""
17
+ self._container.append(value)
18
+
19
+ def dequeue(self) -> Optional[T]:
20
+ """Removes and returns the front item in O(1) time."""
21
+ return self._container.remove_head()
22
+
23
+ def __str__(self) -> str:
24
+ return f"Queue Front -> {self._container}"
@@ -0,0 +1,106 @@
1
+ from typing import Generic, TypeVar, Optional, Generator
2
+ from pure_python_ds.nodes import ListNode
3
+
4
+ # Define the generic type variable
5
+ T = TypeVar('T')
6
+
7
+ class SinglyLinkedList(Generic[T]):
8
+ """
9
+ A strictly typed, memory-optimized Singly Linked List.
10
+ """
11
+ def __init__(self):
12
+ self.head: Optional[ListNode[T]] = None
13
+ self.tail: Optional[ListNode[T]] = None
14
+ self._length: int = 0
15
+
16
+ def __str__(self) -> str:
17
+ """
18
+ Provides a visual representation of the Linked List.
19
+ Example: Future -> Systems -> Architect -> None
20
+ """
21
+ # We leverage our own generator to seamlessly loop through the values
22
+ values = [str(node_val) for node_val in self]
23
+ values.append("None")
24
+ return " -> ".join(values)
25
+ def __len__(self) -> int:
26
+ """Allows the user to call len(linked_list) in O(1) time."""
27
+ return self._length
28
+
29
+ def append(self, value: T) -> None:
30
+ """Adds a new node to the end of the list in O(1) time."""
31
+ new_node = ListNode(value)
32
+ if not self.head or not self.tail:
33
+ self.head = new_node
34
+ self.tail = new_node
35
+ else:
36
+ self.tail.next = new_node
37
+ self.tail = new_node
38
+ self._length += 1
39
+
40
+ def prepend(self, value: T) -> None:
41
+ """Adds a new node to the beginning of the list in O(1) time."""
42
+ new_node = ListNode(value)
43
+ if not self.head or not self.tail:
44
+ self.head = new_node
45
+ self.tail = new_node
46
+ else:
47
+ new_node.next = self.head
48
+ self.head = new_node
49
+ self._length += 1
50
+
51
+ def __iter__(self) -> Generator[T, None, None]:
52
+ """
53
+ Generator-based traversal.
54
+ Allows users to run: `for val in linked_list:` with O(1) memory overhead.
55
+ """
56
+ current = self.head
57
+ while current:
58
+ yield current.value
59
+ current = current.next
60
+ def remove_head(self) -> Optional[T]:
61
+ """Removes and returns the first node's value in O(1) time."""
62
+ if not self.head:
63
+ return None
64
+
65
+ value = self.head.value
66
+ self.head = self.head.next
67
+ self._length -= 1
68
+
69
+ if self._length == 0:
70
+ self.tail = None
71
+
72
+ return value
73
+ def reverse(self):
74
+ """Reverses the list in O(n) time and O(1) space."""
75
+ prev = None
76
+ current = self.head
77
+ while current:
78
+ next_node = current.next
79
+ current.next = prev
80
+ prev = current
81
+ current = next_node
82
+ self.head = prev
83
+ def search(self, value: T) -> bool:
84
+ """Returns True if value exists in the list, else False."""
85
+ current = self.head
86
+ while current:
87
+ if current.value == value:
88
+ return True
89
+ current = current.next
90
+ return False
91
+ def remove(self, value: T) -> bool:
92
+ """Removes the first occurrence of value from the list."""
93
+ if not self.head:
94
+ return False
95
+
96
+ if self.head.value == value:
97
+ self.head = self.head.next
98
+ return True
99
+
100
+ current = self.head
101
+ while current.next:
102
+ if current.next.value == value:
103
+ current.next = current.next.next
104
+ return True
105
+ current = current.next
106
+ return False
@@ -0,0 +1,28 @@
1
+ from typing import Generic, TypeVar, Optional
2
+ from pure_python_ds.linear.singly_linked_list import SinglyLinkedList
3
+
4
+ T = TypeVar('T')
5
+
6
+ class Stack(Generic[T]):
7
+ """A strictly typed, memory-optimized Stack (LIFO)."""
8
+
9
+ def __init__(self):
10
+ self._container = SinglyLinkedList[T]()
11
+
12
+ def __len__(self) -> int:
13
+ return len(self._container)
14
+
15
+ def push(self, value: T) -> None:
16
+ """Pushes an item onto the top of the stack in O(1) time."""
17
+ self._container.prepend(value)
18
+
19
+ def pop(self) -> Optional[T]:
20
+ """Removes and returns the top item in O(1) time."""
21
+ return self._container.remove_head()
22
+
23
+ def __str__(self) -> str:
24
+ return f"Stack Top -> {self._container}"
25
+ def peek(self) -> Optional[T]:
26
+ if not self._container.head:
27
+ return None
28
+ return self._container.head.value
@@ -0,0 +1,25 @@
1
+ from typing import Generic, TypeVar, Optional, Dict
2
+
3
+ T = TypeVar('T')
4
+
5
+ class ListNode(Generic[T]):
6
+ __slots__ = ['value', 'next', 'prev']
7
+ def __init__(self, value: T):
8
+ self.value: T = value
9
+ self.next: Optional['ListNode[T]'] = None
10
+ self.prev: Optional['ListNode[T]'] = None
11
+
12
+ class TreeNode(Generic[T]):
13
+ __slots__ = ['value', 'left', 'right', 'height']
14
+ def __init__(self, value: T):
15
+ self.value: T = value
16
+ self.left: Optional['TreeNode[T]'] = None
17
+ self.right: Optional['TreeNode[T]'] = None
18
+ self.height: int = 1
19
+
20
+ class TrieNode:
21
+ # FIXED: Added is_end_of_word to slots and init
22
+ __slots__ = ['children', 'is_end_of_word']
23
+ def __init__(self):
24
+ self.children: Dict[str, 'TrieNode'] = {}
25
+ self.is_end_of_word: bool = False
@@ -0,0 +1,9 @@
1
+ from typing import List, Any, Optional
2
+
3
+ class BTreeNode:
4
+ __slots__ = ['keys', 'children', 'leaf']
5
+
6
+ def __init__(self, leaf: bool = False):
7
+ self.keys: List[Any] = []
8
+ self.children: List['BTreeNode'] = []
9
+ self.leaf = leaf
@@ -0,0 +1,16 @@
1
+ from enum import Enum
2
+ from typing import Any, Optional
3
+
4
+ class Color(Enum):
5
+ RED = 0
6
+ BLACK = 1
7
+
8
+ class RBNode:
9
+ __slots__ = ['value', 'left', 'right', 'parent', 'color']
10
+
11
+ def __init__(self, value: Any):
12
+ self.value = value
13
+ self.left: Optional['RBNode'] = None
14
+ self.right: Optional['RBNode'] = None
15
+ self.parent: Optional['RBNode'] = None
16
+ self.color = Color.RED
@@ -0,0 +1,7 @@
1
+ from .binary_tree import BinaryTree # Add this line
2
+ from .binary_search_tree import BinarySearchTree
3
+ from .avl_tree import AVLTree
4
+ from .trie import Trie
5
+ from .segment_tree import SegmentTree
6
+ from .red_black_tree import RedBlackTree
7
+ from .b_tree import BTree