Skip to content

Commit 2ffb804

Browse files
authored
Miguel 3.5 - Sort Stack [Python] (#86)
1 parent 7d1e655 commit 2ffb804

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
+273
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
"""3.5 - Sort Stack
2+
Write a program to sort a stack such that the
3+
smallest items are on the top. You can use an additional
4+
temporary stack, but you may not copy the elements into
5+
any other data structure (such as an array). The stack
6+
supports the following operations
7+
push, pop, peek, and isEmpty
8+
"""
9+
10+
import copy
11+
import unittest
12+
import sys
13+
14+
from abc import abstractmethod
15+
from dataclasses import dataclass
16+
from typing import Generic, TypeVar
17+
from typing import List, Optional, Iterator
18+
19+
from typing import Protocol
20+
21+
T = TypeVar('T', bound='Comparable')
22+
23+
class Comparable(Protocol):
24+
@abstractmethod
25+
def __gt__(self, other: T) -> bool:
26+
pass
27+
28+
@dataclass
29+
class StackNode(Generic[T]):
30+
data: T
31+
next: 'Optional[StackNode[T]]'
32+
33+
class MyStack(Generic[T]):
34+
"""Stack data structure implementation.
35+
Uses LIFO (last-in first-out) ordering.
36+
The most recent item added to the stack is
37+
the first removed. Traversal is top to bottom.
38+
"""
39+
40+
class MyStackIterator(Iterator[T]):
41+
def __init__(self, top: Optional[StackNode[T]], size: int):
42+
self.index = -1
43+
self.current_node = top
44+
self._size = size
45+
46+
def __next__(self) -> T:
47+
self.index += 1
48+
if self.index == self._size or self.current_node is None:
49+
raise StopIteration
50+
n: T = self.current_node.data
51+
self.current_node = self.current_node.next
52+
return n
53+
54+
def __init__(self, *numbers: T):
55+
self.top: Optional[StackNode[T]] = None # top is a pointer to StackNode object
56+
self._size: int = 0
57+
for num in numbers:
58+
self.push(num)
59+
60+
def pop(self) -> T:
61+
"""
62+
Removes the top item from the stack
63+
Raises:
64+
IndexError: raised when pop is attempted on empty stack
65+
Returns:
66+
int: The data at the top of the stack
67+
"""
68+
if self.top is None:
69+
raise IndexError('Stack is Empty.')
70+
item = self.top.data
71+
self.top = self.top.next
72+
self._size -= 1
73+
return item
74+
75+
def push(self, item: T) -> None:
76+
"""
77+
Adds an item to the top of the stack
78+
Args:
79+
item (T): data we want at the top of stack
80+
"""
81+
t: StackNode[T] = StackNode(item, None)
82+
t.next = self.top
83+
self.top = t
84+
self._size += 1
85+
86+
def peek(self) -> T:
87+
"""
88+
Returns data at the top of the stack
89+
Raises:
90+
IndexError: [description]
91+
Returns:
92+
int: the value at the top of the stack
93+
"""
94+
if self.top is None:
95+
raise IndexError('Stack is Empty')
96+
return self.top.data
97+
98+
def __iter__(self) -> MyStackIterator[T]:
99+
"""
100+
Builds a list of the current stack state.
101+
For example, given the following stack:
102+
3 -> 2 -> 1, where 3 is the top,
103+
Expect:
104+
[3, 2, 1]
105+
Returns:
106+
List[int]: list of integers
107+
"""
108+
return self.MyStackIterator(self.top, self._size)
109+
110+
def __bool__(self) -> bool:
111+
"""
112+
True is returned when the container is not empty.
113+
From https://docs.python.org/3/reference/datamodel.html#object.__bool__ :
114+
Called to implement truth value testing and the built-in operation bool();
115+
should return False or True. When this method is not defined, len() is called,
116+
if it is defined, and the object is considered true if its result is nonzero.
117+
If a class defines neither len() nor bool(), all its instances are considered true.
118+
Returns:
119+
bool: False when empty, True otherwise
120+
"""
121+
return self._size > 0
122+
123+
def __len__(self) -> int:
124+
return self._size
125+
126+
def __str__(self) -> str:
127+
if self._size == 0:
128+
return '<Empty>'
129+
values = []
130+
n = self.top
131+
while n and n.next:
132+
values.append(str(n.data))
133+
n = n.next
134+
if n:
135+
values.append(str(n.data))
136+
return '->'.join(values)
137+
138+
139+
class TestMyStack(unittest.TestCase, Generic[T]):
140+
def test_stack_push(self) -> None:
141+
s: MyStack[T] = MyStack()
142+
self.assertEqual(len(s), 0)
143+
self.assertEqual(s.top, None)
144+
s.push(2)
145+
self.assertEqual(len(s), 1)
146+
self.assertEqual(s.top.data, 2)
147+
self.assertEqual(s.top.next, None)
148+
s.push(3)
149+
self.assertEqual(len(s), 2)
150+
self.assertEqual(s.top.data, 3)
151+
self.assertEqual(s.top.next.data, 2)
152+
s.push(4)
153+
self.assertEqual(len(s), 3)
154+
self.assertEqual(s.top.data, 4)
155+
self.assertEqual(s.top.next.data, 3)
156+
self.assertEqual(list(s), [4, 3, 2])
157+
158+
# test adding different types, (float and int)
159+
s.push(1.2)
160+
self.assertEqual(len(s), 4)
161+
self.assertEqual(s.top.data, 1.2)
162+
self.assertEqual(s.top.next.data, 4)
163+
self.assertEqual(list(s), [1.2, 4, 3, 2])
164+
165+
def test_stack_peek(self) -> None:
166+
s: MyStack[T] = MyStack()
167+
with self.assertRaises(IndexError):
168+
s.peek()
169+
s.push(1)
170+
s.push(2)
171+
s.push(99)
172+
top_val = s.peek()
173+
self.assertEqual(top_val, 99)
174+
175+
def test_stack_pop(self) -> None:
176+
# first case, attempt to pop an empty stack
177+
s: MyStack[T] = MyStack()
178+
with self.assertRaises(IndexError):
179+
s.pop()
180+
s.push(1)
181+
s.push(2)
182+
s.push(3)
183+
# size is 3
184+
self.assertEqual(list(s), [3, 2, 1])
185+
val = s.pop()
186+
self.assertEqual(val, 3)
187+
self.assertEqual(s._size, 2) # size should now be 2
188+
self.assertEqual(list(s), [2, 1])
189+
190+
def test__bool__(self) -> None:
191+
s: MyStack[T] = MyStack()
192+
self.assertFalse(s)
193+
s.push(3)
194+
self.assertTrue(s)
195+
196+
197+
def sorted_stack(stack: MyStack[T]) -> None:
198+
"""This function will take in a stack
199+
and modify the input stack to be sorted such
200+
that the smallest elements are at the top.
201+
Runtime:
202+
Worst Case: O(n^2)
203+
Best Case: O(n)
204+
Avg. Case: O(n^2)
205+
Space: O(n) where n is the number of elements in the input stack.
206+
207+
Args:
208+
stack (MyStack): stack of items
209+
"""
210+
# create temporary auxiliary stack
211+
num_ops = 0
212+
aux_stack: MyStack[T] = MyStack()
213+
while stack:
214+
t: T = stack.pop()
215+
num_ops += 1
216+
while aux_stack and aux_stack.peek() > t:
217+
num_ops += 1
218+
stack.push(aux_stack.pop())
219+
aux_stack.push(t)
220+
# elements are in order with highest values
221+
# on top. Need to put elements back into original stack.
222+
print("num core operations (operations dependent on size of stack) when n is {}: {}".format(len(aux_stack), num_ops))
223+
while aux_stack:
224+
stack.push(aux_stack.pop())
225+
226+
227+
class TestSortStack(unittest.TestCase, Generic[T]):
228+
def test_sort_stack_average_case(self) -> None:
229+
s: MyStack[T] = MyStack(1, 9, 5, 7, 3, 8)
230+
# will look like this (leftmost is top of stack):
231+
# [8, 3, 7, 5, 9, 1]
232+
self.assertEqual(list(s), [8, 3, 7, 5, 9, 1])
233+
# after sorting, should look like this (smallest values on top):
234+
# [1, 3, 5, 7, 8, 9]
235+
print("Sorting stack average case")
236+
sorted_stack(s)
237+
self.assertEqual(list(s), [1, 3, 5, 7, 8, 9])
238+
239+
def test_sort_stack_ascending_order_worst_case(self):
240+
# ascending order runtime is worst case with a complexity
241+
# of O(n^2).
242+
# Why? Because for every element e in stack of size n,
243+
# we will need to shift more elements as we get closer
244+
# to sorting completion and the number of operations
245+
# increases parabolically.
246+
s: MyStack[T] = MyStack(1, 2, 3, 4, 5)
247+
# will look like this (leftmost is top of stack):
248+
# [5, 4, 3, 2, 1]
249+
self.assertEqual(list(s), [5, 4, 3, 2, 1])
250+
# after sorting, should look like this (smallest values on top):
251+
# [1, 2, 3, 4, 5]
252+
print("Sorting stack ascending order (worst case)")
253+
sorted_stack(s)
254+
self.assertEqual(list(s), [1, 2, 3, 4, 5])
255+
256+
def test_sort_stack_descending_order_best_case(self) -> None:
257+
# smallest items will be on top of stack. Basically,
258+
# already sorted.
259+
# with a stack in already sorted order, algorithm
260+
# will act the fastest.
261+
s: MyStack[T] = MyStack(5, 4, 3, 2, 1)
262+
# will look like this (leftmost is top of stack):
263+
# [1, 2, 3, 4]
264+
self.assertEqual(list(s), [1, 2, 3, 4, 5])
265+
# after sorting, should look like this (smallest values on top):
266+
# [1, 2, 3, 4]
267+
print("Sorting stack ascending order (best case)")
268+
sorted_stack(s)
269+
self.assertEqual(list(s), [1, 2, 3, 4, 5])
270+
271+
272+
if __name__ == '__main__':
273+
unittest.main()

0 commit comments

Comments
 (0)