Skip to main content

Space Complexity Analysis

This guide covers space complexity analysis, memory usage patterns, and how to analyze algorithm memory efficiency across different programming languages.

🎯 What is Space Complexity?

Space complexity measures how the memory usage of an algorithm grows as the input size increases. It helps us understand memory requirements and optimize for memory-constrained environments.

Space Complexity Notation

  • O(1): Constant space - memory usage doesn't depend on input size
  • O(log n): Logarithmic space - grows very slowly
  • O(n): Linear space - grows proportionally with input size
  • O(n log n): Linearithmic space - common in efficient sorting
  • O(n²): Quadratic space - grows quickly, common in nested data structures

🔧 Space Complexity Examples

O(1) - Constant Space

Temporary Variables

Using a few additional variables (like a temp for swapping or a counter for a loop) consumes a fixed amount of memory regardless of the input size. This is the definition of O(1) space complexity.

# Swapping two variables
def swap(a, b):
temp = a # One extra variable
a = b
b = temp
return a, b

# Finding maximum
def find_max(arr):
max_val = arr[0] # One extra variable
for num in arr:
if num > max_val:
max_val = num
return max_val

O(n) - Linear Space

# Creating a copy of array
def reverse_array(arr):
reversed_arr = [] # O(n) space
for i in range(len(arr)-1, -1, -1):
reversed_arr.append(arr[i])
return reversed_arr

# Recursive factorial (call stack)
def factorial(n):
if n <= 1:
return 1
return n * factorial(n-1) # O(n) call stack space

O(n²) - Quadratic Space

# Creating all pairs
def all_pairs(arr):
pairs = [] # O(n²) space
for i in range(len(arr)):
for j in range(i+1, len(arr)):
pairs.append((arr[i], arr[j]))
return pairs

# Adjacency matrix for graph
def create_adjacency_matrix(n):
matrix = [[0] * n for _ in range(n)] # O(n²) space
return matrix

📊 Memory Usage Comparison

Input SizeO(1)O(log n)O(n)O(n log n)O(n²)
10141033100
1001710066410,000
1,0001101,0009,9661,000,000
10,00011310,000132,877100,000,000

🎯 Language-Specific Memory Analysis

Java Memory

// Primitive types - fixed size
int num = 42; // 4 bytes
double value = 3.14; // 8 bytes
boolean flag = true; // 1 byte

// Objects - overhead + fields
String str = "Hello"; // ~12 bytes + overhead
ArrayList<Integer> list = new ArrayList<>(); // Dynamic, grows as needed

// Memory estimation
public class MemoryExample {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();

int[] array = new int[1000000]; // ~4MB
long newUsedMemory = runtime.totalMemory() - runtime.freeMemory();

System.out.println("Memory used: " + (newUsedMemory - usedMemory) + " bytes");
}
}

Python Memory

import sys

# Check object size
def get_size(obj):
return sys.getsizeof(obj)

# Lists have overhead
my_list = [1, 2, 3, 4, 5]
print(f"List size: {get_size(my_list)} bytes") # ~120 bytes

# Dictionaries have more overhead
my_dict = {"a": 1, "b": 2, "c": 3}
print(f"Dict size: {get_size(my_dict)} bytes") # ~360 bytes

# Strings are objects
my_string = "Hello, World!"
print(f"String size: {get_size(my_string)} bytes") # ~50 bytes

C Memory

#include <stdio.h>
#include <stdlib.h>

// Manual memory management
int* create_array(int size) {
int* array = (int*)malloc(size * sizeof(int));
if (array == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
return array;
}

// Memory usage tracking
void check_memory_usage() {
// Platform-specific memory checking
#ifdef __linux__
FILE* status = fopen("/proc/self/status", "r");
// Parse memory information
fclose(status);
#endif
}

🚀 Memory Optimization Strategies

In-Place Operations

# Bad: O(n) extra space
def reverse_array_extra(arr):
reversed_arr = [] # O(n) space
for i in range(len(arr)-1, -1, -1):
reversed_arr.append(arr[i])
return reversed_arr

# Good: O(1) extra space
def reverse_array_inplace(arr):
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
return arr

Generator Usage

# Bad: O(n) space for large dataset
def process_large_file_bad(filename):
with open(filename, 'r') as file:
lines = file.readlines() # Loads entire file
return [line.strip() for line in lines]

# Good: O(1) space using generator
def process_large_file_good(filename):
with open(filename, 'r') as file:
for line in file: # Process one line at a time
yield line.strip() # Generator, memory efficient

Memory Pool Pattern

// Bad: Creating many objects
public class BadExample {
public void processItems(List<Item> items) {
for (Item item : items) {
ProcessedItem processed = new ProcessedItem(item); // Many objects
process(processed);
}
}
}

// Good: Reusing objects
public class GoodExample {
private ProcessedItem reusableItem = new ProcessedItem();

public void processItems(List<Item> items) {
for (Item item : items) {
reusableItem.reset(item); // Reuse object
process(reusableItem);
}
}
}

Efficient Data Structures

# Bad: List for frequent lookups
user_permissions_list = [("alice", ["read", "write"]), ("bob", ["read"])]
def has_permission(username, permission):
for user, perms in user_permissions_list: # O(n)
if user == username:
return permission in perms
return False

# Good: Dictionary for O(1) lookups
user_permissions_dict = {
"alice": {"read", "write"},
"bob": {"read"}
}
def has_permission_optimized(username, permission):
return permission in user_permissions_dict.get(username, set()) # O(1)

🛠️ Memory Profiling

Java Profiling

// Using VisualVM or JConsole
// 1. Run application with: java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
// 2. Connect VisualVM: jvisualvm
// 3. Monitor heap usage, garbage collection, and object creation

// Programmatic memory checking
public class MemoryProfiler {
public static void logMemoryUsage(String label) {
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
long usedMemory = totalMemory - freeMemory;

System.out.println(label + ": " + (usedMemory / 1024 / 1024) + " MB");
}
}

Python Profiling

import tracemalloc
import sys

# Memory tracing
tracemalloc.start()

# Your code here
def memory_intensive_function():
large_list = [i for i in range(1000000)]
return sum(large_list)

result = memory_intensive_function()

# Get memory statistics
current, peak = tracemalloc.get_traced_memory()
print(f"Current memory usage: {current / 1024 / 1024:.2f} MB")
print(f"Peak memory usage: {peak / 1024 / 1024:.2f} MB")

# Get top memory consumers
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)

C Memory Debugging

# Using Valgrind
gcc -g program.c -o program
valgrind --tool=memcheck --leak-check=full ./program

# Using AddressSanitizer
gcc -fsanitize=address -g program.c -o program
./program

# Memory leak detection
# In code:
#include <stdlib.h>

void* safe_malloc(size_t size) {
void* ptr = malloc(size);
if (ptr == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
printf("Allocated %zu bytes at %p\n", size, ptr);
return ptr;
}

void safe_free(void* ptr) {
if (ptr != NULL) {
printf("Freeing memory at %p\n", ptr);
free(ptr);
}
}

📈 Memory Optimization Patterns

Lazy Loading

# Bad: Load everything upfront
class BadDataLoader:
def __init__(self):
self.all_data = self.load_all_data() # Loads everything

def get_user(self, user_id):
return self.all_data[user_id]

# Good: Load on demand
class GoodDataLoader:
def __init__(self):
self.cache = {}

def get_user(self, user_id):
if user_id not in self.cache:
self.cache[user_id] = self.load_user(user_id) # Load only when needed
return self.cache[user_id]

Memory-Mapped Files

// For large files, use memory mapping
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

void* map_file(const char* filename, size_t* size) {
int fd = open(filename, O_RDONLY);
if (fd == -1) return NULL;

struct stat sb;
if (fstat(fd, &sb) == -1) return NULL;

*size = sb.st_size;
void* mapped = mmap(NULL, *size, PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);

return mapped; // File mapped into memory
}

Streaming Processing

# Process large datasets without loading everything
def process_large_dataset(filename):
with open(filename, 'r') as file:
for line in file: # Process one line at a time
data = parse_line(line)
if meets_criteria(data):
yield process_data(data) # Generator, memory efficient

# Usage
for result in process_large_dataset("large_file.txt"):
handle_result(result)

🧪 Practice Problems

Easy

  1. Space Analysis: What is the space complexity of reversing a string in-place?
  2. Memory Usage: How much memory does storing 1 million integers require?
  3. Optimization: Convert O(n²) space algorithm to O(n) space.

Medium

  1. In-Place Operations: Implement in-place quicksort and analyze its space complexity.
  2. Memory Pool: Design a memory pool for frequent object allocation/deallocation.
  3. Streaming: Process a large CSV file without loading it entirely into memory.

Hard

  1. External Sorting: Design an algorithm to sort data that doesn't fit in memory.
  2. Memory Mapping: Implement file processing using memory-mapped I/O.
  3. Garbage Collection: Analyze memory usage patterns and optimize for GC efficiency.

🔗 Language-Specific Memory

📍 Visit Us

🏫 VD Computer Tuition Surat

VD Computer Tuition
📍 Address
2/66 Faram Street, Rustompura
Surat395002, Gujarat, India
📞 Phone / WhatsApp
+91 84604 41384
🌐 Website

Computer Classes & Tuition — Areas We Serve in Surat

AdajanAlthanAmroliAthwaAthwalinesBhagalBhatarBhestanCanal RoadChowkCitylightDumasGaurav PathGhod Dod RoadHaziraJahangirpuraKamrejKapodraKatargamLimbayatMagdallaMajura GateMota VarachhaNanpuraNew CitylightOlpadPalPandesaraParle PointPiplodPunaRanderRing RoadRustampuraSachinSalabatpuraSarthanaSosyo CircleUdhnaVarachhaVed RoadVesuVIP Road
📞 Call Sir💬 WhatsApp Sir