Files
hello-algo/ru/docs/chapter_computational_complexity/time_complexity.md
2026-01-20 15:08:42 +08:00

28 KiB
Raw Blame History

Временная сложность

Время выполнения может интуитивно и точно отражать эффективность алгоритма. Если мы хотим точно предсказать время выполнения фрагмента кода, как нам следует действовать?

  1. Определить платформу выполнения, включая аппаратную конфигурацию, язык программирования, системное окружение и т. д., так как все эти факторы влияют на эффективность выполнения кода.
  2. Оценить время выполнения различных вычислительных операций, например, операция сложения + требует 1 нс, операция умножения * требует 10 нс, операция вывода print() требует 5 нс и т. д.
  3. Подсчитать все вычислительные операции в коде и суммировать время выполнения всех операций, чтобы получить время выполнения.

Например, в следующем коде размер входных данных равен n:

=== "Python"

```python title=""
# На определенной платформе выполнения
def algorithm(n: int):
    a = 2      # 1 нс
    a = a + 1  # 1 нс
    a = a * 2  # 10 нс
    # Цикл n раз
    for _ in range(n):  # 1 нс
        print(0)        # 5 нс
```

=== "C++"

```cpp title=""
// На определенной платформе выполнения
void algorithm(int n) {
    int a = 2;  // 1 нс
    a = a + 1;  // 1 нс
    a = a * 2;  // 10 нс
    // Цикл n раз
    for (int i = 0; i < n; i++) {  // 1 нс
        cout << 0 << endl;         // 5 нс
    }
}
```

=== "Java"

```java title=""
// На определенной платформе выполнения
void algorithm(int n) {
    int a = 2;  // 1 нс
    a = a + 1;  // 1 нс
    a = a * 2;  // 10 нс
    // Цикл n раз
    for (int i = 0; i < n; i++) {  // 1 нс
        System.out.println(0);     // 5 нс
    }
}
```

=== "C#"

```csharp title=""
// На определенной платформе выполнения
void Algorithm(int n) {
    int a = 2;  // 1 нс
    a = a + 1;  // 1 нс
    a = a * 2;  // 10 нс
    // Цикл n раз
    for (int i = 0; i < n; i++) {  // 1 нс
        Console.WriteLine(0);      // 5 нс
    }
}
```

=== "Go"

```go title=""
// На определенной платформе выполнения
func algorithm(n int) {
    a := 2     // 1 нс
    a = a + 1  // 1 нс
    a = a * 2  // 10 нс
    // Цикл n раз
    for i := 0; i < n; i++ {  // 1 нс
        fmt.Println(a)        // 5 нс
    }
}
```

=== "Swift"

```swift title=""
// На определенной платформе выполнения
func algorithm(n: Int) {
    var a = 2 // 1 нс
    a = a + 1 // 1 нс
    a = a * 2 // 10 нс
    // Цикл n раз
    for _ in 0 ..< n { // 1 нс
        print(0) // 5 нс
    }
}
```

=== "JS"

```javascript title=""
// На определенной платформе выполнения
function algorithm(n) {
    var a = 2; // 1 нс
    a = a + 1; // 1 нс
    a = a * 2; // 10 нс
    // Цикл n раз
    for(let i = 0; i < n; i++) { // 1 нс
        console.log(0); // 5 нс
    }
}
```

=== "TS"

```typescript title=""
// На определенной платформе выполнения
function algorithm(n: number): void {
    var a: number = 2; // 1 нс
    a = a + 1; // 1 нс
    a = a * 2; // 10 нс
    // Цикл n раз
    for(let i = 0; i < n; i++) { // 1 нс
        console.log(0); // 5 нс
    }
}
```

=== "Dart"

```dart title=""
// На определенной платформе выполнения
void algorithm(int n) {
  int a = 2; // 1 нс
  a = a + 1; // 1 нс
  a = a * 2; // 10 нс
  // Цикл n раз
  for (int i = 0; i < n; i++) { // 1 нс
    print(0); // 5 нс
  }
}
```

=== "Rust"

```rust title=""
// На определенной платформе выполнения
fn algorithm(n: i32) {
    let mut a = 2;      // 1 нс
    a = a + 1;          // 1 нс
    a = a * 2;          // 10 нс
    // Цикл n раз
    for _ in 0..n {     // 1 нс
        println!("{}", 0);  // 5 нс
    }
}
```

=== "C"

```c title=""
// На определенной платформе выполнения
void algorithm(int n) {
    int a = 2;  // 1 нс
    a = a + 1;  // 1 нс
    a = a * 2;  // 10 нс
    // Цикл n раз
    for (int i = 0; i < n; i++) {   // 1 нс
        printf("%d", 0);            // 5 нс
    }
}
```

=== "Kotlin"

```kotlin title=""
// На определенной платформе выполнения
fun algorithm(n: Int) {
    var a = 2 // 1 нс
    a = a + 1 // 1 нс
    a = a * 2 // 10 нс
    // Цикл n раз
    for (i in 0..<n) {  // 1 нс
        println(0)      // 5 нс
    }
}
```

=== "Ruby"

```ruby title=""
# На определенной платформе выполнения
def algorithm(n)
    a = 2       # 1 нс
    a = a + 1   # 1 нс
    a = a * 2   # 10 нс
    # Цикл n раз
    (0...n).each do # 1 нс
        puts 0      # 5 нс
    end
end
```

Согласно вышеуказанному методу, можно получить время выполнения алгоритма (6n + 12) нс:


1 + 1 + 10 + (1 + 5) \times n = 6n + 12

Но на самом деле, подсчет времени выполнения алгоритма неразумен и нереалистичен. Во-первых, мы не хотим привязывать прогнозируемое время к платформе выполнения, поскольку алгоритм должен работать на различных платформах. Во-вторых, очень сложно узнать время выполнения каждой операции, что создает огромные трудности для процесса оценки.

Анализ тенденции роста времени

Анализ временной сложности не подсчитывает время выполнения алгоритма, а анализирует тенденцию роста времени выполнения алгоритма по мере увеличения объема данных.

Концепция "тенденции роста времени" довольно абстрактна, давайте разберем ее на примере. Предположим, размер входных данных равен n, даны три алгоритма A, B и C:

=== "Python"

```python title=""
# Временная сложность алгоритма A: константная
def algorithm_A(n: int):
    print(0)
# Временная сложность алгоритма B: линейная
def algorithm_B(n: int):
    for _ in range(n):
        print(0)
# Временная сложность алгоритма C: константная
def algorithm_C(n: int):
    for _ in range(1000000):
        print(0)
```

=== "C++"

```cpp title=""
// Временная сложность алгоритма A: константная
void algorithm_A(int n) {
    cout << 0 << endl;
}
// Временная сложность алгоритма B: линейная
void algorithm_B(int n) {
    for (int i = 0; i < n; i++) {
        cout << 0 << endl;
    }
}
// Временная сложность алгоритма C: константная
void algorithm_C(int n) {
    for (int i = 0; i < 1000000; i++) {
        cout << 0 << endl;
    }
}
```

=== "Java"

```java title=""
// Временная сложность алгоритма A: константная
void algorithm_A(int n) {
    System.out.println(0);
}
// Временная сложность алгоритма B: линейная
void algorithm_B(int n) {
    for (int i = 0; i < n; i++) {
        System.out.println(0);
    }
}
// Временная сложность алгоритма C: константная
void algorithm_C(int n) {
    for (int i = 0; i < 1000000; i++) {
        System.out.println(0);
    }
}
```

=== "C#"

```csharp title=""
// Временная сложность алгоритма A: константная
void AlgorithmA(int n) {
    Console.WriteLine(0);
}
// Временная сложность алгоритма B: линейная
void AlgorithmB(int n) {
    for (int i = 0; i < n; i++) {
        Console.WriteLine(0);
    }
}
// Временная сложность алгоритма C: константная
void AlgorithmC(int n) {
    for (int i = 0; i < 1000000; i++) {
        Console.WriteLine(0);
    }
}
```

=== "Go"

```go title=""
// Временная сложность алгоритма A: константная
func algorithm_A(n int) {
    fmt.Println(0)
}
// Временная сложность алгоритма B: линейная
func algorithm_B(n int) {
    for i := 0; i < n; i++ {
        fmt.Println(0)
    }
}
// Временная сложность алгоритма C: константная
func algorithm_C(n int) {
    for i := 0; i < 1000000; i++ {
        fmt.Println(0)
    }
}
```

=== "Swift"

```swift title=""
// Временная сложность алгоритма A: константная
func algorithmA(n: Int) {
    print(0)
}

// Временная сложность алгоритма B: линейная
func algorithmB(n: Int) {
    for _ in 0 ..< n {
        print(0)
    }
}

// Временная сложность алгоритма C: константная
func algorithmC(n: Int) {
    for _ in 0 ..< 1_000_000 {
        print(0)
    }
}
```

=== "JS"

```javascript title=""
// Временная сложность алгоритма A: константная
function algorithm_A(n) {
    console.log(0);
}
// Временная сложность алгоритма B: линейная
function algorithm_B(n) {
    for (let i = 0; i < n; i++) {
        console.log(0);
    }
}
// Временная сложность алгоритма C: константная
function algorithm_C(n) {
    for (let i = 0; i < 1000000; i++) {
        console.log(0);
    }
}

```

=== "TS"

```typescript title=""
// Временная сложность алгоритма A: константная
function algorithm_A(n: number): void {
    console.log(0);
}
// Временная сложность алгоритма B: линейная
function algorithm_B(n: number): void {
    for (let i = 0; i < n; i++) {
        console.log(0);
    }
}
// Временная сложность алгоритма C: константная
function algorithm_C(n: number): void {
    for (let i = 0; i < 1000000; i++) {
        console.log(0);
    }
}
```

=== "Dart"

```dart title=""
// Временная сложность алгоритма A: константная
void algorithmA(int n) {
  print(0);
}
// Временная сложность алгоритма B: линейная
void algorithmB(int n) {
  for (int i = 0; i < n; i++) {
    print(0);
  }
}
// Временная сложность алгоритма C: константная
void algorithmC(int n) {
  for (int i = 0; i < 1000000; i++) {
    print(0);
  }
}
```

=== "Rust"

```rust title=""
// Временная сложность алгоритма A: константная
fn algorithm_A(n: i32) {
    println!("{}", 0);
}
// Временная сложность алгоритма B: линейная
fn algorithm_B(n: i32) {
    for _ in 0..n {
        println!("{}", 0);
    }
}
// Временная сложность алгоритма C: константная
fn algorithm_C(n: i32) {
    for _ in 0..1000000 {
        println!("{}", 0);
    }
}
```

=== "C"

```c title=""
// Временная сложность алгоритма A: константная
void algorithm_A(int n) {
    printf("%d", 0);
}
// Временная сложность алгоритма B: линейная
void algorithm_B(int n) {
    for (int i = 0; i < n; i++) {
        printf("%d", 0);
    }
}
// Временная сложность алгоритма C: константная
void algorithm_C(int n) {
    for (int i = 0; i < 1000000; i++) {
        printf("%d", 0);
    }
}
```

=== "Kotlin"

```kotlin title=""
// Временная сложность алгоритма A: константная
fun algoritm_A(n: Int) {
    println(0)
}
// Временная сложность алгоритма B: линейная
fun algorithm_B(n: Int) {
    for (i in 0..<n){
        println(0)
    }
}
// Временная сложность алгоритма C: константная
fun algorithm_C(n: Int) {
    for (i in 0..<1000000) {
        println(0)
    }
}
```

=== "Ruby"

```ruby title=""
# Временная сложность алгоритма A: константная
def algorithm_A(n)
    puts 0
end

# Временная сложность алгоритма B: линейная
def algorithm_B(n)
    (0...n).each { puts 0 }
end

# Временная сложность алгоритма C: константная
def algorithm_C(n)
    (0...1_000_000).each { puts 0 }
end
```

На следующем рисунке показана временная сложность этих трех алгоритмических функций.

  • Алгоритм A имеет только 1 операцию вывода, время выполнения алгоритма не увеличивается с ростом n. Мы называем временную сложность этого алгоритма "константной".
  • В алгоритме B операция вывода выполняется n раз в цикле, время выполнения алгоритма растет линейно с увеличением n. Временная сложность этого алгоритма называется "линейной".
  • В алгоритме C операция вывода выполняется 1000000 раз в цикле, хотя время выполнения велико, оно не зависит от размера входных данных n. Поэтому временная сложность C такая же, как у A, и остается "константной".

Тенденция роста времени алгоритмов A, B и C

По сравнению с прямым подсчетом времени выполнения алгоритма, какие особенности имеет анализ временной сложности?

  • Временная сложность может эффективно оценить эффективность алгоритма. Например, время выполнения алгоритма B растет линейно, при n > 1 он медленнее алгоритма A, при n > 1000000 он медленнее алгоритма C. Фактически, при достаточно большом размере входных данных n алгоритм с "константной" сложностью всегда лучше алгоритма с "линейной" сложностью, что и есть смысл тенденции роста времени.
  • Метод вычисления временной сложности более прост. Очевидно, что платформа выполнения и типы вычислительных операций не связаны с тенденцией роста времени выполнения алгоритма. Поэтому при анализе временной сложности мы можем просто считать время выполнения всех вычислительных операций одинаковым "единичным временем", тем самым упрощая "подсчет времени выполнения вычислительных операций" до "подсчета количества вычислительных операций", что значительно снижает сложность оценки.
  • Временная сложность также имеет определенные ограничения. Например, хотя временная сложность алгоритмов A и C одинакова, фактическое время выполнения сильно различается. Аналогично, хотя временная сложность алгоритма B выше, чем у C, при небольшом размере входных данных n алгоритм B явно лучше алгоритма C. В таких случаях часто трудно судить об эффективности алгоритма только по временной сложности. Конечно, несмотря на вышеуказанные проблемы, анализ сложности остается наиболее эффективным и часто используемым методом оценки эффективности алгоритмов.

Асимптотическая верхняя граница функции

Дана функция с размером входных данных n:

=== "Python"

```python title=""
def algorithm(n: int):
    a = 1      # +1
    a = a + 1  # +1
    a = a * 2  # +1
    # Цикл n раз
    for i in range(n):  # +1
        print(0)        # +1
```

=== "C++"

```cpp title=""
void algorithm(int n) {
    int a = 1;  // +1
    a = a + 1;  // +1
    a = a * 2;  // +1
    // Цикл n раз
    for (int i = 0; i < n; i++) { // +1на каждой итерации выполняется i ++
        cout << 0 << endl;    // +1
    }
}
```

=== "Java"

```java title=""
void algorithm(int n) {
    int a = 1;  // +1
    a = a + 1;  // +1
    a = a * 2;  // +1
    // Цикл n раз
    for (int i = 0; i < n; i++) { // +1на каждой итерации выполняется i ++
        System.out.println(0);    // +1
    }
}
```

=== "C#"

```csharp title=""
void Algorithm(int n) {
    int a = 1;  // +1
    a = a + 1;  // +1
    a = a * 2;  // +1
    // Цикл n раз
    for (int i = 0; i < n; i++) {   // +1на каждой итерации выполняется i ++
        Console.WriteLine(0);   // +1
    }
}
```

=== "Go"

```go title=""
func algorithm(n int) {
    a := 1      // +1
    a = a + 1   // +1
    a = a * 2   // +1
    // Цикл n раз
    for i := 0; i < n; i++ {   // +1
        fmt.Println(a)         // +1
    }
}
```

=== "Swift"

```swift title=""
func algorithm(n: Int) {
    var a = 1 // +1
    a = a + 1 // +1
    a = a * 2 // +1
    // Цикл n раз
    for _ in 0 ..< n { // +1
        print(0) // +1
    }
}
```

=== "JS"

```javascript title=""
function algorithm(n) {
    var a = 1; // +1
    a += 1; // +1
    a *= 2; // +1
    // Цикл n раз
    for(let i = 0; i < n; i++){ // +1на каждой итерации выполняется i ++
        console.log(0); // +1
    }
}
```

=== "TS"

```typescript title=""
function algorithm(n: number): void{
    var a: number = 1; // +1
    a += 1; // +1
    a *= 2; // +1
    // Цикл n раз
    for(let i = 0; i < n; i++){ // +1на каждой итерации выполняется i ++
        console.log(0); // +1
    }
}
```

=== "Dart"

```dart title=""
void algorithm(int n) {
  int a = 1; // +1
  a = a + 1; // +1
  a = a * 2; // +1
  // Цикл n раз
  for (int i = 0; i < n; i++) { // +1на каждой итерации выполняется i ++
    print(0); // +1
  }
}
```

=== "Rust"

```rust title=""
fn algorithm(n: i32) {
    let mut a = 1;   // +1
    a = a + 1;      // +1
    a = a * 2;      // +1

    // Цикл n раз
    for _ in 0..n { // +1на каждой итерации выполняется i ++
        println!("{}", 0); // +1
    }
}
```

=== "C"

```c title=""
void algorithm(int n) {
    int a = 1;  // +1
    a = a + 1;  // +1
    a = a * 2;  // +1
    // Цикл n раз
    for (int i = 0; i < n; i++) {   // +1на каждой итерации выполняется i ++
        printf("%d", 0);            // +1
    }
}
```

=== "Kotlin"

```kotlin title=""
fun algorithm(n: Int) {
    var a = 1 // +1
    a = a + 1 // +1
    a = a * 2 // +1
    // Цикл n раз
    for (i in 0..<n) { // +1на каждой итерации выполняется i ++
        println(0) // +1
    }
}
```

=== "Ruby"

```ruby title=""
def algorithm(n)
    a = 1       # +1
    a = a + 1   # +1
    a = a * 2   # +1
    # Цикл n раз
    (0...n).each do # +1
        puts 0      # +1
    end
end
```

Пусть количество операций алгоритма является функцией от размера входных данных n, обозначим ее как T(n), тогда количество операций вышеуказанной функции равно:


T(n) = 3 + 2n

T(n) является линейной функцией, что означает, что тенденция роста времени выполнения линейна, поэтому ее временная сложность является линейной.

Мы обозначаем линейную временную сложность как O(n), этот математический символ называется нотацией большого O (big-O notation), представляющей асимптотическую верхнюю границу (asymptotic upper bound) функции T(n).

Анализ временной сложности по сути является вычислением асимптотической верхней границы "количества операций $T(n)$", он имеет четкое математическое определение.

!!! note "Асимптотическая верхняя граница функции"

Если существуют положительные действительные числа $c$ и $n_0$, такие что для всех $n > n_0$ выполняется $T(n) \leq c \cdot f(n)$, то можно считать, что $f(n)$ дает асимптотическую верхнюю границу для $T(n)$, обозначается как $T(n) = O(f(n))$.

Как показано на следующем рисунке, вычисление асимптотической верхней границы заключается в поиске функции f(n), такой что при n стремящемся к бесконечности T(n) и f(n) находятся на одном уровне роста, отличаясь только константным коэффициентом c.

Асимптотическая верхняя граница функции

Метод вычисления

Асимптотическая верхняя граница имеет довольно математический характер, если вы чувствуете, что не полностью поняли ее, не беспокойтесь. Мы можем сначала освоить метод вычисления, и в процессе постоянной практики постепенно понять ее математический смысл.

Согласно определению, после определения f(n) мы можем получить временную сложность O(f(n)). Так как же определить асимптотическую верхнюю границу f(n)? В целом это делается в два шага: сначала подсчитывается количество операций, затем определяется асимптотическая верхняя граница.

Шаг первый: подсчет количества операций

Для кода вычисление производится построчно сверху вниз. Однако, поскольку в вышеуказанном c \cdot f(n) константный коэффициент c может принимать любое значение, различные коэффициенты и константные члены в количестве операций T(n) можно игнорировать. Исходя из этого принципа, можно обобщить следующие приемы упрощения подсчета.

  1. Игнорировать константы в $T(n)$. Поскольку они не зависят от n, они не влияют на временную сложность.
  2. Опускать все коэффициенты. Например, циклы 2n раз, 5n + 1 раз и т. д. можно упростить до n раз, поскольку коэффициент перед n не влияет на временную сложность.
  3. При вложенных циклах использовать умножение. Общее количество операций равно произведению количества операций внешнего и внутреннего циклов, при этом для каждого уровня цикла можно применять приемы из пункт