2502. Design Memory Allocator
Problem Description
You need to implement a memory allocator system that manages a memory array of size n
. Initially, all memory units in the array are free (unallocated).
The memory allocator should support two main operations:
-
Allocate: Find and reserve a contiguous block of memory
- Given a
size
and an ID (mID
), find the leftmost available block ofsize
consecutive free memory units - Mark these units with the given
mID
to indicate they're allocated - Return the starting index of the allocated block
- If no such consecutive free block exists, return
-1
- Given a
-
Free: Release all memory units associated with a given ID
- Given an
mID
, free all memory units that were allocated with this ID - Memory units with the same
mID
might be scattered across different blocks (non-contiguous) - Return the total number of memory units that were freed
- Given an
The implementation uses a simple array where:
0
represents a free memory unit- Non-zero values represent allocated memory units with their corresponding
mID
The solution approach involves:
- For allocation: Iterate through the array, counting consecutive free units until finding a block of the required size. When found, mark those units with the
mID
and return the starting index. - For freeing: Scan the entire array and set all units with the matching
mID
back to0
, counting how many units were freed.
This simulation-based approach works well given the problem's data constraints, providing a straightforward way to manage memory allocation and deallocation.
Intuition
When thinking about how to manage memory allocation, the most natural approach is to directly simulate what happens in actual memory management. Since we need to track which memory units are free and which are allocated (along with their IDs), we can use an array where each position represents a memory unit.
The key insight is that we can use 0
to represent free memory and non-zero values to represent allocated memory with specific IDs. This makes checking if a unit is free as simple as checking if array[i] == 0
.
For the allocation operation, we need to find consecutive free units. The natural way to do this is to scan from left to right (since we want the leftmost valid block), keeping a counter of how many consecutive free units we've seen. Whenever we encounter an allocated unit, we reset our counter. When our counter reaches the required size, we know we've found a valid block.
For the free operation, since memory units with the same mID
might be scattered across different locations (from multiple allocation calls), we need to scan the entire array. Any unit matching the given mID
should be freed, regardless of where it is.
The simplicity of this approach comes from directly modeling the problem statement - we're essentially creating a miniature version of how memory allocation works in real systems. The array acts as our memory space, and we manipulate it exactly as described: marking blocks as allocated when needed and freeing them when requested.
This simulation approach is feasible because the problem constraints allow for linear time operations. In real-world scenarios with larger memory spaces, more sophisticated data structures might be needed, but for this problem, the direct simulation is both intuitive and efficient enough.
Solution Approach
The implementation uses a simple array-based simulation to manage memory allocation and deallocation.
Data Structure
We use a single array self.m
of size n
to represent the memory space, where:
0
indicates a free memory unit- Non-zero values represent allocated memory units with their corresponding
mID
Initialization
def __init__(self, n: int):
self.m = [0] * n
Create an array of size n
with all elements initialized to 0
, representing that all memory units are initially free.
Allocate Method
def allocate(self, size: int, mID: int) -> int:
cnt = 0
for i, v in enumerate(self.m):
if v:
cnt = 0
else:
cnt += 1
if cnt == size:
self.m[i - size + 1 : i + 1] = [mID] * size
return i - size + 1
return -1
The allocation algorithm works as follows:
- Maintain a counter
cnt
to track consecutive free memory units - Iterate through the array from left to right
- For each position:
- If the unit is allocated (
v != 0
), reset the counter to0
- If the unit is free (
v == 0
), increment the counter - When
cnt
reaches the requiredsize
, we've found a valid block:- Calculate the starting index:
i - size + 1
- Mark all units in this block with
mID
using slice assignment - Return the starting index
- Calculate the starting index:
- If the unit is allocated (
- If no valid block is found after scanning the entire array, return
-1
Free Method
def freeMemory(self, mID: int) -> int:
ans = 0
for i, v in enumerate(self.m):
if v == mID:
self.m[i] = 0
ans += 1
return ans
The freeing algorithm is straightforward:
- Initialize a counter
ans
to track freed units - Iterate through the entire array
- For each unit that matches
mID
:- Set it back to
0
(mark as free) - Increment the counter
- Set it back to
- Return the total count of freed units
Time Complexity
- Allocate:
O(n)
- worst case requires scanning the entire array - Free:
O(n)
- always scans the entire array to find all units with the givenmID
Space Complexity
O(n)
for storing the memory array, wheren
is the size of the memory space
This simulation approach directly models the problem requirements, making it easy to understand and implement while being efficient enough for the given constraints.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with a memory allocator of size 10.
Initial State:
Memory: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] Index: 0 1 2 3 4 5 6 7 8 9
Operation 1: allocate(3, 1)
- We need to find 3 consecutive free units
- Start scanning from index 0:
- Index 0: free, cnt = 1
- Index 1: free, cnt = 2
- Index 2: free, cnt = 3 ✓ Found!
- Allocate indices 0-2 with mID = 1
- Return starting index: 0
Memory: [1, 1, 1, 0, 0, 0, 0, 0, 0, 0] Index: 0 1 2 3 4 5 6 7 8 9
Operation 2: allocate(2, 2)
- Need 2 consecutive free units
- Scan from the beginning:
- Index 0-2: allocated (mID=1), reset cnt to 0
- Index 3: free, cnt = 1
- Index 4: free, cnt = 2 ✓ Found!
- Allocate indices 3-4 with mID = 2
- Return starting index: 3
Memory: [1, 1, 1, 2, 2, 0, 0, 0, 0, 0] Index: 0 1 2 3 4 5 6 7 8 9
Operation 3: allocate(4, 3)
- Need 4 consecutive free units
- Scan:
- Index 0-4: allocated, keep resetting cnt
- Index 5: free, cnt = 1
- Index 6: free, cnt = 2
- Index 7: free, cnt = 3
- Index 8: free, cnt = 4 ✓ Found!
- Allocate indices 5-8 with mID = 3
- Return starting index: 5
Memory: [1, 1, 1, 2, 2, 3, 3, 3, 3, 0] Index: 0 1 2 3 4 5 6 7 8 9
Operation 4: free(2)
- Scan entire array for mID = 2
- Found at indices 3 and 4
- Set both to 0 (free them)
- Return count: 2
Memory: [1, 1, 1, 0, 0, 3, 3, 3, 3, 0] Index: 0 1 2 3 4 5 6 7 8 9
Operation 5: allocate(3, 4)
- Need 3 consecutive free units
- Scan:
- Index 0-2: allocated (mID=1)
- Index 3: free, cnt = 1
- Index 4: free, cnt = 2
- Index 5: allocated (mID=3), reset cnt to 0
- Continue scanning... no 3 consecutive free units found
- Return: -1 (allocation failed)
Memory: [1, 1, 1, 0, 0, 3, 3, 3, 3, 0] (unchanged) Index: 0 1 2 3 4 5 6 7 8 9
This example demonstrates:
- How the allocator finds the leftmost valid block of consecutive free memory
- How allocated blocks are marked with their mID
- How freeing memory can create non-contiguous free spaces
- How allocation can fail when no sufficiently large consecutive block exists
Solution Implementation
1class Allocator:
2 def __init__(self, n: int):
3 """
4 Initialize the allocator with n memory units.
5
6 Args:
7 n: The total number of memory units available
8 """
9 # Create an array of size n, initialized with 0 (indicating free memory)
10 # Each element represents a memory unit, 0 means free, non-zero means allocated to that ID
11 self.memory = [0] * n
12
13 def allocate(self, size: int, mID: int) -> int:
14 """
15 Allocate a contiguous block of memory of given size to the specified ID.
16
17 Args:
18 size: The number of contiguous memory units to allocate
19 mID: The ID to assign to the allocated memory block
20
21 Returns:
22 The starting index of the allocated block, or -1 if allocation fails
23 """
24 consecutive_free_count = 0
25
26 # Iterate through memory to find contiguous free blocks
27 for index, value in enumerate(self.memory):
28 if value != 0:
29 # Memory unit is occupied, reset the counter
30 consecutive_free_count = 0
31 else:
32 # Memory unit is free, increment the counter
33 consecutive_free_count += 1
34
35 # Check if we have found enough consecutive free units
36 if consecutive_free_count == size:
37 # Calculate the starting index of the block
38 start_index = index - size + 1
39
40 # Mark the memory units as allocated with the given ID
41 self.memory[start_index : index + 1] = [mID] * size
42
43 return start_index
44
45 # No suitable contiguous block found
46 return -1
47
48 def freeMemory(self, mID: int) -> int:
49 """
50 Free all memory units allocated to the specified ID.
51
52 Args:
53 mID: The ID of the memory blocks to free
54
55 Returns:
56 The number of memory units that were freed
57 """
58 freed_count = 0
59
60 # Iterate through memory and free all units with the matching ID
61 for index, value in enumerate(self.memory):
62 if value == mID:
63 # Free this memory unit by setting it to 0
64 self.memory[index] = 0
65 freed_count += 1
66
67 return freed_count
68
69
70# Your Allocator object will be instantiated and called as such:
71# obj = Allocator(n)
72# param_1 = obj.allocate(size, mID)
73# param_2 = obj.freeMemory(mID)
74
1class Allocator {
2 // Array to represent memory blocks, where 0 means free and positive values represent allocation IDs
3 private int[] memory;
4
5 /**
6 * Initialize the memory allocator with n blocks of free memory
7 * @param n The total number of memory blocks
8 */
9 public Allocator(int n) {
10 memory = new int[n];
11 }
12
13 /**
14 * Allocate a contiguous block of memory
15 * @param size The number of contiguous blocks to allocate
16 * @param mID The ID to assign to the allocated blocks
17 * @return The starting index of the allocated block, or -1 if allocation fails
18 */
19 public int allocate(int size, int mID) {
20 int consecutiveFreeBlocks = 0;
21
22 for (int i = 0; i < memory.length; i++) {
23 // If current block is occupied, reset the counter
24 if (memory[i] > 0) {
25 consecutiveFreeBlocks = 0;
26 }
27 // If current block is free, increment the counter
28 else if (++consecutiveFreeBlocks == size) {
29 // Found enough consecutive free blocks, allocate them
30 Arrays.fill(memory, i - size + 1, i + 1, mID);
31 return i - size + 1; // Return the starting index
32 }
33 }
34
35 // No suitable contiguous block found
36 return -1;
37 }
38
39 /**
40 * Free all memory blocks allocated with the given ID
41 * @param mID The ID of the blocks to free
42 * @return The number of blocks that were freed
43 */
44 public int freeMemory(int mID) {
45 int freedBlocksCount = 0;
46
47 for (int i = 0; i < memory.length; i++) {
48 // If block belongs to the given mID, free it
49 if (memory[i] == mID) {
50 memory[i] = 0; // Mark as free
51 freedBlocksCount++;
52 }
53 }
54
55 return freedBlocksCount;
56 }
57}
58
59/**
60 * Your Allocator object will be instantiated and called as such:
61 * Allocator obj = new Allocator(n);
62 * int param_1 = obj.allocate(size,mID);
63 * int param_2 = obj.freeMemory(mID);
64 */
65
1class Allocator {
2private:
3 vector<int> memory; // Memory array where 0 indicates free space, positive values indicate allocated block IDs
4
5public:
6 // Initialize allocator with n units of memory
7 Allocator(int n) {
8 memory = vector<int>(n, 0); // Initialize all memory units as free (0)
9 }
10
11 // Allocate a contiguous block of 'size' units with identifier 'mID'
12 // Returns the starting index of allocated block, or -1 if allocation fails
13 int allocate(int size, int mID) {
14 int consecutiveFree = 0; // Counter for consecutive free memory units
15
16 for (int i = 0; i < memory.size(); ++i) {
17 if (memory[i] > 0) {
18 // Current unit is occupied, reset counter
19 consecutiveFree = 0;
20 } else if (++consecutiveFree == size) {
21 // Found enough consecutive free units
22 // Mark the block as allocated with mID
23 int startIndex = i - size + 1;
24 fill(memory.begin() + startIndex, memory.begin() + i + 1, mID);
25 return startIndex;
26 }
27 }
28
29 return -1; // No suitable contiguous block found
30 }
31
32 // Free all memory units allocated with identifier 'mID'
33 // Returns the number of units freed
34 int freeMemory(int mID) {
35 int freedUnits = 0; // Counter for freed memory units
36
37 for (int i = 0; i < memory.size(); ++i) {
38 if (memory[i] == mID) {
39 memory[i] = 0; // Mark unit as free
40 ++freedUnits;
41 }
42 }
43
44 return freedUnits;
45 }
46};
47
48/**
49 * Your Allocator object will be instantiated and called as such:
50 * Allocator* obj = new Allocator(n);
51 * int param_1 = obj->allocate(size,mID);
52 * int param_2 = obj->freeMemory(mID);
53 */
54
1// Memory array to store allocation states
2// 0 indicates free memory, positive values indicate allocated memory with ID
3let memory: number[];
4
5// Initialize the allocator with n units of memory
6function initializeAllocator(n: number): void {
7 memory = Array(n).fill(0);
8}
9
10// Allocate a contiguous block of memory
11// Returns the starting index if successful, -1 if allocation fails
12function allocate(size: number, memoryId: number): number {
13 let consecutiveFreeCount = 0;
14
15 for (let i = 0; i < memory.length; i++) {
16 // Reset counter if current position is occupied
17 if (memory[i] > 0) {
18 consecutiveFreeCount = 0;
19 } else {
20 // Increment counter for free position
21 consecutiveFreeCount++;
22
23 // Check if we found enough consecutive free spaces
24 if (consecutiveFreeCount === size) {
25 // Calculate starting index of the block
26 const startIndex = i - size + 1;
27
28 // Mark all positions in the block with the memory ID
29 for (let j = startIndex; j <= i; j++) {
30 memory[j] = memoryId;
31 }
32
33 return startIndex;
34 }
35 }
36 }
37
38 // No suitable block found
39 return -1;
40}
41
42// Free all memory blocks with the specified ID
43// Returns the number of units freed
44function freeMemory(memoryId: number): number {
45 let freedCount = 0;
46
47 for (let i = 0; i < memory.length; i++) {
48 // Check if current position matches the ID to free
49 if (memory[i] === memoryId) {
50 memory[i] = 0; // Mark as free
51 freedCount++;
52 }
53 }
54
55 return freedCount;
56}
57
Time and Space Complexity
Time Complexity: O(n × q)
- The
__init__
method takesO(n)
time to initialize the memory array with zeros. - The
allocate
method iterates through the memory array once in the worst case, takingO(n)
time. The slicing operationself.m[i - size + 1 : i + 1] = [mID] * size
also takesO(size)
time, but sincesize ≤ n
, this is bounded byO(n)
. - The
freeMemory
method iterates through the entire memory array once, checking and freeing blocks with the givenmID
, takingO(n)
time. - Since each method call takes
O(n)
time and there areq
method calls in total, the overall time complexity isO(n × q)
.
Space Complexity: O(n)
- The memory array
self.m
storesn
integers, requiringO(n)
space. - The
allocate
method uses a constant amount of extra space for variables likecnt
andi
. - The
freeMemory
method uses a constant amount of extra space for variables likeans
andi
. - No additional data structures that scale with input size are used.
- Therefore, the total space complexity is
O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Starting Index Calculation
A common mistake is incorrectly calculating the starting index of the allocated block when a suitable contiguous segment is found.
Pitfall Example:
# Wrong: Using the current index as the start if consecutive_free_count == size: self.memory[i : i + size] = [mID] * size return i # This returns the END index, not the START
Correct Solution:
if consecutive_free_count == size: start_index = i - size + 1 # Calculate the actual starting position self.memory[start_index : i + 1] = [mID] * size return start_index
2. Not Resetting Counter When Encountering Allocated Memory
Failing to reset the consecutive free counter when encountering an allocated unit leads to incorrect counting of contiguous blocks.
Pitfall Example:
# Wrong: Not resetting the counter properly
for i, v in enumerate(self.memory):
if v == 0:
cnt += 1
if cnt == size:
# allocate...
# Missing reset when v != 0
Correct Solution:
for i, v in enumerate(self.memory):
if v != 0: # Allocated memory found
cnt = 0 # Reset the counter
else:
cnt += 1
if cnt == size:
# allocate...
3. Off-by-One Errors in Slice Assignment
Using incorrect slice boundaries when marking allocated memory can lead to allocating wrong memory units.
Pitfall Example:
# Wrong: Missing the last element or including extra elements self.memory[start_index : start_index + size - 1] = [mID] * size # Missing last element # or self.memory[start_index : i + 2] = [mID] * size # May include extra element
Correct Solution:
# Correct: Use proper slice boundaries [start : end+1] self.memory[start_index : i + 1] = [mID] * size # or equivalently self.memory[start_index : start_index + size] = [mID] * size
4. Attempting to Optimize Free Operation Incorrectly
Trying to stop early in the free operation when some units are found can miss scattered memory blocks with the same ID.
Pitfall Example:
# Wrong: Stopping after finding first block
def freeMemory(self, mID: int) -> int:
freed = 0
found_start = False
for i, v in enumerate(self.memory):
if v == mID:
self.memory[i] = 0
freed += 1
found_start = True
elif found_start and v != mID:
break # Wrong! Memory blocks can be non-contiguous
return freed
Correct Solution:
def freeMemory(self, mID: int) -> int:
freed = 0
# Must scan entire array as blocks can be scattered
for i, v in enumerate(self.memory):
if v == mID:
self.memory[i] = 0
freed += 1
return freed
5. Not Handling Edge Cases Properly
Failing to handle cases where size is 0 or greater than the array length.
Prevention: Add validation at the beginning of the allocate method:
def allocate(self, size: int, mID: int) -> int:
if size == 0:
return -1 # or handle as per requirements
if size > len(self.memory):
return -1 # Cannot allocate more than available memory
# ... rest of the implementation
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!