Пространственная сложность — это количество дополнительной памяти, которую использует алгоритм в зависимости от размера входных данных. Мы измеряем auxiliary space — память сверх входных данных.
Два вида пространственной сложности:
Total space = входные данные + дополнительная память
Auxiliary space = только дополнительная память (обычно это то, что нас интересует)
Из чего складывается потребление памяти
1. Переменные и примитивы
function example(int $n): void
{
$x = 5; // O(1) — one number
$name = 'hello'; // O(1) — fixed string
$flag = true; // O(1) — boolean
// All together: O(1)
}
func example(n int) {
x := 5 // O(1) — one number
name := "hello" // O(1) — fixed string
flag := true // O(1) — boolean
// All together: O(1)
_, _, _ = x, name, flag
}
static void Example(int n)
{
int x = 5; // O(1) — one number
string name = "hello"; // O(1) — fixed string
bool flag = true; // O(1) — boolean
// All together: O(1)
Console.WriteLine($"{x} {name} {flag}");
}
def example(n: int) -> None:
x: int = 5 # O(1) — one number
name: str = "hello" # O(1) — fixed string
flag: bool = True # O(1) — boolean
# All together: O(1)
print(x, name, flag)
### 2. Структуры данных
function example(int $n): void
{
$arr = array_fill(0, $n, 0); // O(n) — array of size n
$matrix = [];
for ($i = 0; $i < $n; $i++) {
$matrix[$i] = array_fill(0, $n, 0); // O(n²) — matrix n x n
}
$map = []; // O(k), where k — number of added elements
}
func example(n int) {
arr := make([]int, n) // O(n) — slice of size n
matrix := make([][]int, n)
for i := 0; i < n; i++ {
matrix[i] = make([]int, n) // O(n²) — matrix n x n
}
m := make(map[string]int) // O(k), where k — number of added elements
_, _, _ = arr, matrix, m
}
static void Example(int n)
{
var arr = new int[n]; // O(n) — array of size n
var matrix = new int[n][];
for (int i = 0; i < n; i++)
{
matrix[i] = new int[n]; // O(n²) — matrix n x n
}
var map = new Dictionary<string, int>(); // O(k), where k — number of added elements
Console.WriteLine($"{arr.Length} {matrix.Length} {map.Count}");
}
def example(n: int) -> None:
arr = [0] * n # O(n) — list of size n
matrix = [[0] * n for _ in range(n)] # O(n²) — matrix n x n
mapping: dict[str, int] = {} # O(k), k — number of added elements
print(len(arr), len(matrix), len(mapping))
### 3. Стек вызовов (рекурсия)
function recursive(int $n): void
{
if ($n <= 0) {
return;
}
recursive($n - 1);
}
// Call stack: n frames -> O(n) memory
func recursive(n int) {
if n <= 0 {
return
}
recursive(n - 1)
}
// Call stack: n frames -> O(n) memory
// Find maximum — only one variable
function findMax(array $arr): int
{
$maxVal = $arr[0]; // O(1) extra memory
foreach ($arr as $x) {
if ($x > $maxVal) {
$maxVal = $x;
}
}
return $maxVal;
}
// Find maximum — only one variable
func findMax(arr []int) int {
maxVal := arr[0] // O(1) extra memory
for _, x := range arr {
if x > maxVal {
maxVal = x
}
}
return maxVal
}
// Find maximum — only one variable
static int FindMax(int[] arr)
{
int maxVal = arr[0]; // O(1) extra memory
foreach (int x in arr)
{
if (x > maxVal)
{
maxVal = x;
}
}
return maxVal;
}
# Find maximum — only one variable
def find_max(arr: list[int]) -> int:
max_val = arr[0] # O(1) extra memory
for x in arr:
if x > max_val:
max_val = x
return max_val
// Reverse array in-place — O(1)
function reverseInplace(array &$arr): void
{
$left = 0;
$right = count($arr) - 1;
while ($left < $right) {
[$arr[$left], $arr[$right]] = [$arr[$right], $arr[$left]];
$left++;
$right--;
}
// Array modified, no extra memory allocated
}
// Reverse slice in-place — O(1)
func reverseInplace(arr []int) {
left, right := 0, len(arr)-1
for left < right {
arr[left], arr[right] = arr[right], arr[left]
left++
right--
}
// Slice modified, no extra memory allocated
}
// Reverse array in-place — O(1)
static void ReverseInplace(int[] arr)
{
int left = 0;
int right = arr.Length - 1;
while (left < right)
{
(arr[left], arr[right]) = (arr[right], arr[left]);
left++;
right--;
}
// Array modified, no extra memory allocated
}
# Reverse list in-place — O(1)
def reverse_inplace(arr: list[int]) -> None:
left, right = 0, len(arr) - 1
while left < right:
arr[left], arr[right] = arr[right], arr[left]
left += 1
right -= 1
# List modified, no extra memory allocated
# Note: arr[::-1] would allocate a new list — that is O(n)
---
O(n) — линейная память
// Creating a new array
function doubleValues(array $arr): array
{
$result = []; // New array
foreach ($arr as $x) {
$result[] = $x * 2;
}
return $result; // result holds n elements -> O(n)
}
// Creating a new slice
func doubleValues(arr []int) []int {
result := make([]int, 0, len(arr)) // New slice
for _, x := range arr {
result = append(result, x*2)
}
return result // result holds n elements -> O(n)
}
// Creating a new list
static List<int> DoubleValues(int[] arr)
{
var result = new List<int>(arr.Length); // New list
foreach (int x in arr)
{
result.Add(x * 2);
}
return result; // result holds n elements -> O(n)
}
# Creating a new list
def double_values(arr: list[int]) -> list[int]:
return [x * 2 for x in arr] # New list of n elements -> O(n)
// Hash table for frequency counting
function frequencyCount(array $arr): array
{
$freq = []; // Worst case: n unique elements
foreach ($arr as $x) {
$freq[$x] = ($freq[$x] ?? 0) + 1;
}
return $freq; // O(n)
}
// Map for frequency counting
func frequencyCount(arr []int) map[int]int {
freq := make(map[int]int) // Worst case: n unique elements
for _, x := range arr {
freq[x]++
}
return freq // O(n)
}
// Dictionary for frequency counting
static Dictionary<int, int> FrequencyCount(int[] arr)
{
var freq = new Dictionary<int, int>(); // Worst case: n unique elements
foreach (int x in arr)
{
freq[x] = freq.GetValueOrDefault(x) + 1;
}
return freq; // O(n)
}
from collections import Counter
# Counter for frequency counting
def frequency_count(arr: list[int]) -> dict[int, int]:
return dict(Counter(arr)) # Worst case: n unique keys -> O(n)
---
O(n²) — квадратичная память
// Adjacency matrix of a graph
function createAdjacencyMatrix(int $n): array
{
$matrix = [];
for ($i = 0; $i < $n; $i++) {
$matrix[$i] = array_fill(0, $n, 0); // n x n -> O(n²)
}
return $matrix;
}
// Adjacency matrix of a graph
func createAdjacencyMatrix(n int) [][]int {
matrix := make([][]int, n)
for i := 0; i < n; i++ {
matrix[i] = make([]int, n) // n x n -> O(n²)
}
return matrix
}
// Adjacency matrix of a graph
static int[][] CreateAdjacencyMatrix(int n)
{
var matrix = new int[n][];
for (int i = 0; i < n; i++)
{
matrix[i] = new int[n]; // n x n -> O(n²)
}
return matrix;
}
# Adjacency matrix of a graph
def create_adjacency_matrix(n: int) -> list[list[int]]:
# Comprehension per row — [[0] * n] * n would share one row object!
return [[0] * n for _ in range(n)] # n x n -> O(n²)
// All pair sums (for caching results)
func allPairSums(arr []int) [][]int {
n := len(arr)
result := make([][]int, n)
for i := 0; i < n; i++ {
result[i] = make([]int, n)
for j := 0; j < n; j++ {
result[i][j] = arr[i] + arr[j]
}
}
return result // O(n²)
}
// All pair sums (for caching results)
static int[][] AllPairSums(int[] arr)
{
int n = arr.Length;
var result = new int[n][];
for (int i = 0; i < n; i++)
{
result[i] = new int[n];
for (int j = 0; j < n; j++)
{
result[i][j] = arr[i] + arr[j];
}
}
return result; // O(n²)
}
# All pair sums (for caching results)
def all_pair_sums(arr: list[int]) -> list[list[int]]:
return [[a + b for b in arr] for a in arr] # O(n²)
---
Рекурсия и стек вызовов
// Linear recursion: O(n) memory
function sumRecursive(array $arr, int $index = 0): int
{
if ($index === count($arr)) {
return 0;
}
return $arr[$index] + sumRecursive($arr, $index + 1);
}
// Stack: n frames -> O(n)
// Linear recursion: O(n) memory
func sumRecursive(arr []int, index int) int {
if index == len(arr) {
return 0
}
return arr[index] + sumRecursive(arr, index+1)
}
// Stack: n frames -> O(n)
// Linear recursion: O(n) memory
static int SumRecursive(int[] arr, int index = 0)
{
if (index == arr.Length)
{
return 0;
}
return arr[index] + SumRecursive(arr, index + 1);
}
// Stack: n frames -> O(n)
# Linear recursion: O(n) memory
def sum_recursive(arr: list[int], index: int = 0) -> int:
if index == len(arr):
return 0
return arr[index] + sum_recursive(arr, index + 1)
# Stack: n frames -> O(n)
// Recursion with two branches: O(n) memory (not O(2^n)!)
function fib(int $n): int
{
if ($n <= 1) {
return $n;
}
return fib($n - 1) + fib($n - 2);
}
// Time: O(2^n), but MEMORY: O(n)
// Because the stack holds only ONE branch at a time!
// Recursion with two branches: O(n) memory (not O(2^n)!)
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
// Time: O(2^n), but MEMORY: O(n)
// Because the stack holds only ONE branch at a time!
// Recursion with two branches: O(n) memory (not O(2^n)!)
static int Fib(int n)
{
if (n <= 1)
{
return n;
}
return Fib(n - 1) + Fib(n - 2);
}
// Time: O(2^n), but MEMORY: O(n)
// Because the stack holds only ONE branch at a time!
# Recursion with two branches: O(n) memory (not O(2^n)!)
def fib(n: int) -> int:
if n <= 1:
return n
return fib(n - 1) + fib(n - 2)
# Time: O(2^n), but MEMORY: O(n)
# Because the stack holds only ONE branch at a time!
Это важный момент! Дерево рекурсии Фибоначчи имеет O(2^n) узлов, но максимальная глубина стека — n:
Стек в момент самого глубокого вызова fib(5):
Шаг 1: fib(5) -> fib(4) -> fib(3) -> fib(2) -> fib(1)
Стек: [fib(5), fib(4), fib(3), fib(2), fib(1)] — глубина 5
Шаг 2: fib(1) вернул 1, fib(2) вызывает fib(0)
Стек: [fib(5), fib(4), fib(3), fib(2), fib(0)] — глубина 5
Максимальная глубина стека = n -> O(n) по памяти
// Tail recursion (PHP does NOT optimize it)
function sumTail(array $arr, int $index = 0, int $acc = 0): int
{
if ($index === count($arr)) {
return $acc;
}
return sumTail($arr, $index + 1, $acc + $arr[$index]);
}
// Theoretically O(1), but PHP still uses O(n) stack
// Tail recursion (Go does NOT optimize it either)
func sumTail(arr []int, index, acc int) int {
if index == len(arr) {
return acc
}
return sumTail(arr, index+1, acc+arr[index])
}
// Theoretically O(1), but Go still uses O(n) stack
// Tail recursion (the C# compiler does NOT guarantee TCO)
static int SumTail(int[] arr, int index = 0, int acc = 0)
{
if (index == arr.Length)
{
return acc;
}
return SumTail(arr, index + 1, acc + arr[index]);
}
// Theoretically O(1), but the CLR still uses O(n) stack here
# Tail recursion (Python does NOT optimize it either)
def sum_tail(arr: list[int], index: int = 0, acc: int = 0) -> int:
if index == len(arr):
return acc
return sum_tail(arr, index + 1, acc + arr[index])
# Theoretically O(1), but Python still uses O(n) stack
# and raises RecursionError past sys.getrecursionlimit()
// Iterative version — O(1) memory
function sumArray(array $arr): int
{
$acc = 0;
foreach ($arr as $x) {
$acc += $x;
}
return $acc;
}
// Iterative version — O(1) memory
func sumArray(arr []int) int {
acc := 0
for _, x := range arr {
acc += x
}
return acc
}
// Iterative version — O(1) memory
static int SumArray(int[] arr)
{
int acc = 0;
foreach (int x in arr)
{
acc += x;
}
return acc;
}
# Iterative version — O(1) memory
def sum_array(arr: list[int]) -> int:
acc = 0
for x in arr:
acc += x
return acc
## In-place алгоритмы
In-place алгоритм использует O(1) дополнительной памяти (или O(log n) для рекурсии).
func insertionSort(arr []int) {
n := len(arr)
for i := 1; i < n; i++ {
key := arr[i]
j := i - 1
for j >= 0 && arr[j] > key {
arr[j+1] = arr[j]
j--
}
arr[j+1] = key
}
// Original slice modified
// Extra memory: O(1) (variables key, i, j)
}
static void InsertionSort(int[] arr)
{
int n = arr.Length;
for (int i = 1; i < n; i++)
{
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
// Original array modified
// Extra memory: O(1) (variables key, i, j)
}
def insertion_sort(arr: list[int]) -> None:
for i in range(1, len(arr)):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
# Original list modified
# Extra memory: O(1) (variables key, i, j)
### Пример: merge sort — НЕ in-place
function mergeSort(array $arr): array
{
if (count($arr) <= 1) {
return $arr;
}
$mid = intdiv(count($arr), 2);
$left = mergeSort(array_slice($arr, 0, $mid)); // Creates new array!
$right = mergeSort(array_slice($arr, $mid)); // Creates new array!
return merge($left, $right); // Another array!
}
// Extra memory: O(n) for merging + O(log n) stack = O(n)
func mergeSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
mid := len(arr) / 2
left := mergeSort(arr[:mid]) // Creates new slice!
right := mergeSort(arr[mid:]) // Creates new slice!
return merge(left, right) // Another slice!
}
// Extra memory: O(n) for merging + O(log n) stack = O(n)
static List<int> MergeSort(List<int> arr)
{
if (arr.Count <= 1)
{
return arr;
}
int mid = arr.Count / 2;
var left = MergeSort(arr.GetRange(0, mid)); // Creates new list!
var right = MergeSort(arr.GetRange(mid, arr.Count - mid)); // Creates new list!
return Merge(left, right); // Another list!
}
// Extra memory: O(n) for merging + O(log n) stack = O(n)
def merge_sort(arr: list[int]) -> list[int]:
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid]) # Slicing creates a new list!
right = merge_sort(arr[mid:]) # Creates a new list!
return merge(left, right) # Another list!
# Extra memory: O(n) for merging + O(log n) stack = O(n)
### Сравнение: in-place vs not in-place
Алгоритм
Время
Доп. память
In-place?
Bubble Sort
O(n²)
O(1)
Да
Insertion Sort
O(n²)
O(1)
Да
Selection Sort
O(n²)
O(1)
Да
Merge Sort
O(n log n)
O(n)
Нет
Quick Sort
O(n log n)
O(log n)*
Да
Heap Sort
O(n log n)
O(1)
Да
Counting Sort
O(n + k)
O(k)
Нет
* — O(log n) для стека рекурсии
Trade-off: время vs память
Часто можно обменять память на скорость и наоборот.
Пример: проверка дубликатов
// Approach 1: O(1) memory, O(n²) time
function hasDupBrute(array $arr): bool
{
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
if ($arr[$i] === $arr[$j]) {
return true;
}
}
}
return false;
}
// Approach 2: O(n) memory, O(n) time
function hasDupSet(array $arr): bool
{
$seen = [];
foreach ($arr as $x) {
if (isset($seen[$x])) {
return true;
}
$seen[$x] = true;
}
return false;
}
// Approach 3: O(1) memory*, O(n log n) time
function hasDupSort(array &$arr): bool
{
sort($arr); // Modifies the original array!
for ($i = 1; $i < count($arr); $i++) {
if ($arr[$i] === $arr[$i - 1]) {
return true;
}
}
return false;
}
// Approach 1: O(1) memory, O(n²) time
func hasDupBrute(arr []int) bool {
n := len(arr)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if arr[i] == arr[j] {
return true
}
}
}
return false
}
// Approach 2: O(n) memory, O(n) time
func hasDupSet(arr []int) bool {
seen := make(map[int]bool)
for _, x := range arr {
if seen[x] {
return true
}
seen[x] = true
}
return false
}
// Approach 3: O(1) memory*, O(n log n) time
func hasDupSort(arr []int) bool {
sort.Ints(arr) // Modifies the original slice!
for i := 1; i < len(arr); i++ {
if arr[i] == arr[i-1] {
return true
}
}
return false
}
// Approach 1: O(1) memory, O(n²) time
static bool HasDupBrute(int[] arr)
{
int n = arr.Length;
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (arr[i] == arr[j])
{
return true;
}
}
}
return false;
}
// Approach 2: O(n) memory, O(n) time
static bool HasDupSet(int[] arr)
{
var seen = new HashSet<int>();
foreach (int x in arr)
{
if (!seen.Add(x)) // Add returns false if already present
{
return true;
}
}
return false;
}
// Approach 3: O(1) memory*, O(n log n) time
static bool HasDupSort(int[] arr)
{
Array.Sort(arr); // Modifies the original array!
for (int i = 1; i < arr.Length; i++)
{
if (arr[i] == arr[i - 1])
{
return true;
}
}
return false;
}
from itertools import pairwise
# Approach 1: O(1) memory, O(n²) time
def has_dup_brute(arr: list[int]) -> bool:
n = len(arr)
for i in range(n):
for j in range(i + 1, n):
if arr[i] == arr[j]:
return True
return False
# Approach 2: O(n) memory, O(n) time
def has_dup_set(arr: list[int]) -> bool:
seen: set[int] = set()
for x in arr:
if x in seen:
return True
seen.add(x)
return False
# Approach 3: O(1) memory*, O(n log n) time
def has_dup_sort(arr: list[int]) -> bool:
arr.sort() # In-place — modifies the original list! (sorted() would copy)
# pairwise is a lazy iterator: no extra list is allocated
return any(a == b for a, b in pairwise(arr))
### Пример: Two Sum
// Approach 1: O(1) memory, O(n²) time
function twoSumBrute(array $arr, int $target): array
{
$n = count($arr);
for ($i = 0; $i < $n; $i++) {
for ($j = $i + 1; $j < $n; $j++) {
if ($arr[$i] + $arr[$j] === $target) {
return [$i, $j];
}
}
}
return [];
}
// Approach 2: O(n) memory, O(n) time
function twoSumHash(array $arr, int $target): array
{
$seen = [];
foreach ($arr as $i => $num) {
$complement = $target - $num;
if (isset($seen[$complement])) {
return [$seen[$complement], $i];
}
$seen[$num] = $i;
}
return [];
}
// Approach 1: O(1) memory, O(n²) time
func twoSumBrute(arr []int, target int) [2]int {
n := len(arr)
for i := 0; i < n; i++ {
for j := i + 1; j < n; j++ {
if arr[i]+arr[j] == target {
return [2]int{i, j}
}
}
}
return [2]int{-1, -1}
}
// Approach 2: O(n) memory, O(n) time
func twoSumHash(arr []int, target int) [2]int {
seen := make(map[int]int)
for i, num := range arr {
complement := target - num
if j, ok := seen[complement]; ok {
return [2]int{j, i}
}
seen[num] = i
}
return [2]int{-1, -1}
}
// Approach 1: O(1) memory, O(n²) time
static (int, int)? TwoSumBrute(int[] arr, int target)
{
int n = arr.Length;
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (arr[i] + arr[j] == target)
{
return (i, j);
}
}
}
return null;
}
// Approach 2: O(n) memory, O(n) time
static (int, int)? TwoSumHash(int[] arr, int target)
{
var seen = new Dictionary<int, int>();
for (int i = 0; i < arr.Length; i++)
{
int complement = target - arr[i];
if (seen.TryGetValue(complement, out int j))
{
return (j, i);
}
seen[arr[i]] = i;
}
return null;
}
# Approach 1: O(1) memory, O(n²) time
def two_sum_brute(arr: list[int], target: int) -> tuple[int, int] | None:
n = len(arr)
for i in range(n):
for j in range(i + 1, n):
if arr[i] + arr[j] == target:
return i, j
return None
# Approach 2: O(n) memory, O(n) time
def two_sum_hash(arr: list[int], target: int) -> tuple[int, int] | None:
seen: dict[int, int] = {}
for i, num in enumerate(arr):
complement = target - num
if complement in seen:
return seen[complement], i
seen[num] = i
return None
## Частые ловушки
Ловушка 1: срезы массива создают копии
function process(array $arr): void
{
$half = array_slice($arr, 0, intdiv(count($arr), 2)); // Creates a COPY! O(n) memory
// ...
}
// Better: use indices
function processInplace(array $arr, int $start, int $end): void
{
for ($i = $start; $i < $end; $i++) {
// work with $arr[$i]
}
}
func process(arr []int) {
half := make([]int, len(arr)/2)
copy(half, arr[:len(arr)/2]) // Explicit copy! O(n) memory
// ...
_ = half
}
// Better: use slice expressions (no copy, shares underlying array)
func processInplace(arr []int, start, end int) {
sub := arr[start:end] // No copy — shares memory
for _, v := range sub {
_ = v // work with v
}
}
static void Process(int[] arr)
{
var half = arr[..(arr.Length / 2)]; // Range operator on an array COPIES! O(n) memory
Console.WriteLine(half.Length);
}
// Better: Span<T> / ReadOnlySpan<T> is a view, it allocates nothing
static void ProcessInplace(int[] arr, int start, int end)
{
ReadOnlySpan<int> sub = arr.AsSpan(start, end - start); // No copy — shares memory
foreach (int v in sub)
{
Console.WriteLine(v); // work with v
}
}
from itertools import islice
def process(arr: list[int]) -> None:
half = arr[: len(arr) // 2] # Slicing a list creates a COPY! O(n) memory
print(len(half))
# Better: memoryview / islice give a view without copying
def process_inplace(arr: list[int], start: int, end: int) -> None:
for v in islice(arr, start, end): # Lazy iterator — no copy
print(v) # work with v
### Ловушка 2: Конкатенация строк
// O(n²) memory and time!
function buildStringBad(int $n): string
{
$result = '';
for ($i = 0; $i < $n; $i++) {
$result .= $i; // Creates a new string each time!
}
return $result;
}
// O(n) — correct approach
function buildStringGood(int $n): string
{
$parts = [];
for ($i = 0; $i < $n; $i++) {
$parts[] = $i;
}
return implode('', $parts);
}
// O(n²) memory and time!
func buildStringBad(n int) string {
result := ""
for i := 0; i < n; i++ {
result += fmt.Sprintf("%d", i) // Creates a new string each time!
}
return result
}
// O(n) — correct approach using strings.Builder
func buildStringGood(n int) string {
var b strings.Builder
for i := 0; i < n; i++ {
fmt.Fprintf(&b, "%d", i)
}
return b.String()
}
// O(n²) memory and time!
static string BuildStringBad(int n)
{
string result = "";
for (int i = 0; i < n; i++)
{
result += i; // Strings are immutable — a new one is allocated each time!
}
return result;
}
// O(n) — correct approach using StringBuilder
static string BuildStringGood(int n)
{
var sb = new StringBuilder();
for (int i = 0; i < n; i++)
{
sb.Append(i);
}
return sb.ToString();
}
# O(n²) memory and time!
def build_string_bad(n: int) -> str:
result = ""
for i in range(n):
result += str(i) # Strings are immutable — a new one each time!
return result
# O(n) — correct approach: collect parts and join once
def build_string_good(n: int) -> str:
return "".join(str(i) for i in range(n))
### Ловушка 3: Рекурсия на больших данных
// May cause StackOverflow for large n
function deepRecursion(int $n): int
{
if ($n <= 0) {
return 0;
}
return 1 + deepRecursion($n - 1);
}
// Solution: iterative version
function iterativeVersion(int $n): int
{
$count = 0;
while ($n > 0) {
$count++;
$n--;
}
return $count;
}
// May cause stack overflow for large n
// (Go goroutine stacks grow dynamically but still have limits)
func deepRecursion(n int) int {
if n <= 0 {
return 0
}
return 1 + deepRecursion(n-1)
}
// Solution: iterative version
func iterativeVersion(n int) int {
count := 0
for n > 0 {
count++
n--
}
return count
}
// May cause StackOverflowException for large n
// (in .NET it cannot be caught — the process is terminated)
static int DeepRecursion(int n)
{
if (n <= 0)
{
return 0;
}
return 1 + DeepRecursion(n - 1);
}
// Solution: iterative version
static int IterativeVersion(int n)
{
int count = 0;
while (n > 0)
{
count++;
n--;
}
return count;
}
# Raises RecursionError for large n (default limit ~1000 frames)
def deep_recursion(n: int) -> int:
if n <= 0:
return 0
return 1 + deep_recursion(n - 1)
# Solution: iterative version
def iterative_version(n: int) -> int:
count = 0
while n > 0:
count += 1
n -= 1
return count
## Таблица: память для типичных структур данных
Структура
Память
Массив (n элементов)
O(n)
Матрица (n x m)
O(n*m)
HashMap (n пар)
O(n)
Связный список (n узлов)
O(n)
Бинарное дерево (n узлов)
O(n)
Стек рекурсии (глубина d)
O(d)
Граф (V вершин, E рёбер)
O(V + E)
Trie (с p символами)
O(p * alphabet_size)
Запомни: Пространственная сложность — это не только явные структуры данных. Не забывай про стек вызовов при рекурсии! Рекурсия с глубиной n потребляет O(n) памяти, даже если вы не создаёте массивов. Когда выбираешь между решениями — учитывай trade-off между временем и памятью.
Итоги
Считай дополнительную память, а не входные данные
Стек рекурсии = O(глубина рекурсии) памяти
In-place алгоритмы: O(1) доп. памяти
Срезы и конкатенация строк создают копии — осторожно!
Trade-off: часто можно обменять O(n) памяти на ускорение с O(n²) до O(n)
Для больших данных предпочитай итерацию рекурсии
Проверь себя
5 из 8
PHP не оптимизирует хвостовую рекурсию. Что это значит на практике?
Какой из подходов к проверке дубликатов использует O(1) дополнительной памяти?
В Go оптимизации хвостовых вызовов нет. Чем тогда ограничена глубина рекурсии в горутине?
Merge Sort имеет временную сложность O(n log n). Какова его пространственная сложность?
Какова пространственная сложность наивной рекурсивной функции Фибоначчи fib(n)?