Space Complexity
5 min readSpace Complexity
TL;DR
Space complexity measures how much memory an algorithm needs as input grows, and it's the other half of the performance picture next to time. Senior engineers actively trade between the two — caching turns O(n) lookups into O(1) at the cost of O(n) memory, while LINQ deferred execution, in-place mutation, ArrayPool<T>, and iterative-instead-of-recursive rewrites keep peak allocations flat and the GC quiet on hot paths.
How it works
Space Complexity — Memory Usage
"Space complexity measures memory used by an algorithm relative to input size—critical for large datasets."
❌ Bad example:
public class TradeTransformer
{
public List<Trade> FilterAndTransform(List<Trade> trades)
{
// O(n) space - creates 3 intermediate collections
var filtered = trades.Where(t => t.Amount > 1000).ToList(); // copy 1
var sorted = filtered.OrderBy(t => t.Price).ToList(); // copy 2
var projected = sorted.Select(t => new Trade
{
Id = t.Id,
Symbol = t.Symbol,
Price = t.Price * 1.05m
}).ToList(); // copy 3
return projected; // 3 full copies in memory simultaneously
}
}
// Processing 1 million trades: ~3 million Trade objects in memory
Multiple intermediate collections waste memory—especially bad for large datasets.
✅ Good example:
public class TradeTransformer
{
public IEnumerable<Trade> FilterAndTransform(IEnumerable<Trade> trades)
{
// O(1) space (excluding result) - streaming with deferred execution
return trades
.Where(t => t.Amount > 1000) // no allocation
.OrderBy(t => t.Price) // O(n) when enumerated
.Select(t => new Trade
{
Id = t.Id,
Symbol = t.Symbol,
Price = t.Price * 1.05m
}); // creates items on demand
}
// If materialization is needed, single allocation
public List<Trade> FilterAndTransformMaterialized(List<Trade> trades)
{
return trades
.Where(t => t.Amount > 1000)
.OrderBy(t => t.Price)
.Select(t => new Trade
{
Id = t.Id,
Symbol = t.Symbol,
Price = t.Price * 1.05m
})
.ToList(); // single allocation for result
}
}
👉 LINQ deferred execution avoids intermediate collections—only final result is allocated.
🔥 In-place operations (O(1) space):
public class ArrayProcessor
{
public void ReverseArray(int[] array)
{
// O(1) space - modifies in-place
int left = 0, right = array.Length - 1;
while (left < right)
{
int temp = array[left];
array[left] = array[right];
array[right] = temp;
left++;
right--;
}
}
public void SortInPlace(List<Trade> trades)
{
// O(log n) space - quicksort recursion stack
trades.Sort((a, b) => a.Price.CompareTo(b.Price));
}
}
👉 In-place algorithms minimize memory allocation—critical for embedded systems or low-latency paths.
🔥 Trading space for time:
public class PriceCache
{
// O(n) space - cache all prices for O(1) lookups
private Dictionary<string, decimal> _priceCache = new();
public void LoadPrices(List<(string Symbol, decimal Price)> prices)
{
// O(n) space - store all prices
foreach (var (symbol, price) in prices)
{
_priceCache[symbol] = price;
}
}
public decimal GetPrice(string symbol)
{
// O(1) time - instant lookup
return _priceCache.TryGetValue(symbol, out var price) ? price : 0;
}
}
// Alternative: no cache, query each time
public class PriceLookupNoCache
{
private List<(string Symbol, decimal Price)> _prices;
public decimal GetPrice(string symbol)
{
// O(1) space, but O(n) time - must scan list
return _prices.FirstOrDefault(p => p.Symbol == symbol).Price;
}
}
👉 Caching uses O(n) space but reduces lookups from O(n) to O(1) time.
🔥 Recursive space complexity:
public class TreeProcessor
{
public int CalculateDepth(TreeNode node)
{
// O(h) space - recursion stack depth
// h = tree height (log n for balanced, n for skewed)
if (node == null) return 0;
return 1 + Math.Max(
CalculateDepth(node.Left),
CalculateDepth(node.Right)
);
}
// Iterative alternative - explicit stack
public int CalculateDepthIterative(TreeNode root)
{
// O(h) space - explicit stack
if (root == null) return 0;
var stack = new Stack<(TreeNode Node, int Depth)>();
stack.Push((root, 1));
int maxDepth = 0;
while (stack.Count > 0)
{
var (node, depth) = stack.Pop();
maxDepth = Math.Max(maxDepth, depth);
if (node.Left != null) stack.Push((node.Left, depth + 1));
if (node.Right != null) stack.Push((node.Right, depth + 1));
}
return maxDepth;
}
}
👉 Recursion uses call stack—O(h) where h is recursion depth.
🔥 Avoiding memory leaks:
public class EventProcessor
{
// ❌ Bad: event handlers create memory leaks
public void ProcessEvents(IEventSource source)
{
source.OnEvent += (s, e) => HandleEvent(e); // captures 'this', prevents GC
}
// ✅ Good: explicit unsubscribe
public void ProcessEventsCorrectly(IEventSource source)
{
EventHandler<Event> handler = (s, e) => HandleEvent(e);
source.OnEvent += handler;
// Later: unsubscribe
source.OnEvent -= handler;
}
// ✅ Alternative: weak references for long-lived publishers
public void ProcessEventsWeak(IEventSource source)
{
var weakHandler = new WeakEventHandler(this, source);
}
private void HandleEvent(Event e) { /* ... */ }
}
👉 Unmanaged subscriptions create unbounded space growth—always clean up.
💡 In trading systems:
- Use streaming (IEnumerable
) for large result sets to avoid loading everything. - Cache frequently accessed reference data (symbols, limits) for O(1) access.
- Profile memory with tools—identify allocations in hot paths causing GC pressure.
Quick recall Q&A
Time measures computational steps; space measures memory used. Some algorithms trade one for the other (caching uses space to save time).
No. O(1) means constant memory regardless of input size. A few variables or a fixed-size buffer is O(1), even if it allocates.
Depends. Deferred queries (Where, Select) are O(1) until materialized. ToList() is O(n). OrderBy() allocates O(n) for sorting.
(1) Use streaming instead of materializing, (2) Operate in-place when possible, (3) Reuse buffers with ArrayPool
O(d) where d is recursion depth. Each call adds a stack frame. Deep recursion can cause StackOverflowException—use iteration for unbounded depth.
No. O(0) doesn't exist. Minimum is O(1) (constant space for variables). Algorithms always use some memory.
O(n) where n is the final string length. StringBuilder allocates a buffer and resizes as needed, but total space scales with content.
Space complexity measures peak memory. GC reclaims unused objects, but high allocation rates cause GC pressure. Minimize allocations in hot paths.
ArrayPool
Use profilers (dotMemory, PerfView) to track allocations. Look for O(n) growth in object count or heap size as input scales.