diff --git a/Python/data_structures/queue/array_queue.py b/Python/data_structures/queue/array_queue.py new file mode 100644 index 00000000..71245299 --- /dev/null +++ b/Python/data_structures/queue/array_queue.py @@ -0,0 +1,133 @@ +# Python/data_structures/queue/array_queue.py + +""" +Data Structure Description: +A Queue is a linear data structure that follows the First-In, First-Out (FIFO) +principle. Elements are added (enqueued) at the rear and removed (dequeued) +from the front. This implementation uses a standard Python list as the +underlying storage. While simple, dequeuing from the beginning of a Python +list can be inefficient (O(n)) as elements need to be shifted. +""" + +# Time Complexity: +# - Enqueue (append): O(1) on average +# - Dequeue (pop(0)): O(n) because all subsequent elements must be shifted +# - Peek: O(1) +# - is_empty: O(1) +# - size: O(1) +# Space Complexity: O(n) +# - Where n is the number of elements stored in the queue. + +class ArrayQueue: + """Implements a queue using a Python list.""" + + def __init__(self): + """Initializes an empty queue.""" + self._items = [] + + def enqueue(self, item): + """Adds an item to the rear of the queue. + >>> q = ArrayQueue() + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> print(q._items) + [1, 2] + """ + self._items.append(item) + + def dequeue(self): + """Removes and returns the item from the front of the queue. + Raises IndexError if the queue is empty. + >>> q = ArrayQueue() + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> q.dequeue() + 1 + >>> q.dequeue() + 2 + >>> q.dequeue() + Traceback (most recent call last): + ... + IndexError: dequeue from empty queue + """ + if not self._items: + raise IndexError("dequeue from empty queue") + return self._items.pop(0) # Inefficient part + + def peek(self): + """Returns the item at the front of the queue without removing it. + Raises IndexError if the queue is empty. + >>> q = ArrayQueue() + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> q.peek() + 1 + >>> q.dequeue() + 1 + >>> q.peek() + 2 + >>> q.dequeue() + 2 + >>> q.peek() + Traceback (most recent call last): + ... + IndexError: peek from empty queue + """ + if not self._items: + raise IndexError("peek from empty queue") + return self._items[0] + + def is_empty(self): + """Returns True if the queue is empty, False otherwise. + >>> q = ArrayQueue() + >>> q.is_empty() + True + >>> q.enqueue(1) + >>> q.is_empty() + False + """ + return len(self._items) == 0 + + def size(self): + """Returns the number of items in the queue. + >>> q = ArrayQueue() + >>> q.size() + 0 + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> q.size() + 2 + """ + return len(self._items) + + def __len__(self): + """Allows using len(q).""" + return self.size() + + def __str__(self): + """String representation of the queue.""" + return f"ArrayQueue({self._items})" + + def __repr__(self): + """Representation of the queue.""" + return f"ArrayQueue({self._items})" + +if __name__ == "__main__": + import doctest + doctest.testmod() + + q = ArrayQueue() + print("Is queue empty?", q.is_empty()) + q.enqueue("A") + q.enqueue("B") + q.enqueue("C") + print(q) + print("Size:", len(q)) + print("Peek:", q.peek()) + print("Dequeue:", q.dequeue()) + print(q) + print("Dequeue:", q.dequeue()) + print("Is queue empty?", q.is_empty()) + print("Dequeue:", q.dequeue()) + print("Is queue empty?", q.is_empty()) + # print("Dequeue:", q.dequeue()) # This will raise IndexError \ No newline at end of file diff --git a/Python/data_structures/queue/queue_using_stacks.py b/Python/data_structures/queue/queue_using_stacks.py new file mode 100644 index 00000000..5055c0a5 --- /dev/null +++ b/Python/data_structures/queue/queue_using_stacks.py @@ -0,0 +1,155 @@ +# Python/data_structures/queue/queue_using_stacks.py + +""" +Data Structure Description: +This implements a Queue (FIFO - First-In, First-Out) using two Stacks +(LIFO - Last-In, First-Out). One stack (`_in_stack`) is used for enqueue +operations, and the other (`_out_stack`) is used for dequeue and peek +operations. Elements are moved from `_in_stack` to `_out_stack` only when +`_out_stack` is empty, ensuring the FIFO order is maintained efficiently +on average (amortized O(1) for dequeue). +""" + +# Time Complexity: +# - Enqueue: O(1) - Simply push onto the in_stack. +# - Dequeue: O(1) amortized. In the worst case (when out_stack is empty +# and in_stack has n elements), it takes O(n) to transfer elements. +# However, each element is moved only once, so over n operations, the +# average cost is O(1). +# - Peek: O(1) amortized - Same reasoning as dequeue. +# - is_empty: O(1) +# - size: O(1) +# Space Complexity: O(n) +# - Where n is the number of elements stored across both stacks. + +class QueueUsingStacks: + """Implements a FIFO queue using two LIFO stacks.""" + + def __init__(self): + """Initializes the two stacks.""" + self._in_stack = [] # Used for enqueue + self._out_stack = [] # Used for dequeue/peek + + def enqueue(self, item): + """Adds an item to the rear of the queue. + >>> q = QueueUsingStacks() + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> print(q._in_stack) + [1, 2] + """ + self._in_stack.append(item) + + def _transfer_elements(self): + """Transfers elements from in_stack to out_stack if out_stack is empty.""" + if not self._out_stack: + while self._in_stack: + self._out_stack.append(self._in_stack.pop()) + + def dequeue(self): + """Removes and returns the item from the front of the queue. + Raises IndexError if the queue is empty. + >>> q = QueueUsingStacks() + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> q.dequeue() # Transfers 1, 2 to out_stack [2, 1], pops 1 + 1 + >>> q.enqueue(3) + >>> q.dequeue() # Pops 2 from out_stack + 2 + >>> q.dequeue() # Transfers 3 to out_stack [3], pops 3 + 3 + >>> q.dequeue() + Traceback (most recent call last): + ... + IndexError: dequeue from empty queue + """ + self._transfer_elements() + if not self._out_stack: + raise IndexError("dequeue from empty queue") + return self._out_stack.pop() + + def peek(self): + """Returns the item at the front of the queue without removing it. + Raises IndexError if the queue is empty. + >>> q = QueueUsingStacks() + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> q.peek() # Transfers 1, 2 to out_stack [2, 1], returns 1 + 1 + >>> q.dequeue() + 1 + >>> q.peek() # Returns 2 from out_stack + 2 + >>> q.dequeue() + 2 + >>> q.peek() + Traceback (most recent call last): + ... + IndexError: peek from empty queue + """ + self._transfer_elements() + if not self._out_stack: + raise IndexError("peek from empty queue") + return self._out_stack[-1] # Peek at the top of out_stack + + def is_empty(self): + """Returns True if the queue is empty, False otherwise. + >>> q = QueueUsingStacks() + >>> q.is_empty() + True + >>> q.enqueue(1) + >>> q.is_empty() + False + """ + return not self._in_stack and not self._out_stack + + def size(self): + """Returns the total number of items in the queue. + >>> q = QueueUsingStacks() + >>> q.size() + 0 + >>> q.enqueue(1) + >>> q.enqueue(2) + >>> q.size() + 2 + >>> q.dequeue() + 1 + >>> q.size() + 1 + """ + return len(self._in_stack) + len(self._out_stack) + + def __len__(self): + """Allows using len(q).""" + return self.size() + + def __str__(self): + """String representation showing logical FIFO order (may be slow).""" + # Note: This is inefficient for visualization but shows the logical order + elements = list(reversed(self._out_stack)) + list(self._in_stack) + return f"QueueUsingStacks({elements})" + + def __repr__(self): + """Representation showing internal stack states.""" + return f"QueueUsingStacks(in={self._in_stack}, out={self._out_stack})" + +if __name__ == "__main__": + import doctest + doctest.testmod() + + q = QueueUsingStacks() + print("Is empty?", q.is_empty()) + q.enqueue('A') + q.enqueue('B') + print(q) + print("Peek:", q.peek()) + print("Dequeue:", q.dequeue()) + print(q) + q.enqueue('C') + print(q) + print("Size:", len(q)) + print("Dequeue:", q.dequeue()) + print("Dequeue:", q.dequeue()) + print("Is empty?", q.is_empty()) + # print("Dequeue:", q.dequeue()) # Raises IndexError \ No newline at end of file diff --git a/Python/data_structures/stacks/stack_with_min.py b/Python/data_structures/stacks/stack_with_min.py new file mode 100644 index 00000000..3119a876 --- /dev/null +++ b/Python/data_structures/stacks/stack_with_min.py @@ -0,0 +1,212 @@ +# Python/data_structures/stack/stack_with_min.py + +""" +Data Structure Description: +This implements a Stack (LIFO - Last-In, First-Out) with an additional +operation `get_min()` that returns the minimum element currently in the stack +in O(1) time complexity. + +This is achieved by using an auxiliary stack (`_min_stack`) that stores +the minimum value seen *so far* at each level of the main stack (`_main_stack`). +When pushing an element, if it's less than or equal to the current minimum +(the top of `_min_stack`), it's pushed onto `_min_stack` as well. +When popping, if the element being popped is the current minimum (top of +`_min_stack`), it's also popped from `_min_stack`. +""" + +# Time Complexity: +# - Push (append): O(1) +# - Pop: O(1) +# - Top (peek): O(1) +# - GetMin: O(1) +# - is_empty: O(1) +# - size: O(1) +# Space Complexity: O(n) +# - In the worst case (e.g., pushing decreasing numbers), the _min_stack +# can store as many elements as the _main_stack. + +class StackWithMin: + """Implements a stack supporting O(1) get_min operation.""" + + def __init__(self): + """Initializes the main stack and the auxiliary min stack.""" + self._main_stack = [] + self._min_stack = [] + + def push(self, item): + """Pushes an item onto the stack. + >>> s = StackWithMin() + >>> s.push(5) + >>> s.push(2) + >>> s.push(3) + >>> s.push(1) + >>> print(s._main_stack) + [5, 2, 3, 1] + >>> print(s._min_stack) # Stores min seen at each stage + [5, 2, 1] + """ + self._main_stack.append(item) + # Push to min_stack if it's empty or item is <= current min + if not self._min_stack or item <= self._min_stack[-1]: + self._min_stack.append(item) + + def pop(self): + """Removes and returns the top item from the stack. + Raises IndexError if the stack is empty. + >>> s = StackWithMin() + >>> s.push(5) + >>> s.push(2) + >>> s.push(1) + >>> s.pop() # Removes 1 from both + 1 + >>> print(s._main_stack) + [5, 2] + >>> print(s._min_stack) + [5, 2] + >>> s.pop() # Removes 2 from both + 2 + >>> s.push(3) + >>> s.pop() # Removes 3 from main, not min + 3 + >>> print(s._min_stack) + [5] + >>> s.pop() # Removes 5 from both + 5 + >>> s.pop() + Traceback (most recent call last): + ... + IndexError: pop from empty stack + """ + if not self._main_stack: + raise IndexError("pop from empty stack") + + item_to_pop = self._main_stack.pop() + # If the popped item is the current minimum, pop from min_stack too + if item_to_pop == self._min_stack[-1]: + self._min_stack.pop() + return item_to_pop + + def top(self): + """Returns the top item of the stack without removing it. + Raises IndexError if the stack is empty. + >>> s = StackWithMin() + >>> s.push(5) + >>> s.push(2) + >>> s.top() + 2 + >>> s.pop() + 2 + >>> s.top() + 5 + >>> s.pop() + 5 + >>> s.top() + Traceback (most recent call last): + ... + IndexError: top from empty stack + """ + if not self._main_stack: + raise IndexError("top from empty stack") + return self._main_stack[-1] + + def get_min(self): + """Returns the minimum item currently in the stack in O(1). + Raises IndexError if the stack is empty. + >>> s = StackWithMin() + >>> s.push(5) + >>> s.get_min() + 5 + >>> s.push(2) + >>> s.get_min() + 2 + >>> s.push(3) + >>> s.get_min() + 2 + >>> s.push(1) + >>> s.get_min() + 1 + >>> s.pop() + 1 + >>> s.get_min() + 2 + >>> s.pop() + 3 + >>> s.get_min() + 2 + >>> s.pop() + 2 + >>> s.get_min() + 5 + >>> s.pop() + 5 + >>> s.get_min() + Traceback (most recent call last): + ... + IndexError: get_min from empty stack + """ + if not self._min_stack: + raise IndexError("get_min from empty stack") + return self._min_stack[-1] + + def is_empty(self): + """Returns True if the stack is empty, False otherwise. + >>> s = StackWithMin() + >>> s.is_empty() + True + >>> s.push(1) + >>> s.is_empty() + False + """ + return len(self._main_stack) == 0 + + def size(self): + """Returns the number of items in the stack. + >>> s = StackWithMin() + >>> s.size() + 0 + >>> s.push(1) + >>> s.push(2) + >>> s.size() + 2 + """ + return len(self._main_stack) + + def __len__(self): + """Allows using len(s).""" + return self.size() + + def __str__(self): + """String representation of the main stack.""" + return f"StackWithMin({self._main_stack})" + + def __repr__(self): + """Representation showing internal state.""" + return f"StackWithMin(main={self._main_stack}, min={self._min_stack})" + + +if __name__ == "__main__": + import doctest + doctest.testmod() + + s = StackWithMin() + print("Is empty?", s.is_empty()) + s.push(10) + s.push(5) + s.push(15) + s.push(3) # New min + print(s) + print("Size:", len(s)) + print("Top:", s.top()) + print("Current Min:", s.get_min()) + print("Pop:", s.pop()) # Removes 3 + print(s) + print("Current Min:", s.get_min()) # Should be 5 + print("Pop:", s.pop()) # Removes 15 + print("Current Min:", s.get_min()) # Still 5 + print("Pop:", s.pop()) # Removes 5 + print("Current Min:", s.get_min()) # Should be 10 + print("Is empty?", s.is_empty()) + print("Pop:", s.pop()) # Removes 10 + print("Is empty?", s.is_empty()) + # print("Pop:", s.pop()) # Raises IndexError + # print("Get Min:", s.get_min()) # Raises IndexError \ No newline at end of file