Массивы: два указателя и скользящее окно

⏱ 45 мин · 🧩 заданий: 5

Задачи на массивы и строки — самые частые на собеседованиях. Наивное решение обычно перебирает пары за O(n²); приёмы этого урока делают то же за один проход O(n).

Два указателя навстречу

Указатели стоят на концах и двигаются к центру. Работает, когда массив отсортирован или задача симметрична (палиндромы, разворот).

def pair_sum_sorted(nums, target):
    left, right = 0, len(nums) - 1
    while left < right:
        s = nums[left] + nums[right]
        print(f"  left={left} ({nums[left]}), right={right} ({nums[right]}), сумма={s}")
        if s == target:
            return left, right
        if s < target:
            left += 1          # сумма мала → нужен больший элемент слева
        else:
            right -= 1         # сумма велика → нужен меньший элемент справа
    return None

print(pair_sum_sorted([1, 3, 4, 6, 8, 11], 10))

Пройдите пример 👣 Пошагово: видно, как указатели сближаются.

Почему это корректно: если nums[left] + nums[right] < target, то nums[left] не образует пару ни с каким элементом (все остальные справа не больше nums[right]) — его можно отбросить. Аналогично для правого.

Ещё задачи на этот шаблон: разворот массива на месте, проверка палиндрома, «контейнер с наибольшим количеством воды», сумма трёх чисел (3Sum: сортировка + два указателя для каждого первого элемента, O(n²)).

Быстрый и медленный указатели

Оба идут в одну сторону: fast просматривает элементы, slow указывает, куда записать следующий «хороший». Так массив меняют на месте, без дополнительной памяти.

def remove_duplicates(nums):
    """Удалить дубликаты из отсортированного списка на месте, вернуть новую длину."""
    if not nums:
        return 0
    slow = 1
    for fast in range(1, len(nums)):
        if nums[fast] != nums[slow - 1]:      # новый уникальный элемент
            nums[slow] = nums[fast]
            slow += 1
    return slow

data = [1, 1, 2, 2, 2, 3, 5, 5]
k = remove_duplicates(data)
print(k, data[:k])

Этот же шаблон — «быстрый и медленный» — в связных списках находит середину и цикл (урок «Связные списки»).

Скользящее окно

Окно — непрерывный подотрезок [left, right]. При сдвиге окно обновляется, а не пересчитывается с нуля.

Фиксированный размер

def max_average(nums, k):
    window = sum(nums[:k])                 # первое окно — O(k)
    best = window
    for i in range(k, len(nums)):
        window += nums[i] - nums[i - k]    # вошёл новый, вышел старый — O(1)
        best = max(best, window)
    return best / k

print(max_average([1, 12, -5, -6, 50, 3], 4))

Переменный размер

Правый край расширяет окно, левый сжимает, когда условие нарушено:

left = 0
for right in range(len(s)):
    добавить s[right] в окно
    while окно невалидно:
        убрать s[left] из окна
        left += 1
    обновить ответ по окну [left, right]
from collections import Counter

def min_window_with_sum(nums, target):
    """Длина кратчайшего подмассива с суммой >= target (числа положительные)."""
    left = total = 0
    best = float("inf")
    for right, x in enumerate(nums):
        total += x
        while total >= target:             # окно валидно — пробуем сжать
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if best == float("inf") else best

print(min_window_with_sum([2, 3, 1, 2, 4, 3], 7))     # [4, 3] → 2

def longest_with_k_distinct(s, k):
    """Самая длинная подстрока, где не больше k разных символов."""
    counts = Counter()
    left = best = 0
    for right, ch in enumerate(s):
        counts[ch] += 1
        while len(counts) > k:
            counts[s[left]] -= 1
            if counts[s[left]] == 0:
                del counts[s[left]]
            left += 1
        best = max(best, right - left + 1)
    return best

print(longest_with_k_distinct("eceba", 2))            # "ece" → 3

Каждый элемент входит в окно и выходит из него не более одного раза → O(n), хотя внутри цикла есть while.

Префиксные суммы

Если нужно много раз узнавать сумму отрезка, предвычислите префиксы: prefix[i] — сумма первых i элементов. Тогда сумма nums[l:r] = prefix[r] - prefix[l] за O(1):

from itertools import accumulate

nums = [3, -1, 4, 1, 5, 9, -2, 6]
prefix = [0] + list(accumulate(nums))
print(prefix)

def range_sum(l, r):            # сумма nums[l:r]
    return prefix[r] - prefix[l]

print(range_sum(2, 5), sum(nums[2:5]))

def count_subarrays_with_sum(nums, k):
    """Сколько подмассивов имеют сумму k (числа любого знака) — префиксы + словарь, O(n)."""
    seen = {0: 1}
    total = count = 0
    for x in nums:
        total += x
        count += seen.get(total - k, 0)     # есть ли префикс, дающий отрезок с суммой k
        seen[total] = seen.get(total, 0) + 1
    return count

print(count_subarrays_with_sum([1, 1, 1], 2), count_subarrays_with_sum([1, 2, 3, -3, 3], 3))

Как узнать шаблон

Признак в условии Приём
отсортированный массив, пара/тройка с суммой два указателя навстречу
палиндром, разворот два указателя навстречу
изменить массив на месте, удалить/переставить элементы быстрый и медленный
«подмассив/подстрока подряд», «самая длинная/короткая», «не больше k» скользящее окно
много запросов суммы отрезка, «подмассив с суммой k» префиксные суммы (+ словарь)

Окно не работает с отрицательными числами

Сжатие окна по сумме корректно, только когда числа положительные (сумма монотонна). Для «подмассив с суммой k» при отрицательных числах — префиксные суммы со словарём.

Итог

  • Два указателя навстречу — отсортированные массивы и симметричные задачи.
  • Быстрый/медленный — изменение массива на месте за O(1) памяти.
  • Скользящее окно — подотрезки подряд; каждый элемент входит и выходит один раз → O(n).
  • Префиксные суммы — сумма отрезка за O(1), с хеш-таблицей — подсчёт подмассивов с суммой k.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Программа на Python

Пара с заданной суммой. Дан отсортированный по возрастанию список nums и число target. Верните индексы (i, j), i < j, двух элементов с суммой target, или None, если таких нет. Решение — двумя указателями за O(n) времени и O(1) памяти.

Какие тесты проверяют код (3)
assert pair_sum_sorted([1, 3, 4, 6, 8, 11], 10) == (2, 3)
assert pair_sum_sorted([1, 2], 3) == (0, 1) and pair_sum_sorted([1, 2, 3], 100) is None
assert pair_sum_sorted([], 0) is None
Задание 2 Программа на Python

Палиндром. Напишите is_palindrome(s): является ли строка палиндромом, если учитывать только буквы и цифры и не учитывать регистр. "А роза упала на лапу Азора" → True. Используйте два указателя, без создания очищенной копии строки (O(1) доп. памяти).

Какие тесты проверяют код (3)
assert is_palindrome("А роза упала на лапу Азора")
assert is_palindrome("A man, a plan, a canal: Panama") and not is_palindrome("race a car")
assert is_palindrome("") and is_palindrome(".,!")
Задание 3 Программа на Python

Самая длинная подстрока без повторов. Верните длину самой длинной подстроки строки s, в которой все символы различны. "abcabcbb" → 3 ("abc"). Скользящее окно, O(n).

Какие тесты проверяют код (4)
assert longest_unique("abcabcbb") == 3 and longest_unique("bbbbb") == 1
assert longest_unique("pwwkew") == 3 and longest_unique("") == 0
assert longest_unique("abba") == 2 and longest_unique("dvdf") == 3
import time
s = "abcdefghij" * 20_000
start = time.perf_counter()
assert longest_unique(s) == 10
assert time.perf_counter() - start < 1, "Нужно решение за O(n)"

Задание 4 Программа на Python

Перенос нулей. Переставьте все нули списка nums в конец на месте, сохранив порядок остальных элементов. Функция move_zeroes(nums) ничего не возвращает. Быстрый и медленный указатели.

Какие тесты проверяют код (4)
a = [0, 1, 0, 3, 12]; assert move_zeroes(a) is None and a == [1, 3, 12, 0, 0]
a = [0]; move_zeroes(a); assert a == [0]
a = [4, 2, 0, 0, 1]; b = a; move_zeroes(a); assert b is a and a == [4, 2, 1, 0, 0]
import inspect; src = inspect.getsource(move_zeroes); assert "sort" not in src, "Решите без сортировки — двумя указателями за O(n)"
Задание 5 Программа на Python

Максимальная сумма окна. Верните наибольшую сумму k подряд идущих элементов списка (гарантируется 1 ≤ k ≤ len(nums)). Окно фиксированного размера, O(n).

Какие тесты проверяют код (2)
assert max_window_sum([2, 1, 5, 1, 3, 2], 3) == 9 and max_window_sum([-1, -2, -3], 1) == -1
import time
nums = list(range(60_000))
start = time.perf_counter()
assert max_window_sum(nums, 3_000) == sum(range(57_000, 60_000))
assert time.perf_counter() - start < 0.25, "Пересчитывайте сумму окна за O(1) при сдвиге"

Войдите, чтобы отмечать пройденные уроки.