1+ # Python/data_structures/stack/stack_with_min.py
2+
3+ """
4+ Data Structure Description:
5+ This implements a Stack (LIFO - Last-In, First-Out) with an additional
6+ operation `get_min()` that returns the minimum element currently in the stack
7+ in O(1) time complexity.
8+
9+ This is achieved by using an auxiliary stack (`_min_stack`) that stores
10+ the minimum value seen *so far* at each level of the main stack (`_main_stack`).
11+ When pushing an element, if it's less than or equal to the current minimum
12+ (the top of `_min_stack`), it's pushed onto `_min_stack` as well.
13+ When popping, if the element being popped is the current minimum (top of
14+ `_min_stack`), it's also popped from `_min_stack`.
15+ """
16+
17+ # Time Complexity:
18+ # - Push (append): O(1)
19+ # - Pop: O(1)
20+ # - Top (peek): O(1)
21+ # - GetMin: O(1)
22+ # - is_empty: O(1)
23+ # - size: O(1)
24+ # Space Complexity: O(n)
25+ # - In the worst case (e.g., pushing decreasing numbers), the _min_stack
26+ # can store as many elements as the _main_stack.
27+
28+ class StackWithMin :
29+ """Implements a stack supporting O(1) get_min operation."""
30+
31+ def __init__ (self ):
32+ """Initializes the main stack and the auxiliary min stack."""
33+ self ._main_stack = []
34+ self ._min_stack = []
35+
36+ def push (self , item ):
37+ """Pushes an item onto the stack.
38+ >>> s = StackWithMin()
39+ >>> s.push(5)
40+ >>> s.push(2)
41+ >>> s.push(3)
42+ >>> s.push(1)
43+ >>> print(s._main_stack)
44+ [5, 2, 3, 1]
45+ >>> print(s._min_stack) # Stores min seen at each stage
46+ [5, 2, 1]
47+ """
48+ self ._main_stack .append (item )
49+ # Push to min_stack if it's empty or item is <= current min
50+ if not self ._min_stack or item <= self ._min_stack [- 1 ]:
51+ self ._min_stack .append (item )
52+
53+ def pop (self ):
54+ """Removes and returns the top item from the stack.
55+ Raises IndexError if the stack is empty.
56+ >>> s = StackWithMin()
57+ >>> s.push(5)
58+ >>> s.push(2)
59+ >>> s.push(1)
60+ >>> s.pop() # Removes 1 from both
61+ 1
62+ >>> print(s._main_stack)
63+ [5, 2]
64+ >>> print(s._min_stack)
65+ [5, 2]
66+ >>> s.pop() # Removes 2 from both
67+ 2
68+ >>> s.push(3)
69+ >>> s.pop() # Removes 3 from main, not min
70+ 3
71+ >>> print(s._min_stack)
72+ [5]
73+ >>> s.pop() # Removes 5 from both
74+ 5
75+ >>> s.pop()
76+ Traceback (most recent call last):
77+ ...
78+ IndexError: pop from empty stack
79+ """
80+ if not self ._main_stack :
81+ raise IndexError ("pop from empty stack" )
82+
83+ item_to_pop = self ._main_stack .pop ()
84+ # If the popped item is the current minimum, pop from min_stack too
85+ if item_to_pop == self ._min_stack [- 1 ]:
86+ self ._min_stack .pop ()
87+ return item_to_pop
88+
89+ def top (self ):
90+ """Returns the top item of the stack without removing it.
91+ Raises IndexError if the stack is empty.
92+ >>> s = StackWithMin()
93+ >>> s.push(5)
94+ >>> s.push(2)
95+ >>> s.top()
96+ 2
97+ >>> s.pop()
98+ 2
99+ >>> s.top()
100+ 5
101+ >>> s.pop()
102+ 5
103+ >>> s.top()
104+ Traceback (most recent call last):
105+ ...
106+ IndexError: top from empty stack
107+ """
108+ if not self ._main_stack :
109+ raise IndexError ("top from empty stack" )
110+ return self ._main_stack [- 1 ]
111+
112+ def get_min (self ):
113+ """Returns the minimum item currently in the stack in O(1).
114+ Raises IndexError if the stack is empty.
115+ >>> s = StackWithMin()
116+ >>> s.push(5)
117+ >>> s.get_min()
118+ 5
119+ >>> s.push(2)
120+ >>> s.get_min()
121+ 2
122+ >>> s.push(3)
123+ >>> s.get_min()
124+ 2
125+ >>> s.push(1)
126+ >>> s.get_min()
127+ 1
128+ >>> s.pop()
129+ 1
130+ >>> s.get_min()
131+ 2
132+ >>> s.pop()
133+ 3
134+ >>> s.get_min()
135+ 2
136+ >>> s.pop()
137+ 2
138+ >>> s.get_min()
139+ 5
140+ >>> s.pop()
141+ 5
142+ >>> s.get_min()
143+ Traceback (most recent call last):
144+ ...
145+ IndexError: get_min from empty stack
146+ """
147+ if not self ._min_stack :
148+ raise IndexError ("get_min from empty stack" )
149+ return self ._min_stack [- 1 ]
150+
151+ def is_empty (self ):
152+ """Returns True if the stack is empty, False otherwise.
153+ >>> s = StackWithMin()
154+ >>> s.is_empty()
155+ True
156+ >>> s.push(1)
157+ >>> s.is_empty()
158+ False
159+ """
160+ return len (self ._main_stack ) == 0
161+
162+ def size (self ):
163+ """Returns the number of items in the stack.
164+ >>> s = StackWithMin()
165+ >>> s.size()
166+ 0
167+ >>> s.push(1)
168+ >>> s.push(2)
169+ >>> s.size()
170+ 2
171+ """
172+ return len (self ._main_stack )
173+
174+ def __len__ (self ):
175+ """Allows using len(s)."""
176+ return self .size ()
177+
178+ def __str__ (self ):
179+ """String representation of the main stack."""
180+ return f"StackWithMin({ self ._main_stack } )"
181+
182+ def __repr__ (self ):
183+ """Representation showing internal state."""
184+ return f"StackWithMin(main={ self ._main_stack } , min={ self ._min_stack } )"
185+
186+
187+ if __name__ == "__main__" :
188+ import doctest
189+ doctest .testmod ()
190+
191+ s = StackWithMin ()
192+ print ("Is empty?" , s .is_empty ())
193+ s .push (10 )
194+ s .push (5 )
195+ s .push (15 )
196+ s .push (3 ) # New min
197+ print (s )
198+ print ("Size:" , len (s ))
199+ print ("Top:" , s .top ())
200+ print ("Current Min:" , s .get_min ())
201+ print ("Pop:" , s .pop ()) # Removes 3
202+ print (s )
203+ print ("Current Min:" , s .get_min ()) # Should be 5
204+ print ("Pop:" , s .pop ()) # Removes 15
205+ print ("Current Min:" , s .get_min ()) # Still 5
206+ print ("Pop:" , s .pop ()) # Removes 5
207+ print ("Current Min:" , s .get_min ()) # Should be 10
208+ print ("Is empty?" , s .is_empty ())
209+ print ("Pop:" , s .pop ()) # Removes 10
210+ print ("Is empty?" , s .is_empty ())
211+ # print("Pop:", s.pop()) # Raises IndexError
212+ # print("Get Min:", s.get_min()) # Raises IndexError
0 commit comments