# Временная сложность Время выполнения может интуитивно и точно отражать эффективность алгоритма. Если мы хотим точно предсказать время выполнения фрагмента кода, как нам следует действовать? 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.. 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..нотацией большого $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$. ![Асимптотическая верхняя граница функции](../assets/asymptotic_upper_bound.png) ## Метод вычисления Асимптотическая верхняя граница имеет довольно математический характер, если вы чувствуете, что не полностью поняли ее, не беспокойтесь. Мы можем сначала освоить метод вычисления, и в процессе постоянной практики постепенно понять ее математический смысл. Согласно определению, после определения $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. **При вложенных циклах использовать умножение**. Общее количество операций равно произведению количества операций внешнего и внутреннего циклов, при этом для каждого уровня цикла можно применять приемы из пункт