Dynamic Stack Using Resizable Array in DSA Python - Time & Space Complexity
We want to understand how fast operations like push and pop run when using a stack built on a resizable array.
Specifically, how does the time to add or remove items change as the stack grows?
Analyze the time complexity of the following dynamic stack implementation.
class DynamicStack:
def __init__(self):
self.array = [None] * 2
self.size = 0
def push(self, value):
if self.size == len(self.array):
self._resize(2 * len(self.array))
self.array[self.size] = value
self.size += 1
def pop(self):
if self.size == 0:
return None
self.size -= 1
value = self.array[self.size]
self.array[self.size] = None
if 0 < self.size <= len(self.array) // 4:
self._resize(len(self.array) // 2)
return value
def _resize(self, new_capacity):
new_array = [None] * new_capacity
for i in range(self.size):
new_array[i] = self.array[i]
self.array = new_array
This code shows a stack that grows and shrinks its array size as needed.
Look for loops and repeated steps that take time.
- Primary operation: Copying elements during resizing in
_resize. - How many times: Happens only when array is full or too empty, not every push/pop.
Most pushes and pops take constant time, but resizing copies all elements.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 simple steps, occasional 10-step copy |
| 100 | Mostly 100 simple steps, occasional 100-step copy |
| 1000 | Mostly 1000 simple steps, occasional 1000-step copy |
Pattern observation: Most operations are quick, but resizing causes longer steps occasionally.
Time Complexity: O(1) amortized
This means each push or pop usually takes a fixed small time, even though resizing sometimes takes longer.
[X] Wrong: "Every push or pop takes time proportional to the stack size because of resizing."
[OK] Correct: Resizing happens rarely, so most operations are fast. Over many operations, the average time stays constant.
Understanding amortized time helps you explain why dynamic arrays and stacks are efficient in real programs.
"What if we tripled the array size instead of doubling it? How would the time complexity change?"
Practice
Solution
Step 1: Understand fixed-size array stack limitation
A fixed-size array stack has a set capacity and cannot grow beyond it, causing overflow if exceeded.Step 2: Analyze dynamic stack behavior
A dynamic stack using a resizable array automatically increases or decreases its size when needed, avoiding overflow and wasted space.Final Answer:
It can grow or shrink automatically as elements are pushed or popped. -> Option DQuick Check:
Dynamic resizing = automatic growth/shrink [OK]
- Thinking dynamic stack never resizes
- Confusing dynamic stack with linked list stack
- Assuming fixed capacity for dynamic stack
Solution
Step 1: Understand array resizing in Python list
To double the size, we add extra None elements equal to current length to the list.Step 2: Check each option
self.array.extend([None] * len(self.array)) correctly extends the list by adding None elements equal to current length, doubling size.Final Answer:
self.array.extend([None] * len(self.array)) -> Option AQuick Check:
Extend with None * current length = double size [OK]
- Using multiplication on list copies elements, not size
- Assigning new list loses old elements
- Appending array inside itself causes nested list
def push(self, item):
if self.top == len(self.array):
self.array.extend([None] * len(self.array))
self.array[self.top] = item
self.top += 1
stack = DynamicStack()
stack.push(10)
stack.push(20)
stack.push(30)
print(stack.array[:stack.top])
What will be the printed output?Solution
Step 1: Trace push calls and array state
Initially array is empty or small. On first push, item 10 is placed at index 0, top becomes 1. Similarly for 20 and 30 at indices 1 and 2.Step 2: Print array slice up to top
Printing array[:top] shows only elements at indices 0,1,2 which are [10, 20, 30].Final Answer:
[10, 20, 30] -> Option AQuick Check:
Print array slice up to top = pushed elements [OK]
- Printing full array including None values
- Confusing top index with length
- Expecting error due to resizing
def pop(self):
if self.top == 0:
return None
self.top -= 1
item = self.array[self.top]
self.array[self.top] = None
if self.top < len(self.array) // 4:
self.array = self.array[:len(self.array) // 2]
return item
What is the main issue with this code?Solution
Step 1: Understand shrinking condition
The array shrinks when top is less than one-fourth of array length, which can cause frequent resizing if elements fluctuate near this threshold.Step 2: Identify problem with frequent resizing
Frequent shrinking and expanding wastes time and resources, so better to shrink less aggressively.Final Answer:
It shrinks the array too early, causing frequent resizing. -> Option CQuick Check:
Shrink threshold too high = frequent resize [OK]
- Ignoring empty stack check (it is present)
- Confusing shrinking with doubling
- Not clearing popped element
def pop(self):
if self.top == 0:
return None
self.top -= 1
item = self.array[self.top]
self.array[self.top] = None
if ???:
self.array = self.array[:len(self.array) // 2]
return itemSolution
Step 1: Understand shrinking threshold
To shrink only when usage is less than 1/8, the condition must check if top is less than array length divided by 8.Step 2: Match condition to code
if self.top < len(self.array) // 8 correctly uses 'if self.top < len(self.array) // 8' to trigger shrinking at 1/8 usage.Final Answer:
if self.top < len(self.array) // 8 -> Option BQuick Check:
Shrink when usage < 1/8 = condition in if self.top < len(self.array) // 8 [OK]
- Using 1/4 threshold shrinks too early
- Using equality instead of less than
- Shrinking when stack is empty only
Ready to prove it?
Pass the test and this pattern moves unknown
