введение
Сегодняшняя статья объясняет тему, которая активно используется в интервью с различными крупными компаниями (Tencent, Byte, Ali и т. д.): вопрос Top K, который будет представлен в следующем контексте:
- В чем проблема Top K
- Пять классических решений проблемы Top K
- Быстрый обзор и резюме
- Упражнение Top K Three Musketeers: минимум K чисел, K топовых высокочастотных элементов, K-й наименьший элемент (включая решения)
Давайте начнем ниже.
1. В чем проблема Top K
В чем проблема Top K? Проще говоря, это поиск первых K чисел с наибольшей частотой в наборе данных или первых K больших (конечно, лучших K маленьких) чисел.
Классические задачи Top K: наибольшее (наименьшее) число K, первые K высокочастотных элементов, K-й наибольший (наименьший) элемент.
Ниже приведен пример поиска K-го по величине элемента в массиве, чтобы представить решение проблемы Top K.
тема:
найти первое в несортированном массивеkсамый большой элемент. Обратите внимание, что вам нужно найти k-й самый большой элемент отсортированного массива.
Пример:
输入: [4,5,1,6,2,7,3,8] 和 k = 4
输出: 5
Во-вторых, решение: глобальная сортировка, взять k-е число
Самое простое, что мы можем придумать, это отсортировать массив (это может быть простейшая быстрая сортировка), просто взять первые K чисел, так просто
Код:
let findKthLargest = function(nums, k) {
nums.sort((a, b) => b - a);
return nums[k-1]
};
Анализ сложности:
- Временная сложность: O(nlogn)
- Сложность пространства: O(logn)
Уведомление:
До версии движка V8 7.0, когда длина массива меньше 10,Array.prototype.sort()
Используется сортировка вставками, в противном случае используется быстрая сортировка.
От быстрой сортировки отказались после версии движка V8 7.0, потому что это нестабильный алгоритм сортировки, и в худшем случае временная сложность упадет до O(n2).
Вместо этого используется гибридный алгоритм сортировки:TimSort.
Этот функциональный алгоритм изначально использовался в языке Python, строго говоря, он не принадлежит ни к одному из 10 вышеперечисленных алгоритмов сортировки, а относится к гибридному алгоритму сортировки:
Использование в подмассивах с небольшим объемом данныхСортировка вставками, затем используйтеСортировка слияниемСортировка слиянием отсортированных подмассивов, временная сложностьO(nlogn)
.
В-третьих, решение: локальная сортировка, барботирование
Теме нужно только спросить k-й самый большой элемент в массиве, нет необходимости сортировать массив.
Вы можете использовать Bubble Source, чтобы пузырировать наибольшее количество на данный момент каждый раз и только пузырьк K раз.
Анимированная демонстрация
Код
let findKthLargest = function(nums, k) {
// 进行k轮冒泡排序
bubbleSort(nums, k)
return nums[nums.length-k]
}
let bubbleSort = function(arr, k) {
for (let i = 0; i < k; i++) {
// 提前退出冒泡循环的标识位
let flag = false;
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
// 表示发生了数据交换
}
}
// 没有数据交换
if(!flag) break
}
}
Анализ сложности:
- Временная сложность: лучшая временная сложность O (n), средняя временная сложность O (n * k)
- Космическая сложность: O(1)
В-четвертых, решение: до строительстваk
Маленькая верхняя куча с самым большим элементом, возьмите вершину кучи
Мы также можем построить пред-k
Значение любого узла в небольшой верхней куче должно быть меньше или равно значению его левого и правого дочерних узлов, то есть вершина кучи является минимальным значением.
Итак, мы можем взять из массиваk
Элементы создают небольшую верхнюю кучу, а затем сравнивают оставшиеся элементы с маленькой верхней кучей. Если она больше, чем верхняя часть кучи, замените верхнюю часть кучи, а затем сложите ее. После обхода всех элементов верхняя кучи является первым.k
Максимум
Конкретные шаги заключаются в следующем:
- выйти вперед из массива
k
количество (0
прибытьk-1
биты), построить небольшую верхнюю кучу - от
k
Бит начинает обход массива, и каждые данные сравниваются с верхним элементом маленькой верхней кучи, если он меньше верхнего элемента кучи, ничего не делать и продолжать обход следующего элемента, если он больше верхний элемент кучи, замените этот элемент.Соберите верхний элемент в кучу, а затем сложите его в маленькую верхнюю кучу. - После завершения обхода данные в верхней части кучи являются K-ми по величине данными.
Код:
let findKthLargest = function(nums, k) {
// 从 nums 中取出前 k 个数,构建一个小顶堆
let heap = [,], i = 0
while(i < k) {
heap.push(nums[i++])
}
buildHeap(heap, k)
// 从 k 位开始遍历数组
for(let i = k; i < nums.length; i++) {
if(heap[1] < nums[i]) {
// 替换并堆化
heap[1] = nums[i]
heapify(heap, k, 1)
}
}
// 返回堆顶元素
return heap[1]
};
// 原地建堆,从后往前,自上而下式建小顶堆
let buildHeap = (arr, k) => {
if(k === 1) return
// 从最后一个非叶子节点开始,自上而下式堆化
for(let i = Math.floor(k/2); i>=1 ; i--) {
heapify(arr, k, i)
}
}
// 堆化
let heapify = (arr, k, i) => {
// 自上而下式堆化
while(true) {
let minIndex = i
if(2*i <= k && arr[2*i] < arr[i]) {
minIndex = 2*i
}
if(2*i+1 <= k && arr[2*i+1] < arr[minIndex]) {
minIndex = 2*i+1
}
if(minIndex !== i) {
swap(arr, i, minIndex)
i = minIndex
} else {
break
}
}
}
// 交换
let swap = (arr, i , j) => {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
Анализ сложности:
- Временная сложность: для обхода массива требуется временная сложность O (n), а для одной кучи требуется временная сложность O (logk), поэтому временная сложность использования кучи для поиска задачи Top k составляет O (nlogk).
- Пространственная сложность: O(k)
Воспользуйтесь преимуществами heap top k задач
Такого рода проблему Top k можно решить с помощью сортировки, но что, если нам нужно найти элементы Top k в динамическом массиве?
Динамические массивы могут вставлять или удалять элементы, нужно ли нам переупорядочивать массив каждый раз, когда мы решаем задачу Top k? Временная сложность каждого времени составляет O (nlogn)
Здесь вы можете использовать кучу, мы можем поддерживать небольшой размер верхней части кучи A k, когда данные добавляются в массив, его будет сравниваться с верхней частью элемента кучи, если элемент больше, чем верхний кучи, он заменит элемент куча верхнего элемента, а затем в верхнюю часть кучи кучи; если элемент меньше верхней части кучи, то без обработки. Таким образом, каждый раз, когда ищут верхнюю сложность задачи K (LOGK)
Можно просмотреть больше содержимого кучиAdvanced Front-End Algorithm 9: после прочтения этой статьи я больше не буду бояться сортировки в куче, Top K и опросов по медианным вопросам
5. Решение: оптимизация, алгоритм быстрого выбора
Будь то алгоритм сортировки или построение кучи для решения задачи Top k, мы прошли некоторое количество ненужных операций:
- Если мы используем алгоритм сортировки, нам нужно только k-е наибольшее значение, но мы сортируем массив глобально или локально.
- Если вы используете сортировку кучей, вам необходимо поддерживать размер
k
Куча (куча большая точка, верхняя часть маленькой кучи), в то время как тратить время очень дорого и сложность времениO(nlogk)
Итак, существует ли алгоритм для получения K-го по величине элемента без сортировки и без траты лишнего места?
Это про алгоритм быстрого выбора 😊
Алгоритм quickselect по идее похож на quicksort.Давайте популяризируем quicksort.Кто уже знает,может пропустить этот абзац.
Быстрый ряд
Быстрая сортировка использует идею стратегии «разделяй и властвуй».Так называемая «разделяй и властвуй», как следует из названия, заключается в том, чтобы разделить и властвовать, разделить сложную проблему на две или более похожие подзадачи, а затем разделить подзадачи. проблемы на более мелкие подзадачи до тех пор, пока меньшие подзадачи не могут быть просто решены, а подзадачи решены, а решение исходной проблемы представляет собой комбинацию решений подзадач.
Процесс быстрой сортировки состоит всего из трех шагов:
- Сначала выберите номер из последовательности в качестве ссылочного номера
- Поместите все числа, большие этого числа, справа от него, а все числа, меньшие или равные ему, поместите слева от него (быстрая сортировка).
partition
) - Затем повторите описанные выше операции для левой и правой сторон эталона соответственно, пока массив не будет полностью отсортирован.
Конкретные шаги заключаются в следующем:
- 1. Создайте два указателя на крайний левый и крайний правый концы массива соответственно.
- 2, произвольно взять элемент в массиве в качестве эталона
- 3. Левый указатель начинает двигаться вправо и натыкается на упор больше основания
- 4. Правый указатель начинает двигаться влево, останавливается, когда встречает элемент, меньший, чем ссылка, и меняет местами элементы, на которые указывают левый и правый указатели.
- 5. Повторяйте 3 и 4, пока левый указатель не превысит правый указатель.В это время значение, меньшее, чем эталонное значение, будет помещено в левую часть эталона, а значение, большее, чем эталонное значение, появится в правой части эталон
- 6, а затем повторять описанные выше операции для левой и правой частей эталона до тех пор, пока массив не будет полностью отсортирован.
Заметили, как бенчмарки здесь выбраны Нангом? Самый простой способ сделать это — каждый раз выбирать крайний левый элемент в качестве ссылки:
Но это не лучший выбор для почти уже упорядоченных последовательностей, он приведет к худшей производительности алгоритма. Другой способ - выбрать средний номер или пройтиMath.random()
Чтобы случайным образом выбрать число в качестве эталона, следующая реализация кода основана на случайном числе в качестве эталона.
Код
let quickSort = (arr) => {
quick(arr, 0 , arr.length - 1)
}
let quick = (arr, left, right) => {
let index
if(left < right) {
// 划分数组
index = partition(arr, left, right)
if(left < index - 1) {
quick(arr, left, index - 1)
}
if(index < right) {
quick(arr, index, right)
}
}
}
// 一次快排
let partition = (arr, left, right) => {
// 取中间项为基准
var datum = arr[Math.floor(Math.random() * (right - left + 1)) + left],
i = left,
j = right
// 开始调整
while(i <= j) {
// 左指针右移
while(arr[i] < datum) {
i++
}
// 右指针左移
while(arr[j] > datum) {
j--
}
// 交换
if(i <= j) {
swap(arr, i, j)
i += 1
j -= 1
}
}
return i
}
// 交换
let swap = (arr, i , j) => {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
// 测试
let arr = [1, 3, 2, 5, 4]
quickSort(arr)
console.log(arr) // [1, 2, 3, 4, 5]
// 第 2 个最大值
console.log(arr[arr.length - 2]) // 4
Быстрый ряд — это сортировка от мелкого к крупному, поэтомуk
Максимальное значение находится наn-k
место нахождения
Анализ сложности
- Временная сложность: O(nlogn)
- Пространственная сложность: O(nlogn)
алгоритм быстрого выбора
Выше мы реализовали быструю сортировку, чтобы получить k-е наибольшее значение, но это не обязательно так хлопотно.
Нам нужно только сравнить, находится ли позиция эталонного значения вn-k
позиция,
- если меньше чем
n-k
, то k-е максимальное значение находится справа от эталонного значения, нам нужно только рекурсивно быстро отсортировать подпоследовательность справа от эталонного значения; - если больше, чем
n-k
, то k-е максимальное значение находится на стороне эталонного значения, нам нужно только рекурсивно быстро отсортировать подпоследовательность слева от эталонного значения; - если равно
n-k
, то k-е наибольшее значение является эталонным значением
Код:
let findKthLargest = function(nums, k) {
return quickSelect(nums, nums.length - k)
};
let quickSelect = (arr, k) => {
return quick(arr, 0 , arr.length - 1, k)
}
let quick = (arr, left, right, k) => {
let index
if(left < right) {
// 划分数组
index = partition(arr, left, right)
// Top k
if(k === index) {
return arr[index]
} else if(k < index) {
// Top k 在左边
return quick(arr, left, index-1, k)
} else {
// Top k 在右边
return quick(arr, index+1, right, k)
}
}
return arr[left]
}
let partition = (arr, left, right) => {
// 取中间项为基准
var datum = arr[Math.floor(Math.random() * (right - left + 1)) + left],
i = left,
j = right
// 开始调整
while(i < j) {
// 左指针右移
while(arr[i] < datum) {
i++
}
// 右指针左移
while(arr[j] > datum) {
j--
}
// 交换
if(i < j) swap(arr, i, j)
// 当数组中存在重复数据时,即都为datum,但位置不同
// 继续递增i,防止死循环
if(arr[i] === arr[j] && i !== j) {
i++
}
}
return i
}
// 交换
let swap = (arr, i , j) => {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
Анализ сложности:
- Временная сложность: средняя временная сложность составляет O (n), а временная сложность в худшем случае — O (n).2)
- Космическая сложность: O(1)
Шестое, решение: продолжить оптимизацию алгоритма медианы медианы (BFPRT)
Также известен какМедианный алгоритм для медианы, его наихудшая временная сложность равна O(n) , которая определяется выражениемБлюм, Флойд, Пратт, Ривест, Тарьянпредложить. Идея алгоритма состоит в том, чтобы модифицировать метод выбора поворота алгоритма быстрого выбора, чтобы улучшить временную сложность алгоритма в худшем случае.
В алгоритме BFPTR изменен только алгоритм быстрого выбораPartion
При выборе эталонного значения в алгоритме quickselect мы можем выбрать первый элемент или последний элемент в качестве эталонного элемента, а оптимизированный может выбрать в качестве эталонного элемента случайный элемент, тогда как в алгоритме BFPTR каждый время Выберите медиану квинтилей в качестве базовой единицы (также известную как опорнаяpivot), цель этого состоит в том, чтобы сделать разделение более разумным, чтобы избежать наихудшего случая.
Шаги алгоритма BFPRT следующие:
- Выберите сводную точку
- Разделите n элементов по порядку на
n/5
группы по 5 элементов в каждой, отбросить все оставшиеся - за это
n/5
Каждый набор групп, использующих сортировку вставками, найден в соответствующей медиане - Для всех медиан, найденных на предыдущем шаге, вызовите алгоритм BFPRT, чтобы найти их медианы в качестве точки опоры;
- Разделите n элементов по порядку на
- За точку разделения возьмем стержень, поставим меньший, чем стержень, слева, а больший, чем стержень, справа;
- Определите положение опорной точки и размер k и выборочно рекурсивно влево или вправо
Код:
let findKthLargest = function(nums, k) {
return nums[bfprt(nums, 0, nums.length - 1, nums.length - k)]
}
let bfprt = (arr, left , right, k) => {
let index
if(left < right) {
// 划分数组
index = partition(arr, left, right)
// Top k
if(k === index) {
return index
} else if(k < index) {
// Top k 在左边
return bfprt(arr, left, index-1, k)
} else {
// Top k 在右边
return bfprt(arr, index+1, right, k)
}
}
return left
}
let partition = (arr, left, right) => {
// 基准
var datum = arr[findMid(arr, left, right)],
i = left,
j = right
// 开始调整
while(i < j) {
// 左指针右移
while(arr[i] < datum) {
i++
}
// 右指针左移
while(arr[j] > datum) {
j--
}
// 交换
if(i < j) swap(arr, i, j)
// 当数组中存在重复数据时,即都为datum,但位置不同
// 继续递增i,防止死循环
if(arr[i] === arr[j] && i !== j) {
i++
}
}
return i
}
/**
* 数组 arr[left, right] 每五个元素作为一组,并计算每组的中位数,
* 最后返回这些中位数的中位数下标(即主元下标)。
*
* @attention 末尾返回语句最后一个参数多加一个 1 的作用其实就是向上取整的意思,
* 这样可以始终保持 k 大于 0。
*/
let findMid = (arr, left, right) => {
if (right - left < 5)
return insertSort(arr, left, right);
let n = left - 1;
// 每五个作为一组,求出中位数,并把这些中位数全部依次移动到数组左边
for (let i = left; i + 4 <= right; i += 5)
{
let index = insertSort(arr, i, i + 4);
swap(arr[++n], arr[index]);
}
// 利用 bfprt 得到这些中位数的中位数下标(即主元下标)
return findMid(arr, left, n);
}
/**
* 对数组 arr[left, right] 进行插入排序,并返回 [left, right]
* 的中位数。
*/
let insertSort = (arr, left, right) => {
let temp, j
for (let i = left + 1; i <= right; i++) {
temp = arr[i];
j = i - 1;
while (j >= left && arr[j] > temp)
{
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = temp;
}
return ((right - left) >> 1) + left;
}
// 交换
let swap = (arr, i , j) => {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
Анализ сложности:
Почему 5?
Почему в алгоритме BFPRT выбрано 5 групп?
Во-первых, исключаются четные числа, потому что для нечетных чисел легче вычислить медиану.
Если выбрано 3,
, количество элементов операнда по-прежнемуn
.
Если вы выберете 7, 9 или больше, время, затрачиваемое на сортировку вставками, постоянно увеличивается.c
Он будет огромным, и это небольшая потеря.
7. Обзор и заключение
Напоследок резюмируем, что найти задачу topk несложно, есть в основном следующие идеи:
- Общая сортировка: O(nlogn)
- частичный заказ: только верхние k самых больших значений в пузырьковой сортировке, O(n*k)
- использовать кучу: O(nlogk)
- подсчет или сортировка ведра: сортировка подсчетом используется для k самых высоких значений, а временная сложность составляет O(n + m), где m представляет диапазон данных; сортировка сегментов используется для наибольшей частоты k, а временная сложность составляет O(n). ;Но оба требуют, чтобы входные данные были целыми числами с определенным диапазоном, эта статья не представляет слишком многого, вы можете перейти кGitHub.com/sister/JA…См. серию кучи для получения дополнительной информации.
- Оптимизация: алгоритм быстрого выбора: Средний O (n), худший o (n2)
- Продолжить оптимизацию: алгоритм средних чисел (BFPRT): наихудший O (n)
Вот краткое описание последних двух вариантов:
-
Прежде всего, вам нужно знать, что такое быстрая сортировка.Процесс быстрой сортировки состоит из трех шагов: во-первых, выберите число из последовательности в качестве ссылочного номера, поместите все числа слева от него (быстрая сортировка
partition
); затем повторите вышеуказанные операции для левой и правой сторон эталона, пока массив не будет полностью отсортирован -
Алгоритм быстрого выбора: быстрая сортировка сортирует все данные, и нам нужно только k-е наибольшее значение, поэтому
quickselect
Оптимизировать быструю сортировку: после каждого выполнения быстрой сортировки (partition
), сравните, находится ли позиция опорного значения вn-k
(к-е наибольшее = n-к-е наименьшее, n - длина массива) позиция, если меньшеn-k
, то k-е максимальное значение находится справа от эталонного значения, нам просто нужно рекурсивно отсортировать подпоследовательность справа от эталонного значения; если оно большеn-k
, то k-е максимальное значение находится на стороне эталонного значения, нам нужно только рекурсивно отсортировать подпоследовательность слева от эталонного значения, если она равнаn-k
, то k-е максимальное значение является эталонным значением, средняя временная сложность равна O(n), а временная сложность в наихудшем случае равна O(n2), в худшем случае временная сложность все еще очень высока -
Алгоритм медианы медиан (bfprt): Идея этого алгоритма состоит в том, чтобы изменить быстрый выбор (
quickselect
) метод выбора поворота алгоритма для улучшения временной сложности алгоритма в худшем случае
Наконец, прикрепите классические практические вопросы из Top K вопросов, слишком много вопросов для решения, перейдите в репозиторий git, чтобы просмотреть ответы.
-
Tencent & bytes и т. д.: наименьшее число k:
-
leetcode347: Топ K высокочастотных элементов:
-
Byte & leetcode215: массив из K-го самого большого элемента:
Прекрасное прошлое
- Advanced Front-End Algorithm 9: после прочтения этой статьи я больше не буду бояться сортировки в куче, Top K и опросов по медианным вопросам
- Усовершенствованный алгоритм внешнего интерфейса 8: проблема с хеш-таблицей в заголовках
- Front-end Advanced Algorithm 7: деревья и бинарные деревья, которые может понять Xiaobai
- Интерфейсный расширенный алгоритм 6: очередь, двусторонняя очередь, скользящее окно и вспомогательные вопросы по алгоритму
- Передовой расширенный алгоритм: общие проблемы алгоритма и идеальные решения
- Видео-интервью УВЧ онлайн вопросы по программированию, понимания которых достаточно для работы с большинством компаний
- 10 вопросов и 10 ответов, которые быстро приведут вас к интерфейсному алгоритму
- Расширенный алгоритм внешнего интерфейса 5: всесторонне интерпретировать структуру стека, используемую во внешнем интерфейсе (+ вопросы по чистке кода)
- Усовершенствованный алгоритм внешнего интерфейса 4: Связанный список такой простой (+ вопросы по кисту leetcode)
- Усовершенствованный алгоритм внешнего интерфейса 3: изучите алгоритм LRU из стратегии удаления кеша браузера и проверки активности Vue.
- Резюме первой недели лагеря передовых алгоритмов переднего плана бутылки
- Передовой расширенный алгоритм 2: просмотр массива JavaScript из исходного кода Chrome V8 (с вопросами интервью Tencent)
- Интерфейсный расширенный алгоритм 1: как проанализировать и подсчитать эффективность выполнения и потребление ресурсов алгоритма?
Спасибо за прочтение
Добро пожаловать, чтобы обратить внимание на «Front-end Bottle Master», ответьте на «Exchange», чтобы присоединиться к группе внешнего обмена!
Добро пожаловать, чтобы обратить внимание на «Front-end Bottle Master», ответьте «Алгоритм», чтобы автоматически присоединиться и построить полную структуру данных и систему алгоритмов от 0 до 1!
Здесь мистер Боттл не только представляет алгоритм, но и объединяет алгоритм с различными полями интерфейса, включая браузеры, HTTP, V8, React, исходный код Vue и т. д.
Здесь (группа алгоритмов) вы можете изучать большой вопрос по программированию заводского алгоритма (Ali, Tencent, Baidu, Byte и т. д.) или литкод каждый день, а мистер Бутылка ответит на него на следующий день!
Кроме того, каждую неделю есть рукописные вопросы по исходному коду, и мистер Боттл тоже на них ответит!