Массивы
Заменить элементы наибольшим справа
Заменить каждый элемент в массиве наибольшим элементом справа
Постановка задачи
Описание (LeetCode)
Дан целочисленный массив arr, замените каждый элемент в этом массиве
наибольшим среди элементов справа от него, последний элемент замените на -1.
После выполнения замены верните этот массив.
Пример 1:
arr = [17, 18, 5, 4, 6, 1]
Результат:
[18, 6, 6, 6, 1, -1]
Пояснение:
- индекс 0 --> наибольший элемент справа от индекса 0 имеет индекс 1 (18)
- индекс 1 --> наибольший элемент справа от индекса 1 имеет индекс 4 (6)
- индекс 2 --> наибольший элемент справа от индекса 2 имеет индекс 4 (6)
- индекс 3 --> наибольший элемент справа от индекса 3 имеет индекс 4 (6)
- индекс 4 --> наибольший элемент справа от индекса 4 имеет индекс 5 (1)
- индекс 5 --> справа от индекса 5 нет элементов, поэтому используем -1
Пример 2:
arr = [400]
Результат:
[-1]
Пояснение:
Справа от индекса 0 нет элементов.
Ограничения:
1 <= arr.length <= 10⁴1 <= arr[i] <= 10⁵
Решение
Это легкая задача на понимание прохода с конца - приема, который часто спасает, если нужно модифицировать массив "на месте" (in-place).
Наивное решение - это дополнительный поиск наибольшего элемента справа (вложенный цикл) от текущего элемента для каждого элемента массива (внешний цикл).
- Внешний цикл: проходим по массиву слева направо и для каждого элемента с
индексом
i:- Внутренний цикл (поиск): запускаем второй цикл, который просматривает
все элементы от
i + 1до самого конца массива и находит самое большое число справа от индексаi. - Найденное число записываем в массив на позицию
i.
- Внутренний цикл (поиск): запускаем второй цикл, который просматривает
все элементы от
- Обработка исключения: записываем
-1на место последнего элемента.
В таком решении "в лоб" (brute force, полный перебор) мы получим сложность
O(n²), что неэффективно для больших массивов. На интервью вы можете предложить
такое решение, но только как стартовую версию, которую сразу нужно будет
улучшить.
Решение задачи "в лоб" может быть полезно, и не стоит сразу от него отмахиваться. Хотя такие решения почти всегда проигрывают по производительности, на практике их используют в нескольких сценариях: в тестировании, когда малы объемы данных, а также как базу для понимания задачи и улучшения алгоритма решения.
Давайте попытаемся найти более оптимальный подход.
Когда мы идем слева направо, для элемента i мы сканируем все элементы от i + 1 до конца и находим наибольший элемент, но при переходе к элементу i + 1 мы
опять же сканируем все те же элементы от i + 2 до конца массива. Мы постоянно
пересчитываем наибольший элемент на пересекающихся отрезках.
Давайте представим себе, что мы уже нашли наибольший элемент справа для i+1-го
элемента в подмассиве arr[i+1 ... n-1]. Каким будет наибольший элемент справа
для i-го элемента? Ясно, что это либо тот же самый наибольший элемент из
arr[i+1 ... n-1], либо i+1-й элемент.
Это ключевая идея для оптимального решения. Вместо того, чтобы постоянно пересчитывать наибольший элемент, мы могли бы запоминать и использовать найденный на отрезке справа наибольший элемент. Для этого нам нужно двигаться справа налево по массиву, но это совсем несложно сделать.
В задачах на массивы, где результат элемента зависит от его правых соседей, всегда стоит задать себе вопрос: "А что, если пойти с конца?"
Если мы стоим в конце, мы уже "знаем" все о правой части, потому что мы ее только что прошли. Двигаясь справа налево "будущее" (то, что справа) становится для нас "прошлым" (тем, что мы уже видели).
Таким образом алгоритм заключается в том, чтобы двигаться справа налево, сохраняя значение самого большого встреченного элемента.
Основные шаги:
- Инициализируем переменную
currentMaxзначением-1(для последнего элемента правого соседа нет). - Идем циклом с последнего индекса массива к нулевому:
- Сохраняем текущее значение элемента во временную переменную.
- Заменяем текущий элемент массива на
currentMax. - Обновляем
currentMax, выбирая максимум между старым значением и сохраненным элементом.
- Возвращаем измененный массив.
После обсуждения идеи решения надо прогнать его на примерах вручную. В этой легкой задаче мы предоставим вам это сделать самостоятельно.
Оценка сложности
Временная сложность
Мы проходим по массиву ровно один раз с конца до начала с помощью одного цикла. Каждая итерация цикла выполняет операции с константным временем.
Таким образом, это линейная сложность: O(n).
Пространственная сложность
Мы используем фиксированное количество дополнительной памяти - это константная
сложность: O(1).
Такое решение является оптимальным.
Код решения
Теперь можем написать код решения.
func replaceElements(arr []int) []int {
currentMax := -1
for i := len(arr) - 1; i >= 0; i-- {
temp := arr[i]
arr[i] = currentMax
currentMax = max(currentMax, temp)
}
return arr
}Итоги
В ходе разбора задачи мы перешли от интуитивного, но медленного подхода к оптимальному алгоритму. Вот главные выводы:
-
Оптимизация через направление: ключом к решению стал обратный порядок обхода (проход с конца). Это позволило превратить задачу с квадратичной сложностью
O(n²)в линейнуюO(n). -
Экономия ресурсов: благодаря модификации исходного массива "на месте" (in-place) и использованию всего одной вспомогательной переменной
currentMax, мы добились константногоO(1)потребления памяти. -
Универсальный паттерн: данная задача - это классический пример использования суффиксного максимума. Этот прием полезен в любых сценариях, где текущее решение зависит от состояния "будущих" элементов массива.
-
Практический навык: повторяющийся поиск максимума в одних и тех же подотрезках - верный сигнал к тому, что задачу можно решить эффективнее с помощью накопления состояния в переменной.