Где применяется
1. Задача
В двух прошлых уроках список был отсортирован целиком, от меньшего к большему. Бинарный поиск работал, потому что по средней ячейке всегда было видно, в какой половине искомое значение (target). Теперь список тоже отсортирован, но его повернули: разрезали в одном месте и поменяли два куска местами.
Возьми [4, 5, 6, 7, 0, 1, 2]. Числа растут 4, 5, 6, 7, потом падают до 0, потом снова растут 1, 2. Это список [0, 1, 2, 4, 5, 6, 7], повёрнутый так, что первым стоит 4. Все числа по-прежнему разные, и внутри каждого куска они идут по возрастанию. Где именно падение, ты не знаешь.
Нужно найти индекс значения за O(log n), так же быстро, как обычным бинарным поиском. Проход по всем ячейкам занял бы O(n) и никак не использовал бы порядок, который в списке остался.
2. Простыми словами
Ищем 0 в [4, 5, 6, 7, 0, 1, 2]. Посередине стоит 7. Слева от неё 4, 5, 6, 7: числа растут без падения. Значит, левая половина отсортирована, и в ней лежат только числа от 4 до 7. Ноля среди них быть не может. Значит, он справа: [0, 1, 2].
Так работает каждый шаг. Ты делишь список пополам. Одна из половин всегда идёт ровно по порядку, без падения внутри. По её первому и последнему числу видно, какие числа в ней есть. Если искомое число попадает в этот промежуток, ищи в этой половине. Если нет, ищи в другой. В обоих случаях половина чисел отбрасывается.
3. Идея
Держи две границы, lo и hi, и бери середину mid. Если nums[mid] равно target, поиск закончен. Если нет, надо выбрать сторону. Массив не отсортирован целиком, поэтому одного сравнения target с серединой не хватает.
Опорный факт: при любом mid хотя бы одна из двух половин отсортирована целиком, без падения внутри. Чтобы узнать какая, сравни nums[lo] с nums[mid]. Если nums[lo] <= nums[mid], отсортирована левая половина. Иначе падение лежит слева, и отсортирована правая. Теперь у тебя есть половина, для которой ты точно знаешь промежуток значений. Если target в него попадает, переходи в эту половину. Если нет, переходи в другую.
Проследим на [4, 5, 6, 7, 0, 1, 2], ищем 0:
- Начало:
lo = 0,hi = 6,mid = 3,nums[mid] = 7. Это не 0.nums[lo] = 4 <= 7, значит, левая половина4..7отсортирована. 0 не лежит в4..7, поэтому идём вправо:lo = 4. - Теперь
lo = 4,hi = 6,mid = 5,nums[mid] = 1. Это не 0.nums[lo] = 0 <= 1, значит, левая половина0..1отсортирована. 0 лежит в ней, поэтому идём влево:hi = 4. - Теперь
lo = 4,hi = 4,mid = 4,nums[4] = 0. Совпало, возвращаем 4.
4. Во что это обходится
Время: O(log n). Каждый шаг, как в обычном бинарном поиске, отбрасывает половину оставшегося. На шаг добавляется пара сравнений, чтобы выбрать отсортированную половину. Это O(1), и число шагов от них не меняется.
Память: O(1). Ты хранишь lo, hi и mid, и ничего из этого не растёт с размером входа.
5. Где это встречается
Кольцевой буфер (массив постоянного размера, который заполняется по кругу) записывает данные по порядку. Дойдя до конца, он продолжает с начала, и место перехода заранее неизвестно. Поэтому сохранённый массив оказывается повёрнутым отсортированным массивом, и поиск значения в нём работает так же. Ещё пример: чтение кольцевого лога по метке времени или номеру записи без прохода по всему логу. В целом этот поиск подходит любой структуре, которая хранит данные по порядку, но начинается с меняющегося места, когда поиск всё равно должен идти за O(log n).
searchRotated([7, 8, 9, 1, 2, 3, 4, 5, 6], 9)lo
hi
Ищем 9 в отсортированном массиве, который разрезали и переставили. Каждая из двух частей внутри отсортирована.
Столбики: отсортированные числа, сдвинутые на 14, поэтому наименьшее стоит на позиции 14. Обе дорожки ищут число ближе к концу.
Одна операция: одно чтение элемента. За каждый кадр все дорожки делают одинаковое число операций, поэтому медленная отстаёт.
O(n)O(log n)
Операции для n чисел и примерное время при 100 миллионах простых операций в секунду. Это оценка по тому же счёту, а не замер.
Как узнать
Эти слова в условии значат этот паттерн
- отсортированный массив провёрнут в неизвестной точке
- найти цель за O(log n)
- без дубликатов (или обработать их отдельно)
- найти минимум / точку вращения
- массив растёт, а потом один раз падает
Похоже, но не то
- Бинарный поиск (массив) (Binary search (array))
Обычный поиск сравнивает середину с целью. Здесь она сначала сравнивается с границей.
- Линейный поиск (Linear search)
Перебор справляется с вращённым массивом вообще без рассуждений. Он стоит O(n) вместо O(log n).
- Бинарный поиск по ответу (Binary search on the answer)
Там отрезок состоит из кандидатов в ответы. Здесь вы ищете реально лежащие значения.
- Быстрая сортировка (merge / quick) (Fast sort (merge / quick))
Сортировка вернёт порядок, но стоит n log n. Поворот и так оставляет достаточно порядка.
n до 1e9 даёт O(log n), как и обычный поиск. Дубликаты доводят худший случай до O(n).
На LeetCode
Задачи на отсортированный массив, который разрезали и поменяли части местами, где нужно найти значение, минимум или пик за O(log n). На каждом шаге одна половина остаётся отсортированной, и её края подсказывают, какую сторону оставить.
Это задача урока, все значения разные.
Сравнивай середину с правым краем, чтобы понять, на какой стороне обрыв.
Тот же поиск с повторами, и когда nums[lo] == nums[mid], приходится сдвигать lo на один.
Дели диапазон пополам, сравнивая середину с её правым соседом.
Твоя задача (Tier 1)
Напиши функцию searchRotated(nums, target).
Список был отсортирован от меньшего к большему. Потом его разрезали в одном месте и поменяли две части местами. Поэтому числа растут, один раз падают и снова растут, например [4, 5, 6, 7, 0, 1, 2]. Все числа разные. Найди target и верни его позицию или -1, если его нет. Дели диапазон пополам на каждом шаге и не проходи по всем числам подряд.
- Вход:
nums, повёрнутый отсортированный список из разных чисел, иtarget, число, которое ищешь. Пример:[4, 5, 6, 7, 0, 1, 2]иtarget = 0. - Возвращает: позицию
targetили-1, если его нет.
Шаги:
- Держи две границы,
loиhi. Покаlo <= hi, бери среднюю позициюmid. Еслиnums[mid]равноtarget, верниmid. - Одна из половин вокруг
midвсегда отсортирована. Сравниnums[lo]сnums[mid], чтобы понять, какая. - Если
targetпопадает в промежуток значений отсортированной половины, переходи в неё. Иначе переходи в другую половину. Если границы разошлись, верни-1.
Готово, когда проходят все тесты:
[4, 5, 6, 7, 0, 1, 2], target0->4(во второй части)[4, 5, 6, 7, 0, 1, 2], target5->1(в первой части)[4, 5, 6, 7, 0, 1, 2], target3->-1(такого числа нет)[1, 2, 3, 4, 5], target4->3(список совсем не повёрнут)[6, 7, 0, 1, 2, 4, 5], target6->0(прямо на месте разреза)[1], target1->0(одно число, нашлось)[1], target2->-1(одно число, не нашлось)[], target5->-1(пустой список, функция не должна падать)
Практика
searchRotated
searchRotated(nums, target) -> index of target in the rotated sorted array, or -1
export function searchRotated(nums, target) {
// Your code here. The task is described in the box above the editor.
}
Ты здесь как гость
Тесты запускаются прямо здесь, без аккаунта. Код сохраняется только в этом браузере. Бесплатный аккаунт сохранит его на любом устройстве, отметит шаг пройденным и покажет, где ты в курсе.
Лестница подсказок
Застрял? Открывай по одной, но сначала попробуй сам.