30 KiB
Пространственная сложность
Пространственная сложность (space complexity) используется для измерения роста объема памяти, занимаемого алгоритмом, по мере увеличения объема данных. Эта концепция очень похожа на временную сложность, только нужно заменить "время выполнения" на "занимаемый объем памяти".
Пространство, связанное с алгоритмом
Память, используемая алгоритмом в процессе выполнения, в основном включает следующие типы.
- Входное пространство: используется для хранения входных данных алгоритма.
- Временное пространство: используется для хранения переменных, объектов, контекста функций и других данных в процессе выполнения алгоритма.
- Выходное пространство: используется для хранения выходных данных алгоритма.
В общем случае диапазон статистики пространственной сложности — это "временное пространство" плюс "выходное пространство".
Временное пространство можно дополнительно разделить на три части.
- Временные данные: используются для сохранения различных констант, переменных, объектов и т. д. в процессе выполнения алгоритма.
- Пространство стекового кадра: используется для сохранения контекстных данных вызываемой функции. Система создает стековый кадр в верхней части стека при каждом вызове функции, и пространство стекового кадра освобождается после возврата функции.
- Пространство инструкций: используется для сохранения скомпилированных инструкций программы, обычно игнорируется при фактической статистике.
При анализе пространственной сложности фрагмента программы мы обычно учитываем три части: временные данные, пространство стекового кадра и выходные данные, как показано на рисунке ниже.
Соответствующий код выглядит следующим образом:
=== "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
```
Метод расчета
Метод расчета пространственной сложности в целом аналогичен временной сложности, только нужно изменить объект статистики с "количества операций" на "размер используемого пространства".
В отличие от временной сложности, мы обычно обращаем внимание только на наихудшую пространственную сложность. Это связано с тем, что память является жестким требованием, и мы должны обеспечить достаточный резерв памяти для всех входных данных.
Рассмотрим следующий код, "наихудший" в наихудшей пространственной сложности имеет два значения.
- На основе наихудших входных данных: когда
n < 10, пространственная сложность равнаO(1); но когдаn > 10, инициализированный массивnumsзанимаетO(n)пространства, поэтому наихудшая пространственная сложность равнаO(n). - На основе пикового значения памяти во время выполнения алгоритма: например, до выполнения последней строки программа занимает
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}
Константный порядок O(1)
Константный порядок часто встречается в константах, переменных, объектах, количество которых не зависит от размера входных данных n.
Следует отметить, что память, занимаемая при инициализации переменных или вызове функций в цикле, освобождается при переходе к следующей итерации, поэтому не накапливается, и пространственная сложность остается O(1):
[file]{space_complexity}-[class]{}-[func]{constant}
Линейный порядок O(n)
Линейный порядок часто встречается в массивах, связанных списках, стеках, очередях и т. д., где количество элементов пропорционально n:
[file]{space_complexity}-[class]{}-[func]{linear}
Как показано на рисунке ниже, глубина рекурсии этой функции равна n, т. е. одновременно существует n невозвращенных функций linear_recur(), использующих O(n) пространства стекового кадра:
[file]{space_complexity}-[class]{}-[func]{linear_recur}
Квадратичный порядок O(n^2)
Квадратичный порядок часто встречается в матрицах и графах, где количество элементов имеет квадратичную зависимость от n:
[file]{space_complexity}-[class]{}-[func]{quadratic}
Как показано на рисунке ниже, глубина рекурсии этой функции равна n, и в каждой рекурсивной функции инициализируется массив длиной n, n-1, \dots, 2, 1 соответственно, средняя длина составляет n / 2, поэтому общее занимаемое пространство составляет O(n^2):
[file]{space_complexity}-[class]{}-[func]{quadratic_recur}
Экспоненциальный порядок O(2^n)
Экспоненциальный порядок часто встречается в двоичных деревьях. Как показано на рисунке ниже, "полное двоичное дерево" с n уровнями име



