Facebook Pixel

2502. Design Memory Allocator

MediumDesignArrayHash TableSimulation
Leetcode Link

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:

  1. Allocate: Find and reserve a contiguous block of memory

    • Given a size and an ID (mID), find the leftmost available block of size 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
  2. 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

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 to 0, 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Maintain a counter cnt to track consecutive free memory units
  2. Iterate through the array from left to right
  3. For each position:
    • If the unit is allocated (v != 0), reset the counter to 0
    • If the unit is free (v == 0), increment the counter
    • When cnt reaches the required size, 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
  4. 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:

  1. Initialize a counter ans to track freed units
  2. Iterate through the entire array
  3. For each unit that matches mID:
    • Set it back to 0 (mark as free)
    • Increment the counter
  4. 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 given mID

Space Complexity

  • O(n) for storing the memory array, where n 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 Evaluator

Example 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 takes O(n) time to initialize the memory array with zeros.
  • The allocate method iterates through the memory array once in the worst case, taking O(n) time. The slicing operation self.m[i - size + 1 : i + 1] = [mID] * size also takes O(size) time, but since size ≤ n, this is bounded by O(n).
  • The freeMemory method iterates through the entire memory array once, checking and freeing blocks with the given mID, taking O(n) time.
  • Since each method call takes O(n) time and there are q method calls in total, the overall time complexity is O(n × q).

Space Complexity: O(n)

  • The memory array self.m stores n integers, requiring O(n) space.
  • The allocate method uses a constant amount of extra space for variables like cnt and i.
  • The freeMemory method uses a constant amount of extra space for variables like ans and i.
  • 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More