Проанализируйте следующий фрагмент Python-кода и предложите более эффективный алгоритм и структуру данных, объяснив временную и пространственную сложность исходного и улучшенного решений: def unique_pairs(arr): res = [] for i in range(len(arr)): for j in range(i+1, len(arr)): if arr[i] + arr[j] == 0: res.append((arr[i], arr[j])) return res
Анализ исходного кода - Вложенные циклы пробегают все пары индексов: время работы O(n2)O(n^2)O(n2), где n=n = n= len(arr). - Дополнительная память помимо результата — константна O(1)O(1)O(1); итоговый список пар может иметь в худшем случае Θ(n2)\Theta(n^2)Θ(n2) элементов, т.е. итоговая память O(n2)O(n^2)O(n2). Задача уточнения: надо ли возвращать все индексные пары (как в оригинале) или только уникальные по значению пары. Предложу оба эффективных варианта. 1) Если нужен только один экземпляр каждой пары значений (каждая комбинация x,−xx,-xx,−x — один раз) Алгоритм: проход по массиву с множеством "seen" и множеством результатов (чтобы не дублировать). Время: O(n)O(n)O(n). Память: O(n+p)O(n + p)O(n+p), где ppp — число уникальных пар (обычно p≤np \le np≤n). Пример (псевдо-/Python): def unique_value_pairs(arr): seen = set() res = set() for x in arr: if -x in seen: a, b = (x, -x) if x <= -x else (-x, x) res.add((a, b)) seen.add(x) return list(res) Особенность: нули добавятся как одна пара (0,0)(0,0)(0,0) только если есть хотя бы два нуля. 2) Если нужно точно те же все индексные пары (включая дубликаты по индексам) Алгоритм: хеш-таблица, хранящая ранее встретившиеся значения (или их индексы / счётчики). При обработке текущего элемента сразу генерируем пары с уже встреченными дополнениями — каждый индексный набор генерируется ровно один раз. Время: O(n+k)O(n + k)O(n+k), где kkk — число возвращаемых пар (включая k=Θ(n2)k= \Theta(n^2)k=Θ(n2) в худшем случае). Память: O(n+k)O(n + k)O(n+k). Пример: from collections import defaultdict def all_index_pairs(arr): positions = defaultdict(list) # value -> list of indices (или просто counts, если не нужны индексы) res = [] for j, x in enumerate(arr): comp = -x for i in positions[comp]: # генерация всех пар (i,j) с суммой 0 res.append((arr[i], arr[j])) positions[x].append(j) return res Особенность: для нулей будет сгенерировано (c2)\binom{c}{2}(2c) пар, где ccc — количество нулей. Выводы по сложностям - Исходный: время O(n2)O(n^2)O(n2), доп. память O(1)O(1)O(1) (без результата), память с результатом — до O(n2)O(n^2)O(n2). - Улучшенный (уникальные значения): время O(n)O(n)O(n), память O(n)O(n)O(n). - Улучшенный (все индексные пары): время O(n+k)O(n + k)O(n+k), память O(n+k)O(n + k)O(n+k), где kkk — число возвращаемых пар. Выберите вариант в зависимости от требуемой семантики результата (уникальные по значению или все индексные пары).
- Вложенные циклы пробегают все пары индексов: время работы O(n2)O(n^2)O(n2), где n=n = n= len(arr).
- Дополнительная память помимо результата — константна O(1)O(1)O(1); итоговый список пар может иметь в худшем случае Θ(n2)\Theta(n^2)Θ(n2) элементов, т.е. итоговая память O(n2)O(n^2)O(n2).
Задача уточнения: надо ли возвращать все индексные пары (как в оригинале) или только уникальные по значению пары. Предложу оба эффективных варианта.
1) Если нужен только один экземпляр каждой пары значений (каждая комбинация x,−xx,-xx,−x — один раз)
Алгоритм: проход по массиву с множеством "seen" и множеством результатов (чтобы не дублировать).
Время: O(n)O(n)O(n). Память: O(n+p)O(n + p)O(n+p), где ppp — число уникальных пар (обычно p≤np \le np≤n).
Пример (псевдо-/Python):
def unique_value_pairs(arr):
seen = set()
res = set()
for x in arr:
if -x in seen:
a, b = (x, -x) if x <= -x else (-x, x)
res.add((a, b))
seen.add(x)
return list(res)
Особенность: нули добавятся как одна пара (0,0)(0,0)(0,0) только если есть хотя бы два нуля.
2) Если нужно точно те же все индексные пары (включая дубликаты по индексам)
Алгоритм: хеш-таблица, хранящая ранее встретившиеся значения (или их индексы / счётчики). При обработке текущего элемента сразу генерируем пары с уже встреченными дополнениями — каждый индексный набор генерируется ровно один раз.
Время: O(n+k)O(n + k)O(n+k), где kkk — число возвращаемых пар (включая k=Θ(n2)k= \Theta(n^2)k=Θ(n2) в худшем случае). Память: O(n+k)O(n + k)O(n+k).
Пример:
from collections import defaultdict
def all_index_pairs(arr):
positions = defaultdict(list) # value -> list of indices (или просто counts, если не нужны индексы)
res = []
for j, x in enumerate(arr):
comp = -x
for i in positions[comp]: # генерация всех пар (i,j) с суммой 0
res.append((arr[i], arr[j]))
positions[x].append(j)
return res
Особенность: для нулей будет сгенерировано (c2)\binom{c}{2}(2c ) пар, где ccc — количество нулей.
Выводы по сложностям
- Исходный: время O(n2)O(n^2)O(n2), доп. память O(1)O(1)O(1) (без результата), память с результатом — до O(n2)O(n^2)O(n2).
- Улучшенный (уникальные значения): время O(n)O(n)O(n), память O(n)O(n)O(n).
- Улучшенный (все индексные пары): время O(n+k)O(n + k)O(n+k), память O(n+k)O(n + k)O(n+k), где kkk — число возвращаемых пар.
Выберите вариант в зависимости от требуемой семантики результата (уникальные по значению или все индексные пары).