0Pricing
Coding Interview Prep · Урок

Анализ циклов и вложенных циклов

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

«Анализ циклов и вложенных циклов» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.

Один цикл: O(n)

Простейший цикл выполняет своё тело n раз, поэтому имеет сложность O(n). Больший шаг меняет количество итераций, но не класс сложности. Всегда начинайте с подсчёта того, сколько раз выполняется тело цикла. Посмотрите код.

# O(n): body runs n times
def count_ops_linear(n):
    ops = 0
    for i in range(n):
        ops += 1     # constant work
    return ops

print(count_ops_linear(100))  # 100

# Still O(n): step=2 halves count but same class
def count_ops_half(n):
    ops = 0
    for i in range(0, n, 2):
        ops += 1
    return ops

print(count_ops_half(100))    # 50  => O(n)

Вложенные циклы: O(n²) и выше

Два вложенных цикла, каждый по n раз, дают n x n = O(n^2); три цикла дают O(n^3). Но если внутренний цикл выполняется фиксированное число раз, вся конструкция остаётся линейной.

def count_pairs(n):
    ops = 0
    for i in range(n):          # n iterations
        for j in range(n):      # n iterations each
            ops += 1
    return ops

print(count_pairs(10))   # 100 = 10^2
print(count_pairs(100))  # 10000 = 100^2
# Doubling n quadruples ops: classic O(n^2)

Треугольный цикл: O(n²/2) = O(n²)

Если внутренний цикл начинается с i+1, количество итераций образует треугольник: n(n-1)/2, что после отбрасывания половины всё равно даёт O(n^2). Задачи на все пары уникальных элементов выглядят именно так.

def count_unique_pairs(n):
    ops = 0
    for i in range(n):          # n iterations
        for j in range(i+1, n): # n-1, n-2, ..., 0
            ops += 1
    return ops

print(count_unique_pairs(10))  # 45 = 10*9/2
print(count_unique_pairs(100)) # 4950
# Still O(n^2) -- constant factor 1/2 dropped

Цикл с сокращающимся диапазоном: O(log n)

Если переменная цикла на каждом шаге делится пополам, получается O(log n). Главный вопрос: диапазон сокращается мультипликативно (log n) или аддитивно (n)? Посмотрите код.

def count_log_ops(n):
    ops = 0
    i = n
    while i >= 1:
        ops += 1
        i //= 2   # halve each iteration
    return ops

import math
for n in [8, 16, 64, 1024]:
    ops = count_log_ops(n)
    print(f'n={n}, ops={ops}, log2={int(math.log2(n))}')
# ops tracks log2(n) closely

Вложенный цикл с сокращающимся внутренним диапазоном: O(n log n)

Внешний цикл, выполняющийся n раз, и внутренний цикл сложности O(log n) дают O(n log n) — такую же структуру имеет сортировка слиянием. Умение замечать внутренний шаг O(log n) важно для анализа сортировок.

import math

def count_n_log_n(n):
    ops = 0
    for i in range(n):    # n iterations
        j = n
        while j >= 1:     # log n iterations
            ops += 1
            j //= 2
    return ops

for n in [8, 32, 128]:
    ops = count_n_log_n(n)
    predicted = int(n * math.log2(n))
    print(f'n={n}: actual={ops}, n*log2(n)~={predicted}')

Зависимые внутренние циклы

Если диапазон внутреннего цикла зависит от внешнего индекса, считайте общее число итераций, а не число итераций на каждом шаге. Внутренний цикл от 0 до i в сумме даёт n(n-1)/2 = O(n^2). Посмотрите код.

# Inner loop runs i times: total = 0+1+2+...+(n-1) = n(n-1)/2 => O(n^2)
def sum_inner_i(n):
    ops = 0
    for i in range(n):
        for j in range(i):   # runs 0,1,2,...,n-1 times
            ops += 1
    return ops

print(sum_inner_i(10))  # 45 = 10*9/2  => O(n^2)

# Inner loop runs n/i times (i doubles): sum ≈ n*log n => O(n log n)
def sum_inner_n_over_i(n):
    ops = 0
    i = 1
    while i <= n:
        for j in range(n // i):
            ops += 1
        i *= 2
    return ops
print(sum_inner_n_over_i(64))  # ~ 64*6 = 384

Пошаговый анализ пузырьковой сортировки

Пузырьковая сортировка выполняет n(n-1)/2 сравнений, поэтому имеет сложность O(n^2). Даже с досрочным завершением входные данные в обратном порядке требуют каждого сравнения. Для больших объёмов данных это слишком медленно.

def bubble_sort(arr):
    n = len(arr)
    comparisons = 0
    for i in range(n):
        swapped = False
        for j in range(0, n - i - 1):
            comparisons += 1
            if arr[j] > arr[j+1]:
                arr[j], arr[j+1] = arr[j+1], arr[j]
                swapped = True
        if not swapped:  # early exit if sorted
            break
    return comparisons

arr = list(range(10, 0, -1))  # worst case: reversed
ops = bubble_sort(arr)
print(f'Sorted: {arr}')
print(f'Comparisons: {ops}')  # 45 = 10*9/2

Циклы по строкам и подстрокам

Будьте внимательны: создание среза в Python имеет сложность O(k), а не является бесплатным, а объединение строк с помощью + в цикле имеет сложность O(n^2), потому что каждый раз выполняется копирование. Вместо этого используйте ''.join(parts). Посмотрите код.

# O(n^2): string concat in loop
def build_bad(n):
    s = ''
    for i in range(n):
        s += str(i)  # copies s each time!
    return s

# O(n): join is a single pass
def build_good(n):
    parts = []
    for i in range(n):
        parts.append(str(i))
    return ''.join(parts)

print(build_good(10))  # '0123456789'

Несколько параметров входных данных

При двух входных данных сложность может учитывать оба: O(m + n) для раздельной работы и O(m x n) для вложенных циклов. Для графов часто используют запись O(V + E). Чётко называйте каждую переменную.

# O(m + n): two independent loops
def independent(m, n):
    a = sum(range(m))  # O(m)
    b = sum(range(n))  # O(n)
    return a + b       # total O(m + n)

# O(m * n): nested
def nested(m, n):
    count = 0
    for i in range(m):     # O(m)
        for j in range(n): # O(n) each
            count += 1
    return count  # O(m * n)

print(independent(5, 10))  # 10 + 45 = 55
print(nested(5, 10))       # 50

Цикл внутри цикла и последовательные вызовы

Вызов функции не бесплатен — нужно учитывать и её внутренний цикл. Если вызывать помощник сложности O(n) n раз, получится O(n^2). При анализе всегда заглядывайте внутрь вызовов, скрывающих реализацию.

# Naive string matching: O(n*m)
def naive_search(text, pattern):
    n, m = len(text), len(pattern)
    matches = []
    for i in range(n - m + 1):  # O(n)
        if text[i:i+m] == pattern:  # O(m) comparison + O(m) slice
            matches.append(i)
    return matches
# Total: O(n*m)

print(naive_search('abcabcabc', 'abc'))  # [0, 3, 6]

Практика: определяем сложность с первого взгляда

Выработайте привычку: считайте уровни вложенности циклов, проверяйте, зависит ли внутренний цикл от внешнего, и следите за скрытыми затратами в вызовах функций и при создании срезов. Код — это задача для практики.

# What is the complexity of this function?
def mystery(nums):
    result = []
    for i in range(len(nums)):          # O(n)
        for j in range(i, len(nums)):   # O(n) worst
            if sum(nums[i:j+1]) == 0:   # O(n) slice + sum!
                result.append((i, j))
    return result
# Answer: O(n^3)  -- three nested n-proportional ops
# Outer O(n) x inner O(n) x sum/slice O(n) = O(n^3)

Быстрая проверка

Быстрая проверка — посмотрим, насколько хорошо Вы запомнили приёмы анализа циклов. Доверьтесь здесь своему рассуждению. 💪

Итоги урока

Итоги: вложенные циклы перемножаются, а независимые складываются; внутренний цикл с делением пополам даёт O(n log n), а скрытые затраты внутри вызовов и при создании срезов тоже необходимо учитывать.

Часто задаваемые вопросы

Урок «Анализ циклов и вложенных циклов» бесплатный?

Да — полный текст урока «Анализ циклов и вложенных циклов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Анализ циклов и вложенных циклов»?

Вычисляйте временную сложность одиночных и вложенных циклов, а также циклов с уменьшающимися диапазонами, например в двоичном поиске или итерациях по треугольнику Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Coding Interview Prep?

Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Анализ циклов и вложенных циклов»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Coding Interview Prep?

Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Нотация Big-O с нуля
  2. Анализ циклов и вложенных циклов
  3. Рекурсия и метод дерева рекурсии
  4. Пространственная сложность и компромиссы
← Назад к Coding Interview Prep