Kth Largest Element in an Array
Дан массив целых чисел nums и целое число k. Верните k-е наибольшее значение в nums: значение на позиции k при отсчёте с 1 после сортировки массива от наибольшего к наименьшему.
Одинаковые значения считаются отдельно. В [5, 5, 1] наибольшее значение — 5, а второе наибольшее — тоже 5.
Функция
- numsinteger-array
- значения для ранжирования
- kinteger
- какое наибольшее значение вернуть: 1 — для наибольшего
- Возвращаетinteger
- k-е по величине значение, включая дубликаты
Ограничения
1 ≤ k ≤ nums.length ≤ 104-104 ≤ nums[i] ≤ 104- Равные значения считаются отдельными значениями.
Примеры
- Ввод
- nums = [7, 2, 9, 4, 9, 1]k = 2
- Вывод
- 9
- Пояснение
- От наибольшего к наименьшему значения расположены так:
9, 9, 7, 4, 2, 1. Две девятки считаются отдельно, поэтому второе по величине значение —9, а не7.
- Ввод
- nums = [5, -3, 8, 0, 2]k = 4
- Вывод
- 0
- Пояснение
- От наибольшего к наименьшему значения расположены так:
8, 5, 2, 0, -3, и четвёртое из них —0.
- Ввод
- nums = [6]k = 1
- Вывод
- 6
- Пояснение
- При одном значении и
k = 1это значение является наибольшим.
+15 скрытых тестов при отправке
Дополнительный вопрос
Теперь значения поступают по одному. Сможешь сообщать медиану всех значений, полученных к этому моменту, после поступления каждого значения за O(log n) времени?
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Отсортированный от наибольшего к наименьшему, ответ находится на известной позиции. На какой именно? И нужны ли тебе все остальные значения, чтобы определить её?
k-е по величине значение — наименьшее из
kнаибольших значений. Если сохранять толькоkнаибольших значений, встреченных на данный момент, с каким из них сравнивать новое значение?Храни min-heap размером не более
kзначений. Новое значение заменяет верхний элемент, если оно больше, а в конце ответом будет верхний элемент. Для среднего времениO(n)разбивай массив относительно случайного опорного элемента, как это делает quicksort, и оставляй только ту часть, в которой находится индексn-k.
Решение
Сортировка и чтение элемента на нужной позиции дают ответ, и в данном случае это достаточно быстро. Интервьюер хочет увидеть, насколько большую часть сортировки можно пропустить, ведь нужна одна позиция, а не все n. Мини-куча размера k хранит только значения, которые ещё могут быть ответом, а quickselect разбивает массив на части, как quicksort, но продолжает работу только с той частью, где находится ответ, что снижает среднее время до O(n).
Сортируйте и считывайте одну позицию
Идея
Значение, являющееся k-м по величине, определяется порядком сортировки, поэтому нужно получить этот порядок. При сортировке от наибольшего к наименьшему [7, 2, 9, 4, 9, 1]</code превращается в <code>[9, 9, 7, 4, 2, 1], а k-е по величине значение находится по индексу k-1. При k = 2 это индекс 1 — вторая 9. Если при сортировке наименьшее значение оказывается первым, вместо этого считывай значение по индексу n-k: значение с индексом 4 в [1, 2, 4, 7, 9, 9] — та же 9.
Дубликаты не требуют особой обработки: сортировка сохраняет все копии, и каждая копия занимает собственную позицию.
При n = 10^4 сортировка выполняет около n log n ≈ 1.3 × 10^5 сравнений, чего достаточно для прохождения всех тестов. Недостаток в том, что она упорядочивает все n значений, хотя важна только одна позиция. Следующие два подхода позволяют выполнить меньше этой работы.
Алгоритм
- Скопируй
nums, чтобы массив вызывающего кода остался неизменным. - Отсортируй копию. Используй числовое сравнение; некоторые языки по умолчанию сравнивают числа как текст.
- Верни индекс
k-1при сортировке от большего к меньшему или индексn-kпри сортировке от меньшего к большему.
def findKthLargest(nums, k):
# Largest first: the k-th largest sits at index k-1.
ordered = sorted(nums, reverse=True)
return ordered[k - 1]Оставьте k наибольших элементов в минимальной куче
Идея
Значение, занимающее k-е место по величине, — наименьшее среди k наибольших значений. Поэтому один раз пройдись по nums и сохраняй только k наибольших значений, встреченных к этому моменту, в мин-куче. Вершина мин-кучи — её наименьшее значение, и именно оно является возможным ответом.
Когда приходит значение x и в куче хранится меньше k значений, добавь его. В противном случае сравни x с вершиной. Если x не больше, то как минимум k сохранённых тобой значений не меньше x, поэтому x никогда не сможет стать ответом, и его можно пропустить. Если x больше, вершина больше не входит в k наибольших значений: замени её на x. В примере 2 при k = 4 первые четыре значения заполняют кучу значениями 5, -3, 8, 0, а вершиной становится -3. Затем 2 оказывается больше -3 и заменяет его, вершиной становится 0, а 0 и есть ответ.
Для каждого значения требуется не более одной операции с кучей, занимающей O(log k), поэтому общая сложность составляет O(n log k) по времени и O(k) по памяти. Это быстрее сортировки, когда k мало, и подходит для потока данных: тебе никогда не нужно хранить все значения одновременно. В Python есть heapq, в Java — PriorityQueue, в C++ — priority_queue с greater, в Go — container/heap, в Rust — BinaryHeap с Reverse, а в PHP — SplMinHeap. В коде для остальных языков куча реализована в массиве: дочерние узлы элемента с индексом i находятся по индексам 2i+1 и 2i+2, а в Lua и R, где отсчёт начинается с 1, — по индексам 2i и 2i+1.
Алгоритм
- Начните с пустой минимальной кучи.
- Для каждого значения
xдобавляйте его, пока в куче меньшеkзначений. - Когда в куче будет
kзначений, заменяйте верхний элемент наxтолько в том случае, еслиxбольше верхнего элемента. - После последнего значения верните верхний элемент кучи.
import heapq
def findKthLargest(nums, k):
# A min-heap of the k largest values so far; its top is the smallest of them.
heap = []
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x) # drop the top, add x
return heap[0]Быстрый выбор с трёхсторонним разбиением
Идея
Быстрая сортировка выбирает опорный элемент и разбивает массив на части: меньшие значения оказываются слева от него, большие — справа. После одного разбиения опорный элемент занимает своё окончательное место в отсортированном массиве, хотя ни одна из частей ещё не отсортирована. На этом основан алгоритм Quickselect. При порядке от меньшего к большему ответ находится по индексу target = n-k. После разбиения target находится либо слева от опорного элемента, либо на его месте, либо справа от него, поэтому продолжайте поиск только в одной части, а другую пропустите.
Для массива [7, 2, 9, 4, 9, 1] и k = 2 индекс target равен 6-2 = 4. Разобьём массив относительно 4: 2 и 1 занимают индексы 0 и 1, 4 — индекс 2, а 7, 9, 9 — индексы с 3 по 5. Индекс 4 находится справа, поэтому оставляем только индексы с 3 по 5. Разобьём эту часть относительно 9: 7 занимает индекс 3, а обе девятки — индексы 4 и 5. По индексу 4 находится 9, поэтому ответ — 9.
Используйте трёхстороннее разбиение: сначала значения меньше опорного элемента, затем равные ему значения, а после них — большие. Границы частей отслеживаются с помощью lt и gt. Блок равных значений [lt, gt] уже находится на своём месте в отсортированном массиве, поэтому, если target попадает в него, поиск завершён. При обычном двухстороннем разбиении массив из 10^4 копий числа 7 уменьшается на одно значение за проход — это около 5 × 10^7 шагов; трёхсторонний вариант находит ответ за один проход.
Выбирайте опорный элемент случайным образом. В половине случаев он попадает в среднюю половину диапазона, сокращая диапазон как минимум до трёх четвертей исходного размера, поэтому ожидаемое количество работы составляет несколько проходов по n значениям: O(n). В худшем случае сложность всё ещё составляет O(n²), если каждый опорный элемент оказывается крайним значением; фиксированный выбор, например первого элемента, приводит к этому на отсортированном массиве. Код работает с копией, для которой требуется O(n) памяти; если входные данные можно изменять, разбиение самого массива nums сокращает расход памяти до O(1).
Алгоритм
- Скопируй
numsвa, установиtarget = n-k,lo = 0иhi = n-1. - Выбери случайный опорный элемент из
a[lo..hi]. - Разбей
a[lo..hi]на значения меньше опорного элемента, равные ему и больше него, оставив равные значения вa[lt..gt]. - Если
target < lt, установиhi = lt-1; еслиtarget > gt, установиlo = gt+1; в противном случае верни опорный элемент. - Повтори, начиная с шага 2.
import random
def findKthLargest(nums, k):
a = list(nums)
target = len(a) - k # the answer's index once a is sorted smallest first
lo, hi = 0, len(a) - 1
while True:
pivot = a[random.randint(lo, hi)]
# Three-way partition of a[lo..hi]: < pivot, then == pivot, then > pivot.
lt, i, gt = lo, lo, hi
while i <= gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]
lt += 1
i += 1
elif a[i] > pivot:
a[i], a[gt] = a[gt], a[i]
gt -= 1
else:
i += 1
# Now a[lt..gt] all equal pivot, and they are in their sorted places.
if target < lt:
hi = lt - 1
elif target > gt:
lo = gt + 1
else:
return pivot
Ловушки и крайние случаи
Большинство неправильных ответов связано с дубликатами и путаницей между двумя способами отсчёта позиций.
- Сначала удалять дубликаты. В задаче учитывается каждое вхождение: в
[7, 2, 9, 4, 9, 1]приk = 2ответ —9, но после преобразования массива в множество получится7. - Выбирать неправильный индекс. Отсчёт
kначинается с 1, поэтому ответ находится по индексуk-1при порядке от наибольшего к наименьшему и по индексуn-kпри порядке от наименьшего к наибольшему, а не по индексуn-k-1. - Сортировать числа как текст. В JavaScript и TypeScript вызов
[10, 9, 2].sort()возвращает[10, 2, 9]. Передайте(a, b) => a - b. - Использовать максимальную кучу размера
k. Удаление наибольшего элемента оставляетkнаименьших значений и возвращает k-й наименьший элемент. - Использовать Quickselect с двухсторонним разбиением или фиксированным опорным элементом. При большом количестве одинаковых значений или отсортированном массиве сложность составит
O(n²), что встречается в больших тестах.
Частые вопросы4
Какова временная сложность алгоритма поиска K-го наибольшего элемента в массиве?
Сортировка занимает время O(n log n). Мини-куча размером k требует времени O(n log k) и памяти O(k). Быстрый выбор со случайным опорным элементом в среднем занимает время O(n), а в худшем случае — O(n²), вероятность которого при случайном опорном элементе очень мала.
Почему для поиска k-го наибольшего элемента используют мин-кучу, а не макс-кучу?
Куча хранит k наибольших значений, встреченных на данный момент, и сравнивать с новым значением и удалять нужно наименьшее из них. Минимальная куча держит это значение на вершине. Максимальная куча подходит, только если поместить в неё все n значений и извлечь максимальный элемент k-1 раз, что требует O(n) памяти.
Что использовать для поиска k-го наибольшего элемента: кучу или quickselect?
Quickselect в среднем работает быстрее — O(n), но ему нужны все значения в памяти, и он меняет их порядок. Сложность кучи — O(n log k), при этом у неё нет плохого случая в худшем сценарии. Она подходит, когда значения поступают по одному и сохранить их все нельзя. На собеседовании объясни оба подхода и напиши код для того, который попросят в дополнительном вопросе.
Можно ли найти k-й по величине элемент за линейное время в худшем случае?
Да. Правило медианы медиан выбирает опорный элемент, который гарантированно отсечёт фиксированную долю значений, благодаря чему выбор выполняется за O(n) в худшем случае, хотя на практике этот метод медленнее, чем выбор случайного опорного элемента. Если значения ограничены диапазоном от -10^4 до 10^4, можно также подсчитать, сколько раз встречается каждое значение, и идти вниз от 10^4, пока не пройдёте k значений, за время O(n + 2 × 10^4).
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def findKthLargest(nums, k):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
nums = [7, 2, 9, 4, 9, 1] k = 2
Ожидается
9