mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-04 11:44:21 +00:00
91 lines
8.7 KiB
Markdown
91 lines
8.7 KiB
Markdown
# Вставка с использованием двоичного поиска
|
|
|
|
Двоичный поиск можно использовать не только для поиска целевого элемента, но и для решения множества других задач, таких как поиск позиции для вставки целевого элемента.
|
|
|
|
## Случай без повторяющихся элементов
|
|
|
|
!!! question
|
|
|
|
Дан упорядоченный массив `nums` длиной $n$ и элемент `target`, массив не содержит повторяющихся элементов. Необходимо вставить `target` в массив `nums`, сохранив его упорядоченность. Если массив уже содержит элемент `target`, вставить его слева от существующего. Вернуть индекс `target` в массиве после вставки. Пример показан на рисунке ниже.
|
|
|
|

|
|
|
|
Если требуется повторно использовать код двоичного поиска из предыдущего раздела, необходимо ответить на следующие два вопроса:
|
|
|
|
**Вопрос первый**: если массив содержит `target`, является ли индекс вставки индексом этого элемента?
|
|
|
|
Условие задачи требует вставить `target` слева от равного элемента, т. е. новый `target` заменяет старое положение `target`. То есть **если массив уже содержит `target`, индекс вставки совпадает с индексом этого `target`**.
|
|
|
|
**Вопрос второй**: если массив не содержит `target`, какой элемент будет иметь индекс вставки?
|
|
|
|
Дальнейший процесс двоичного поиска: когда `nums[m] < target`, указатель $i$ перемещается, т. е. приближается к элементу, большему или равному `target`. Аналогично указатель $j$ всегда приближается к элементу, меньшему или равному `target`.
|
|
|
|
Таким образом, по окончании двоичного поиска указатель $i$ указывает на первый элемент, больший `target`, а указатель $j$ -- на первый элемент, меньший `target`. **Легко понять, что, если массив не содержит `target`, индекс вставки будет равен $i$**. Ниже приведен пример кода.
|
|
|
|
```src
|
|
[file]{binary_search_insertion}-[class]{}-[func]{binary_search_insertion_simple}
|
|
```
|
|
|
|
## Случай с повторяющимися элементами
|
|
|
|
!!! question
|
|
|
|
На основе предыдущей задачи, предположим, что массив может содержать повторяющиеся элементы, остальные условия остаются неизменными.
|
|
|
|
Если в массиве существует несколько одинаковых `target`, то обычный двоичный поиск может вернуть индекс только одного из них, **не определяя, сколько `target` находится слева и справа от этого элемента**.
|
|
|
|
Задача требует вставить целевой элемент в самое левое положение, **поэтому необходимо найти индекс самого левого `target` в массиве**. Первоначально предполагается реализовать решение следующим образом (см. рисунок ниже):
|
|
|
|
1. Выполнить двоичный поиск и получить индекс любого `target`, обозначить его как $k$.
|
|
2. Начиная с индекса $k$, выполнить линейный обход влево и вернуть индекс, когда будет найден самый левый `target`.
|
|
|
|

|
|
|
|
Это рабочий метод, но он включает линейный поиск, поэтому его временная сложность составляет $O(n)$. Когда в массиве много повторяющихся `target`, эффективность этого метода низка.
|
|
|
|
Теперь рассмотрим расширение алгоритма двоичного поиска. Общий процесс остается неизменным: на каждом этапе сначала вычисляется средний индекс $m$, затем определяется отношение между `target` и `nums[m]`. Возможны следующие случаи:
|
|
|
|
- Когда `nums[m] < target` или `nums[m] > target`, тогда `target` еще не найден, поэтому используется операция сужения интервала обычного двоичного поиска, **чтобы указатели $i$ и $j$ приближались к `target`**.
|
|
- Когда `nums[m] == target`, тогда элементы, меньшие `target`, находятся в интервале $[i, m - 1]$. Поэтому используется операция $j = m - 1$ для сужения интервала, **чтобы указатель $j$ приблизился к элементам, меньшим `target`**.
|
|
|
|
После завершения цикла $i$ будет указывать на самый левый `target`, а $j$ -- на первый элемент, меньший `target`. **Поэтому индекс $i$ является точкой вставки**.
|
|
|
|
=== "<1>"
|
|

|
|
|
|
=== "<2>"
|
|

|
|
|
|
=== "<3>"
|
|

|
|
|
|
=== "<4>"
|
|

|
|
|
|
=== "<5>"
|
|

|
|
|
|
=== "<6>"
|
|

|
|
|
|
=== "<7>"
|
|

|
|
|
|
=== "<8>"
|
|

|
|
|
|
Рассмотрим следующий код: ветви условий `nums[m] > target` и `nums[m] == target` выполняют одинаковые операции, поэтому их можно объединить.
|
|
|
|
Тем не менее мы можем оставить условия развернутыми, поскольку логика становится более ясной и читаемой.
|
|
|
|
```src
|
|
[file]{binary_search_insertion}-[class]{}-[func]{binary_search_insertion}
|
|
```
|
|
|
|
!!! tip
|
|
|
|
Код в этом разделе использует запись «двойной замкнутый интервал». Заинтересованные читатели могут самостоятельно реализовать вариант «левозамкнутый правооткрытый интервал».
|
|
|
|
В целом двоичный поиск заключается в установке целей поиска для указателей $i$ и $j$. Целью может быть конкретный элемент (например, `target`) или диапазон элементов (например, элементы, меньшие `target`).
|
|
|
|
В процессе непрерывного циклического деления указатели $i$ и $j$ постепенно приближаются к заранее установленной цели. В конечном итоге они либо успешно находят ответ, либо останавливаются после пересечения границ. |