Погружение в O(n*n)

В этом уроке ты

  • Освоишь оценку сложности O(n*n) на примерах
  • Научишься распознавать квадратичную сложность

Квадратичная сложность - O(n*n)

Квадратичная сложность значит, что время выполнения алгоритма пропорционально квадрату размера входных данных. Обозначается как O(n*n).

Другими словами, если функция имеет оценку O(n*n) по времени и памяти, то при увеличении размера входных данных в 10 раз, время и память возрастут в 100 раз.

Когда встречается
Чаще всего квадратичная сложность появляется при вложенных циклах — когда для каждого элемента массива нам нужно дополнительно пройтись по другим элементам массива.
Пример

Вернёмся к примеру, который у нас был вначале. Программа возвращает true, если массив содержит повторяющиеся числа, и false, если все числа уникальны:

// Время: O(n*n), где n — размер массива nums
// Память: O(1)
using System.Collections.Generic;

public class Solution {
    public bool ContainsDuplicate(List<int> nums) {
        for (int i = 0; i < nums.Count; i++) {
            for (int j = i + 1; j < nums.Count; j++) {
                if (nums[i] == nums[j]) {
                    return true;
                }
            }
        }
        return false;
    }
}
  • Время: O(n*n), и это может быть неочевидно на первый взгляд.
  • Память: O(1), потому что мы не создаём структур данных, сопоставимых по размеру с nums.
Почему время O(n*n)

В Big O мы рассматриваем худший случай — когда программа работает дольше всего. Здесь худший случай — отсутствие дубликатов, ведь тогда придётся перебрать все пары чисел в массиве.

Допустим, nums = [1,2,3,4,5,6,7,8,9,10]. Дополнительно добавим счётчик, который считает число исполненных строк кода:

using System;
using System.Collections.Generic;

public class Solution {
    public bool ContainsDuplicate(List<int> nums) {
        // count считает число исполненных строк кода
        int count = 0;
        for (int i = 0; i < nums.Count; i++) {
            count += 1;
            for (int j = i + 1; j < nums.Count; j++) {
                count += 1;
                if (nums[i] == nums[j]) {
                    count += 1;
                    Console.WriteLine("Программа выполнила " + count + " строк кода");
                    return true;
                }
            }
        }
        Console.WriteLine("Программа выполнила " + count + " строк кода");
        return false;
    }
}

Получим 55 исполняемых строк. А теперь давай еще увеличим размер nums.

Размер массива nums Число выполняемых строк
10 55
100 5050
1 000 500500
10 000 50005000
100 000 5000050000
1 000 000 Много…
Самое главное, что при увеличении массива всего в 10 раз число исполняемых строк увеличивается почти в 100 раз! Это и есть признак квадратичный сложности!

Точная формула для количества операций – n*(n+1)/2 . Если раскрыть скобки, получается: (n*n)/2+n/2

В Big O константы и младшие члены отбрасывают, поэтому из (n*n)/2+n/2 остаётся только O(n*n).

P.S. под константой подразумевается n/2. 2 - это константа, которую можно отбросить, потому что она почти никак не влияет на асимптотику и очень мала по сравнению с n*n.

Со временем ты научишься легко определять, какие части формулы можно опустить в Big O. Если что-то пока не до конца ясно — не переживай! Этому мы посвятим отдельный урок!
Главные выводы
  1. Квадратичная сложность значит, что время выполнения алгоритма пропорционально квадрату размера входных данных. Обозначается как O(n*n).
  2. Чаще всего квадратичная сложность появляется при вложенных циклах
Что дальше

Скорее переходи к следующему уроку, чтобы закрепить моменты, которые мы проговаривали ранее только вскользь, чтобы вывести оценку сложности алгоритма на полностью осознанный уровень!