Files
Yudong Jin 22b3b568ef fix Ru translation (#1894)
* docs(ru): replace prose quotes with guillemets

* docs(ru): replace prose semicolons with periods

* docs(ru): align animation title forms

* docs(ru): align figure and table references
2026-04-14 18:10:12 +08:00

881 lines
33 KiB
Markdown

# Пространственная сложность
<u>Пространственная сложность (space complexity)</u> служит для оценки того, как меняется объем памяти, требуемой алгоритму, по мере роста объема данных. Это понятие очень похоже на временную сложность, только вместо времени выполнения рассматривается объем используемой памяти.
## Пространство, связанное с алгоритмом
Память, которую использует алгоритм во время работы, в основном делится на следующие части.
- **Входное пространство**: используется для хранения входных данных алгоритма.
- **Временное пространство**: используется для хранения переменных, объектов, контекста функций и других данных, возникающих во время выполнения алгоритма.
- **Выходное пространство**: используется для хранения выходных данных алгоритма.
Как правило, при анализе пространственной сложности в расчет включают временное пространство и выходное пространство.
Временное пространство можно дополнительно разделить на три части.
- **Временные данные**: используются для хранения различных констант, переменных, объектов и т.д., возникающих во время выполнения алгоритма.
- **Пространство кадров стека**: используется для хранения контекстных данных вызываемых функций. При каждом вызове функции система создает на вершине стека новый кадр. После возврата функции пространство этого кадра освобождается.
- **Пространство инструкций**: используется для хранения скомпилированных инструкций программы и в реальном подсчете обычно не учитывается.
При анализе пространственной сложности программы **обычно учитываются временные данные, пространство стека и выходные данные**, как показано на рисунке ниже.
![Пространство, используемое алгоритмом](space_complexity.assets/space_types.png)
Ниже приведен соответствующий код:
=== "Python"
```python title=""
class Node:
"""Класс"""
def __init__(self, x: int):
self.val: int = x # Значение узла
self.next: Node | None = None # Ссылка на следующий узел
def function() -> int:
"""Функция"""
# Выполнить некоторые операции...
return 0
def algorithm(n) -> int: # Входные данные
A = 0 # Временные данные (константа, обычно обозначается заглавной буквой)
b = 0 # Временные данные (переменная)
node = Node(0) # Временные данные (объект)
c = function() # Пространство кадра стека (вызов функции)
return A + b + c # Выходные данные
```
=== "C++"
```cpp title=""
/* Структура */
struct Node {
int val;
Node *next;
Node(int x) : val(x), next(nullptr) {}
};
/* Функция */
int func() {
// Выполнить некоторые операции...
return 0;
}
int algorithm(int n) { // Входные данные
const int a = 0; // Временные данные (константа)
int b = 0; // Временные данные (переменная)
Node* node = new Node(0); // Временные данные (объект)
int c = func(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "Java"
```java title=""
/* Класс */
class Node {
int val;
Node next;
Node(int x) { val = x; }
}
/* Функция */
int function() {
// Выполнить некоторые операции...
return 0;
}
int algorithm(int n) { // Входные данные
final int a = 0; // Временные данные (константа)
int b = 0; // Временные данные (переменная)
Node node = new Node(0); // Временные данные (объект)
int c = function(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "C#"
```csharp title=""
/* Класс */
class Node(int x) {
int val = x;
Node next;
}
/* Функция */
int Function() {
// Выполнить некоторые операции...
return 0;
}
int Algorithm(int n) { // Входные данные
const int a = 0; // Временные данные (константа)
int b = 0; // Временные данные (переменная)
Node node = new(0); // Временные данные (объект)
int c = Function(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "Go"
```go title=""
/* Структура */
type node struct {
val int
next *node
}
/* Создать структуру node */
func newNode(val int) *node {
return &node{val: val}
}
/* Функция */
func function() int {
// Выполнить некоторые операции...
return 0
}
func algorithm(n int) int { // Входные данные
const a = 0 // Временные данные (константа)
b := 0 // Временные данные (переменная)
newNode(0) // Временные данные (объект)
c := function() // Пространство кадра стека (вызов функции)
return a + b + c // Выходные данные
}
```
=== "Swift"
```swift title=""
/* Класс */
class Node {
var val: Int
var next: Node?
init(x: Int) {
val = x
}
}
/* Функция */
func function() -> Int {
// Выполнить некоторые операции...
return 0
}
func algorithm(n: Int) -> Int { // Входные данные
let a = 0 // Временные данные (константа)
var b = 0 // Временные данные (переменная)
let node = Node(x: 0) // Временные данные (объект)
let c = function() // Пространство кадра стека (вызов функции)
return a + b + c // Выходные данные
}
```
=== "JS"
```javascript title=""
/* Класс */
class Node {
val;
next;
constructor(val) {
this.val = val === undefined ? 0 : val; // Значение узла
this.next = null; // Ссылка на следующий узел
}
}
/* Функция */
function constFunc() {
// Выполнить некоторые операции
return 0;
}
function algorithm(n) { // Входные данные
const a = 0; // Временные данные (константа)
let b = 0; // Временные данные (переменная)
const node = new Node(0); // Временные данные (объект)
const c = constFunc(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "TS"
```typescript title=""
/* Класс */
class Node {
val: number;
next: Node | null;
constructor(val?: number) {
this.val = val === undefined ? 0 : val; // Значение узла
this.next = null; // Ссылка на следующий узел
}
}
/* Функция */
function constFunc(): number {
// Выполнить некоторые операции
return 0;
}
function algorithm(n: number): number { // Входные данные
const a = 0; // Временные данные (константа)
let b = 0; // Временные данные (переменная)
const node = new Node(0); // Временные данные (объект)
const c = constFunc(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "Dart"
```dart title=""
/* Класс */
class Node {
int val;
Node next;
Node(this.val, [this.next]);
}
/* Функция */
int function() {
// Выполнить некоторые операции...
return 0;
}
int algorithm(int n) { // Входные данные
const int a = 0; // Временные данные (константа)
int b = 0; // Временные данные (переменная)
Node node = Node(0); // Временные данные (объект)
int c = function(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "Rust"
```rust title=""
use std::rc::Rc;
use std::cell::RefCell;
/* Структура */
struct Node {
val: i32,
next: Option<Rc<RefCell<Node>>>,
}
/* Создать структуру Node */
impl Node {
fn new(val: i32) -> Self {
Self { val: val, next: None }
}
}
/* Функция */
fn function() -> i32 {
// Выполнить некоторые операции...
return 0;
}
fn algorithm(n: i32) -> i32 { // Входные данные
const a: i32 = 0; // Временные данные (константа)
let mut b = 0; // Временные данные (переменная)
let node = Node::new(0); // Временные данные (объект)
let c = function(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "C"
```c title=""
/* Функция */
int func() {
// Выполнить некоторые операции...
return 0;
}
int algorithm(int n) { // Входные данные
const int a = 0; // Временные данные (константа)
int b = 0; // Временные данные (переменная)
int c = func(); // Пространство кадра стека (вызов функции)
return a + b + c; // Выходные данные
}
```
=== "Kotlin"
```kotlin title=""
/* Класс */
class Node(var _val: Int) {
var next: Node? = null
}
/* Функция */
fun function(): Int {
// Выполнить некоторые операции...
return 0
}
fun algorithm(n: Int): Int { // Входные данные
val a = 0 // Временные данные (константа)
var b = 0 // Временные данные (переменная)
val node = Node(0) // Временные данные (объект)
val c = function() // Пространство кадра стека (вызов функции)
return a + b + c // Выходные данные
}
```
=== "Ruby"
```ruby title=""
### Класс ###
class Node
attr_accessor :val # Значение узла
attr_accessor :next # Ссылка на следующий узел
def initialize(x)
@val = x
end
end
### Функция ###
def function
# Выполнить некоторые операции...
0
end
### Алгоритм ###
def algorithm(n) # Входные данные
a = 0 # Временные данные (константа)
b = 0 # Временные данные (переменная)
node = Node.new(0) # Временные данные (объект)
c = function # Пространство кадра стека (вызов функции)
a + b + c # Выходные данные
end
```
## Метод вывода
Метод вывода пространственной сложности в целом аналогичен выводу временной сложности: меняется только объект подсчета, с количества операций на размер используемого пространства.
В отличие от временной сложности, **обычно рассматривается только худшая пространственная сложность**. Это связано с тем, что память является жестким ограничением: необходимо гарантировать, что для любых входных данных у программы будет достаточно памяти.
Рассмотрим следующий код. Понятие худшей пространственной сложности здесь имеет два значения.
1. **Ориентир на худшие входные данные**: когда $n < 10$ , пространственная сложность равна $O(1)$ ; но когда $n > 10$ , инициализированный массив `nums` занимает $O(n)$ пространства, поэтому худшая пространственная сложность равна $O(n)$ .
2. **Ориентир на пиковое использование памяти во время выполнения**: например, до выполнения последней строки программа занимает $O(1)$ пространства. При инициализации массива `nums` она занимает $O(n)$ пространства, поэтому худшая пространственная сложность также равна $O(n)$ .
=== "Python"
```python title=""
def algorithm(n: int):
a = 0 # O(1)
b = [0] * 10000 # O(1)
if n > 10:
nums = [0] * n # O(n)
```
=== "C++"
```cpp title=""
void algorithm(int n) {
int a = 0; // O(1)
vector<int> b(10000); // O(1)
if (n > 10)
vector<int> nums(n); // O(n)
}
```
=== "Java"
```java title=""
void algorithm(int n) {
int a = 0; // O(1)
int[] b = new int[10000]; // O(1)
if (n > 10)
int[] nums = new int[n]; // O(n)
}
```
=== "C#"
```csharp title=""
void Algorithm(int n) {
int a = 0; // O(1)
int[] b = new int[10000]; // O(1)
if (n > 10) {
int[] nums = new int[n]; // O(n)
}
}
```
=== "Go"
```go title=""
func algorithm(n int) {
a := 0 // O(1)
b := make([]int, 10000) // O(1)
var nums []int
if n > 10 {
nums := make([]int, n) // O(n)
}
fmt.Println(a, b, nums)
}
```
=== "Swift"
```swift title=""
func algorithm(n: Int) {
let a = 0 // O(1)
let b = Array(repeating: 0, count: 10000) // O(1)
if n > 10 {
let nums = Array(repeating: 0, count: n) // O(n)
}
}
```
=== "JS"
```javascript title=""
function algorithm(n) {
const a = 0; // O(1)
const b = new Array(10000); // O(1)
if (n > 10) {
const nums = new Array(n); // O(n)
}
}
```
=== "TS"
```typescript title=""
function algorithm(n: number): void {
const a = 0; // O(1)
const b = new Array(10000); // O(1)
if (n > 10) {
const nums = new Array(n); // O(n)
}
}
```
=== "Dart"
```dart title=""
void algorithm(int n) {
int a = 0; // O(1)
List<int> b = List.filled(10000, 0); // O(1)
if (n > 10) {
List<int> nums = List.filled(n, 0); // O(n)
}
}
```
=== "Rust"
```rust title=""
fn algorithm(n: i32) {
let a = 0; // O(1)
let b = [0; 10000]; // O(1)
if n > 10 {
let nums = vec![0; n as usize]; // O(n)
}
}
```
=== "C"
```c title=""
void algorithm(int n) {
int a = 0; // O(1)
int b[10000]; // O(1)
if (n > 10)
int nums[n] = {0}; // O(n)
}
```
=== "Kotlin"
```kotlin title=""
fun algorithm(n: Int) {
val a = 0 // O(1)
val b = IntArray(10000) // O(1)
if (n > 10) {
val nums = IntArray(n) // O(n)
}
}
```
=== "Ruby"
```ruby title=""
def algorithm(n)
a = 0 # O(1)
b = Array.new(10000) # O(1)
nums = Array.new(n) if n > 10 # O(n)
end
```
**В рекурсивных функциях необходимо учитывать пространство кадров стека**. Рассмотрим следующий код:
=== "Python"
```python title=""
def function() -> int:
# Выполнить некоторые операции
return 0
def loop(n: int):
"""Пространственная сложность цикла равна O(1)"""
for _ in range(n):
function()
def recur(n: int):
"""Пространственная сложность рекурсии равна O(n)"""
if n == 1:
return
return recur(n - 1)
```
=== "C++"
```cpp title=""
int func() {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* Пространственная сложность рекурсии равна O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Java"
```java title=""
int function() {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* Пространственная сложность рекурсии равна O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "C#"
```csharp title=""
int Function() {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
void Loop(int n) {
for (int i = 0; i < n; i++) {
Function();
}
}
/* Пространственная сложность рекурсии равна O(n) */
int Recur(int n) {
if (n == 1) return 1;
return Recur(n - 1);
}
```
=== "Go"
```go title=""
func function() int {
// Выполнить некоторые операции
return 0
}
/* Пространственная сложность цикла равна O(1) */
func loop(n int) {
for i := 0; i < n; i++ {
function()
}
}
/* Пространственная сложность рекурсии равна O(n) */
func recur(n int) {
if n == 1 {
return
}
recur(n - 1)
}
```
=== "Swift"
```swift title=""
@discardableResult
func function() -> Int {
// Выполнить некоторые операции
return 0
}
/* Пространственная сложность цикла равна O(1) */
func loop(n: Int) {
for _ in 0 ..< n {
function()
}
}
/* Пространственная сложность рекурсии равна O(n) */
func recur(n: Int) {
if n == 1 {
return
}
recur(n: n - 1)
}
```
=== "JS"
```javascript title=""
function constFunc() {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
function loop(n) {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* Пространственная сложность рекурсии равна O(n) */
function recur(n) {
if (n === 1) return;
return recur(n - 1);
}
```
=== "TS"
```typescript title=""
function constFunc(): number {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
function loop(n: number): void {
for (let i = 0; i < n; i++) {
constFunc();
}
}
/* Пространственная сложность рекурсии равна O(n) */
function recur(n: number): void {
if (n === 1) return;
return recur(n - 1);
}
```
=== "Dart"
```dart title=""
int function() {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
function();
}
}
/* Пространственная сложность рекурсии равна O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Rust"
```rust title=""
fn function() -> i32 {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
fn loop(n: i32) {
for i in 0..n {
function();
}
}
/* Пространственная сложность рекурсии равна O(n) */
fn recur(n: i32) {
if n == 1 {
return;
}
recur(n - 1);
}
```
=== "C"
```c title=""
int func() {
// Выполнить некоторые операции
return 0;
}
/* Пространственная сложность цикла равна O(1) */
void loop(int n) {
for (int i = 0; i < n; i++) {
func();
}
}
/* Пространственная сложность рекурсии равна O(n) */
void recur(int n) {
if (n == 1) return;
recur(n - 1);
}
```
=== "Kotlin"
```kotlin title=""
fun function(): Int {
// Выполнить некоторые операции
return 0
}
/* Пространственная сложность цикла равна O(1) */
fun loop(n: Int) {
for (i in 0..<n) {
function()
}
}
/* Пространственная сложность рекурсии равна O(n) */
fun recur(n: Int) {
if (n == 1) return
return recur(n - 1)
}
```
=== "Ruby"
```ruby title=""
def function
# Выполнить некоторые операции
0
end
### Пространственная сложность цикла равна O(1) ###
def loop(n)
(0...n).each { function }
end
### Пространственная сложность рекурсии равна O(n) ###
def recur(n)
return if n == 1
recur(n - 1)
end
```
Функции `loop()` и `recur()` имеют временную сложность $O(n)$ , но их пространственная сложность различается.
- Функция `loop()` вызывает `function()` в цикле $n$ раз. На каждой итерации `function()` возвращается и освобождает пространство своего кадра стека, поэтому пространственная сложность по-прежнему равна $O(1)$ .
- Рекурсивная функция `recur()` во время выполнения одновременно содержит $n$ еще не завершившихся экземпляров `recur()` , поэтому занимает $O(n)$ пространства кадров стека.
## Распространенные типы
Пусть размер входных данных равен $n$ . На рисунке ниже показаны распространенные типы пространственной сложности в порядке от меньшей к большей.
$$
\begin{aligned}
& O(1) < O(\log n) < O(n) < O(n^2) < O(2^n) \newline
& \text{Постоянная} < \text{Логарифмическая} < \text{Линейная} < \text{Квадратичная} < \text{Экспоненциальная}
\end{aligned}
$$
![Распространенные типы пространственной сложности](space_complexity.assets/space_complexity_common_types.png)
### Постоянная сложность $O(1)$
Постоянная сложность обычно встречается у констант, переменных и объектов, количество которых не зависит от размера входных данных $n$ .
Следует заметить, что память, занятая инициализацией переменных или вызовом функций внутри цикла, освобождается при переходе к следующей итерации, поэтому она не накапливается, и пространственная сложность по-прежнему остается $O(1)$ :
```src
[file]{space_complexity}-[class]{}-[func]{constant}
```
### Линейная сложность $O(n)$
Линейная сложность часто встречается у массивов, списков, стеков, очередей и других структур, число элементов в которых пропорционально $n$ :
```src
[file]{space_complexity}-[class]{}-[func]{linear}
```
Как показано на рисунке ниже, глубина рекурсии этой функции равна $n$ , то есть одновременно существует $n$ еще не завершившихся функций `linear_recur()` , которые используют $O(n)$ пространства кадров стека:
```src
[file]{space_complexity}-[class]{}-[func]{linear_recur}
```
![Линейная пространственная сложность, порождаемая рекурсивной функцией](space_complexity.assets/space_complexity_recursive_linear.png)
### Квадратичная сложность $O(n^2)$
Квадратичная сложность часто встречается у матриц и графов, где число элементов связано с $n$ квадратичной зависимостью:
```src
[file]{space_complexity}-[class]{}-[func]{quadratic}
```
Как показано на рисунке ниже, глубина рекурсии этой функции равна $n$ , и в каждой рекурсивной функции инициализируется массив длины $n$ , $n-1$ , $\dots$ , $2$ , $1$. Его средняя длина равна $n / 2$ , поэтому в сумме используется $O(n^2)$ пространства:
```src
[file]{space_complexity}-[class]{}-[func]{quadratic_recur}
```
![Квадратичная пространственная сложность, порождаемая рекурсивной функцией](space_complexity.assets/space_complexity_recursive_quadratic.png)
### Экспоненциальная сложность $O(2^n)$
Экспоненциальная сложность часто встречается у бинарных деревьев. Как видно на рисунке ниже, полное бинарное дерево с $n$ уровнями содержит $2^n - 1$ узлов и занимает $O(2^n)$ пространства:
```src
[file]{space_complexity}-[class]{}-[func]{build_tree}
```
![Экспоненциальная пространственная сложность, порождаемая полным бинарным деревом](space_complexity.assets/space_complexity_exponential.png)
### Логарифмическая сложность $O(\log n)$
Логарифмическая сложность часто встречается в алгоритмах «разделяй и властвуй». Например, при сортировке слиянием входной массив длины $n$ на каждом шаге рекурсии делится пополам, образуя рекурсивное дерево высоты $\log n$ и используя $O(\log n)$ пространства кадров стека.
Еще один пример - преобразование числа в строку. Если задано положительное целое число $n$ , то количество его цифр равно $\lfloor \log_{10} n \rfloor + 1$ , то есть длина соответствующей строки тоже равна $\lfloor \log_{10} n \rfloor + 1$ , следовательно, пространственная сложность составляет $O(\log_{10} n + 1) = O(\log n)$ .
## Компромисс между временем и пространством
В идеальных условиях хотелось бы, чтобы и временная, и пространственная сложность алгоритма были оптимальными. Однако на практике одновременно оптимизировать и время, и память обычно очень трудно.
**Снижение временной сложности обычно достигается ценой увеличения пространственной сложности, и наоборот**. Подход, при котором жертвуют памятью ради ускорения работы алгоритма, называется обменом пространства на время. Обратный подход называется обменом времени на пространство.
Выбор между этими двумя идеями зависит от того, что важнее в конкретной задаче. В большинстве случаев время ценнее памяти, поэтому стратегия обмена пространства на время используется чаще. Но при очень больших объемах данных контроль пространственной сложности тоже становится крайне важным.