Design Memory Allocator
MedPrompt
You are given an integer n representing the size of a 0-indexed memory array. All memory units are initially free.
You have a memory allocator with the following functionalities:
- Allocate a block of
sizeconsecutive free memory units and assign it the idmID. - Free all memory units with the given id
mID.
Note that:
- Multiple blocks can be allocated to the same
mID. - You should free all the memory units with
mID, even if they were allocated in different blocks.
Implement the Allocator class:
Allocator(int n)Initializes anAllocatorobject with a memory array of sizen.int allocate(int size, int mID)Find the leftmost block ofsizeconsecutive free memory units and allocate it with the idmID. Return the block's first index. If such a block does not exist, return-1.int freeMemory(int mID)Free all memory units with the idmID. Return the number of memory units you have freed.
Example 1:
Input
["Allocator", "allocate", "allocate", "allocate", "freeMemory", "allocate", "allocate", "allocate", "freeMemory", "allocate", "freeMemory"]
[[10], [1, 1], [1, 2], [1, 3], [2], [3, 4], [1, 1], [1, 1], [1], [10, 2], [7]]
Output
[null, 0, 1, 2, 1, 3, 1, 6, 3, -1, 0]
Explanation
Allocator loc = new Allocator(10); // Initialize a memory array of size 10. All memory units are initially free.
loc.allocate(1, 1); // The leftmost block's first index is 0. The memory array becomes [1,_,_,_,_,_,_,_,_,_]. We return 0.
loc.allocate(1, 2); // The leftmost block's first index is 1. The memory array becomes [1,2,_,_,_,_,_,_,_,_]. We return 1.
loc.allocate(1, 3); // The leftmost block's first index is 2. The memory array becomes [1,2,3,_,_,_,_,_,_,_]. We return 2.
loc.freeMemory(2); // Free all memory units with mID 2. The memory array becomes [1,_, 3,_,_,_,_,_,_,_]. We return 1 since there is only 1 unit with mID 2.
loc.allocate(3, 4); // The leftmost block's first index is 3. The memory array becomes [1,_,3,4,4,4,_,_,_,_]. We return 3.
loc.allocate(1, 1); // The leftmost block's first index is 1. The memory array becomes [1,1,3,4,4,4,_,_,_,_]. We return 1.
loc.allocate(1, 1); // The leftmost block's first index is 6. The memory array becomes [1,1,3,4,4,4,1,_,_,_]. We return 6.
loc.freeMemory(1); // Free all memory units with mID 1. The memory array becomes [_,_,3,4,4,4,_,_,_,_]. We return 3 since there are 3 units with mID 1.
loc.allocate(10, 2); // We can not find any free block with 10 consecutive free memory units, so we return -1.
loc.freeMemory(7); // Free all memory units with mID 7. The memory array remains the same since there is no memory unit with mID 7. We return 0.
Constraints:
1 <= n, size, mID <= 1000- At most
1000calls will be made toallocateandfreeMemory.
Approaches
3 approaches with complexity analysis and trade-offs.
This is the most straightforward brute-force approach. It uses an array to represent memory and performs linear scans for both allocation and freeing. Allocation involves a nested loop to find a contiguous free block, which is inefficient.
Algorithm
Allocator(n):- Initialize an integer array
memoryof sizenwith all zeros.
- Initialize an integer array
allocate(size, mID):- Iterate with an index
ifrom0ton - size. - For each
i, assume a block is found (isBlockFree = true). - Start a nested loop with an index
jfromitoi + size - 1. - If
memory[j]is not0, setisBlockFree = falseand break the inner loop. - If the inner loop completes and
isBlockFreeis still true: a. Fillmemory[i]tomemory[i + size - 1]withmID. b. Returni. - If the outer loop completes without finding a suitable block, return
-1.
- Iterate with an index
freeMemory(mID):- Initialize
freedCount = 0. - Iterate with an index
ifrom0ton - 1. - If
memory[i] == mID: a. Setmemory[i] = 0. b. IncrementfreedCount. - Return
freedCount.
- Initialize
Walkthrough
A simple integer array memory of size n is used. A value of 0 indicates a free unit, while a non-zero value represents the mID of the occupying block.
-
allocate(size, mID): To find the leftmost free block, we iterate from the first possible start indexi = 0up ton - size. For eachi, a second loop checks if the block of memory fromitoi + size - 1is entirely free. If it is, we've found our spot. We then fill this block with the givenmIDand return the start indexi. If the outer loop finishes without success, it means no suitable block exists, and we return -1. -
freeMemory(mID): This operation requires a full scan of thememoryarray. We iterate from0ton-1, and for each unitmemory[i]that matches themID, we reset it to0and count it. The total count of freed units is returned.
class Allocator { private int[] memory; private int n; public Allocator(int n) { this.n = n; this.memory = new int[n]; } public int allocate(int size, int mID) { for (int i = 0; i <= n - size; i++) { boolean isFree = true; for (int j = 0; j < size; j++) { if (memory[i + j] != 0) { isFree = false; i = i + j; // Optimization to jump past the occupied block break; } } if (isFree) { for (int j = i; j < i + size; j++) { memory[j] = mID; } return i; } } return -1; } public int freeMemory(int mID) { int freedCount = 0; for (int i = 0; i < n; i++) { if (memory[i] == mID) { memory[i] = 0; freedCount++; } } return freedCount; }}Complexity
Time
- **`allocate(size, mID)`**: O(n * size). In the worst-case scenario (e.g., memory is `[1, 0, 1, 0, ...]` and we search for `size=2`), the outer loop iterates up to `n` times, and the inner loop runs `size` times for each outer iteration. - **`freeMemory(mID)`**: O(n). We must scan the entire memory array to find all units associated with the given `mID`.
Space
O(n) - We only need an array of size `n` to store the state of the memory.
Trade-offs
Pros
Simple to understand and implement.
Uses minimal space, just the memory array itself.
Cons
allocateoperation is very slow due to the nested loop structure, making it likely to exceed time limits for larger inputs.
This approach improves upon the naive simulation by optimizing the search for a free block. Instead of re-scanning for each potential starting position, it uses a single pass with a counter to find a contiguous free block of the required size.
Algorithm
Allocator(n):- Initialize an integer array
memoryof sizenwith all zeros.
- Initialize an integer array
allocate(size, mID):- Initialize a counter
consecutiveFree = 0. - Iterate through the
memoryarray with indexifrom0ton - 1. - If
memory[i]is0, incrementconsecutiveFree. Otherwise, resetconsecutiveFreeto0. - If
consecutiveFreebecomes equal tosize: a. Calculate the start index:start = i - size + 1. b. Fill the block fromstarttoiwithmID. c. Returnstart. - If the loop finishes without finding a block, return
-1.
- Initialize a counter
freeMemory(mID):- Initialize
freedCount = 0. - Iterate through the
memoryarray from0ton - 1. - If
memory[i] == mID, setmemory[i] = 0and incrementfreedCount. - Return
freedCount.
- Initialize
Walkthrough
This method still relies on a single integer array memory of size n to represent the memory state.
-
allocate(size, mID): We perform a single linear scan through thememoryarray. A counter,consecutiveFree, tracks the length of the current contiguous sequence of free units. When we encounter a free unit (memory[i] == 0), we increment the counter. If we see an occupied unit, the counter is reset to zero. As soon asconsecutiveFreereaches the desiredsize, we have found our block. The starting index of this block isi - size + 1. We then fill the block withmIDand return the start index. If we traverse the whole array without the counter reachingsize, no such block exists. -
freeMemory(mID): This operation is identical to the naive approach. We scan the entire array, free any units matchingmID, and count them.
class Allocator { private int[] memory; private int n; public Allocator(int n) { this.n = n; this.memory = new int[n]; } public int allocate(int size, int mID) { int consecutiveFree = 0; for (int i = 0; i < n; i++) { if (memory[i] == 0) { consecutiveFree++; } else { consecutiveFree = 0; } if (consecutiveFree == size) { int start = i - size + 1; for (int j = 0; j < size; j++) { memory[start + j] = mID; } return start; } } return -1; } public int freeMemory(int mID) { int freedCount = 0; for (int i = 0; i < n; i++) { if (memory[i] == mID) { memory[i] = 0; freedCount++; } } return freedCount; }}Complexity
Time
- **`allocate(size, mID)`**: O(n). The single pass to find a free block takes O(n), and filling the block takes O(size). The total complexity is O(n + size) = O(n). - **`freeMemory(mID)`**: O(n). A full scan of the memory array is still necessary.
Space
O(n) - Space is dominated by the `memory` array.
Trade-offs
Pros
The
allocateoperation is significantly faster (O(n)) than the naive approach.The implementation remains relatively simple and easy to reason about.
Cons
freeMemorystill requires a full O(n) scan of the memory, which can be slow ifnis large and only a few units need to be freed.
This is a highly optimized approach that avoids linear scans of the entire memory array. It uses a TreeMap to keep track of free memory blocks, allowing for efficient searching and updating. A HashMap is used to track which blocks are allocated to which mID, enabling fast freeing operations.
Algorithm
Allocator(n):- Initialize
memoryarray,mIDToBlocksHashMap, andfreeBlocksTreeMap. - Add an initial entry to
freeBlocks:{0, n}representing the entire memory as free.
- Initialize
allocate(size, mID):- Iterate through the
freeBlocksTreeMap to find the first entry(start, freeSize)wherefreeSize >= size. - If found:
a. Remove the entry from
freeBlocks. b. IffreeSize > size, add a new entry for the remaining part:{start + size, freeSize - size}. c. Add the new allocated block{start, size}to the list formIDinmIDToBlocks. d. Fill the corresponding segment in thememoryarray withmID. e. Returnstart. - If no such block is found, return
-1.
- Iterate through the
freeMemory(mID):- Look up
mIDinmIDToBlocks. If not found, return 0. - For each block
{start, size}to be freed: a. Addsizeto atotalFreedcounter. b. Clear the corresponding segment in thememoryarray. c. Merge the newly freed block{start, size}with any adjacent free blocks infreeBlocksby checkingfloorEntry(start - 1)andget(start + size). - Remove the
mIDentry frommIDToBlocks. - Return
totalFreed.
- Look up
Walkthrough
This approach uses specialized data structures to manage memory efficiently.
-
Data Structures:
TreeMap<Integer, Integer> freeBlocks: Maps the starting index of a free block to its size. ATreeMapkeeps keys (start indices) sorted, which helps find the leftmost block quickly and allows for efficient searching of adjacent blocks for merging.HashMap<Integer, List<int[]>> mIDToBlocks: Maps anmIDto a list of its allocated blocks, where each block is[startIndex, size].int[] memory: The underlying memory array, used for direct manipulation.
-
allocate(size, mID): We iterate through thefreeBlocksTreeMap. Since it's sorted by start index, the first entry(start, freeSize)withfreeSize >= sizeis the leftmost available block. We updatefreeBlocksby removing/resizing this block, updatemIDToBlockswith the new allocation, and fill thememoryarray. -
freeMemory(mID): We usemIDToBlocksto get all blocks for the givenmID. For each block, we free it and then add it back tofreeBlocks. The crucial step is to merge this newly freed block with any adjacent free blocks to prevent fragmentation of the free space representation. This is done by checking for a free block ending just before our new block and one starting just after.
import java.util.*; class Allocator { private int[] memory; private int n; private Map<Integer, List<int[]>> mIDToBlocks; private TreeMap<Integer, Integer> freeBlocks; public Allocator(int n) { this.n = n; this.memory = new int[n]; this.mIDToBlocks = new HashMap<>(); this.freeBlocks = new TreeMap<>(); this.freeBlocks.put(0, n); } public int allocate(int size, int mID) { Integer start = -1; Integer freeSize = -1; for (Map.Entry<Integer, Integer> entry : freeBlocks.entrySet()) { if (entry.getValue() >= size) { start = entry.getKey(); freeSize = entry.getValue(); break; } } if (start == -1) { return -1; } for (int i = start; i < start + size; i++) { memory[i] = mID; } freeBlocks.remove(start); if (freeSize > size) { freeBlocks.put(start + size, freeSize - size); } mIDToBlocks.computeIfAbsent(mID, k -> new ArrayList<>()).add(new int[]{start, size}); return start; } public int freeMemory(int mID) { if (!mIDToBlocks.containsKey(mID)) { return 0; } List<int[]> blocksToFree = mIDToBlocks.remove(mID); int totalFreed = 0; for (int[] block : blocksToFree) { int start = block[0]; int size = block[1]; totalFreed += size; for (int i = start; i < start + size; i++) { memory[i] = 0; } int currentStart = start; int currentSize = size; Map.Entry<Integer, Integer> prevBlock = freeBlocks.floorEntry(start - 1); if (prevBlock != null && prevBlock.getKey() + prevBlock.getValue() == start) { currentStart = prevBlock.getKey(); currentSize += prevBlock.getValue(); freeBlocks.remove(prevBlock.getKey()); } Integer nextBlockStart = currentStart + currentSize; if (freeBlocks.containsKey(nextBlockStart)) { currentSize += freeBlocks.get(nextBlockStart); freeBlocks.remove(nextBlockStart); } freeBlocks.put(currentStart, currentSize); } return totalFreed; }}Complexity
Time
- **`allocate(size, mID)`**: O(f + size), where `f` is the number of free blocks. In the worst case, `f` can be O(n), so the complexity is O(n). - **`freeMemory(mID)`**: O(b_m * log(f) + total_freed_size), where `b_m` is the number of blocks for the given `mID`, `f` is the number of free blocks, and `total_freed_size` is the sum of sizes of all freed blocks. The logarithmic factor comes from `TreeMap` operations.
Space
O(n + q) - `memory` array takes O(n). `freeBlocks` can have up to O(n) entries in a heavily fragmented scenario. `mIDToBlocks` can have up to `q` entries, where `q` is the number of calls.
Trade-offs
Pros
Most efficient approach, especially for scenarios with high memory fragmentation.
Avoids costly full scans of memory for both
allocateandfreeMemoryoperations.
Cons
More complex to implement, particularly the logic for merging free blocks.
Higher constant factor overhead due to the use of complex data structures like
TreeMapandHashMap.
Solutions
Solution
class Allocator { private int [] m ; public Allocator ( int n ) { m = new int [ n ]; } public int allocate ( int size , int mID ) { int cnt = 0 ; for ( int i = 0 ; i < m . length ; ++ i ) { if ( m [ i ] > 0 ) { cnt = 0 ; } else if (++ cnt == size ) { Arrays . fill ( m , i - size + 1 , i + 1 , mID ); return i - size + 1 ; } } return - 1 ; } public int free ( int mID ) { int ans = 0 ; for ( int i = 0 ; i < m . length ; ++ i ) { if ( m [ i ] == mID ) { m [ i ] = 0 ; ++ ans ; } } return ans ; } } /** * Your Allocator object will be instantiated and called as such: * Allocator obj = new Allocator(n); * int param_1 = obj.allocate(size,mID); * int param_2 = obj.free(mID); */class Allocator { public: Allocator ( int n ) { m = vector < int > ( n ); } int allocate ( int size , int mID ) { int cnt = 0 ; for ( int i = 0 ; i < m . size (); ++ i ) { if ( m [ i ]) { cnt = 0 ; } else if ( ++ cnt == size ) { fill ( i - size + 1 , i + 1 , mID ); return i - size + 1 ; } } return - 1 ; } int free ( int mID ) { int ans = 0 ; for ( int i = 0 ; i < m . size (); ++ i ) { if ( m [ i ] == mID ) { m [ i ] = 0 ; ++ ans ; } } return ans ; } private: vector < int > m ; void fill ( int from , int to , int val ) { for ( int i = from ; i < to ; ++ i ) { m [ i ] = val ; } } }; /** * Your Allocator object will be instantiated and called as such: * Allocator* obj = new Allocator(n); * int param_1 = obj->allocate(size,mID); * int param_2 = obj->free(mID); */class Allocator : def __init__ ( self , n : int ): self . m = [ 0 ] * n 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 def free ( self , mID : int ) -> int : ans = 0 for i , v in enumerate ( self . m ): if v == mID : self . m [ i ] = 0 ans += 1 return ans # Your Allocator object will be instantiated and called as such: # obj = Allocator(n) # param_1 = obj.allocate(size,mID) # param_2 = obj.free(mID)Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.