1. введение
В этой статье мы представим различные решения для нахождения k – го наибольшего элемента в последовательности уникальных чисел. В наших примерах мы будем использовать массив целых чисел.
Мы также поговорим о средней и наихудшей временной сложности каждого алгоритма.
2. Решения
Теперь давайте рассмотрим несколько возможных решений — одно с использованием простой сортировки и два с использованием алгоритма быстрого выбора, полученного из быстрой сортировки.
2.1. Сортировка
Когда мы думаем о проблеме, возможно, самое очевидное решение, которое приходит на ум, – это | сортировка массива .
Давайте определим необходимые шаги:
- Сортировка массива в порядке возрастания
- Поскольку последний элемент массива будет самым большим элементом, то k – й самый большой элемент будет находиться в индексе , где x(массив) – k
Как мы видим, решение простое, но требует сортировки всего массива. Следовательно, временная сложность будет O(n*logn) :
public int findKthLargestBySorting(Integer[] arr, int k) { Arrays.sort(arr); int targetIndex = arr.length - k; return arr[targetIndex]; }
Альтернативный подход состоит в том, чтобы отсортировать массив в порядке убывания и просто вернуть элемент по индексу (k-1) th:
public int findKthLargestBySortingDesc(Integer[] arr, int k) { Arrays.sort(arr, Collections.reverseOrder()); return arr[k-1]; }
2.2. Быстрый выбор
Это можно считать оптимизацией предыдущего подхода. В этом случае мы выбираем QuickSort для сортировки. Анализируя постановку задачи, мы понимаем, что на самом деле нам не нужно сортировать весь массив — нам нужно только переставить его содержимое так, чтобы k – й элемент массива был k – м самым большим или самым маленьким.
В QuickSort мы выбираем элемент поворота и перемещаем его в правильное положение. Мы также разделяем массив вокруг него. В Quick Select идея состоит в том, чтобы остановиться в точке, где сам стержень является k – м самым большим элементом.
Мы можем оптимизировать алгоритм дальше, если не будем повторяться как для левой, так и для правой сторон разворота. Нам нужно только повторить для одного из них в соответствии с положением оси вращения.
Давайте рассмотрим основные идеи алгоритма быстрого выбора:
- Выберите сводный элемент и разделите массив соответствующим образом
- Выберите самый правый элемент в качестве точки поворота
- Перетасуйте массив таким образом, чтобы элемент pivot был помещен на свое законное место — все элементы, меньшие, чем pivot, будут иметь более низкие индексы, а элементы, большие, чем pivot, будут помещены в более высокие индексы, чем pivot
- Если pivot помещен в k – й элемент массива, завершите процесс, так как pivot является k – м самым большим элементом
- Если положение поворота больше k, затем продолжите процесс с левым подмассивом, в противном случае повторите процесс с правым подмассивом
Мы можем написать общую логику, которая также может быть использована для поиска k – го наименьшего элемента. Мы определим метод findKthElement с помощью QuickSelect () , который вернет k – й элемент в отсортированном массиве.
Если мы отсортируем массив в порядке возрастания, то k – й элемент массива будет k – м наименьшим элементом. Чтобы найти k – й самый большой элемент, мы можем передать k= длина(массив) – k.
Давайте реализуем это решение:
public int findKthElementByQuickSelect(Integer[] arr, int left, int right, int k) { if (k >= 0 && k <= right - left + 1) { int pos = partition(arr, left, right); if (pos - left == k) { return arr[pos]; } if (pos - left > k) { return findKthElementByQuickSelect(arr, left, pos - 1, k); } return findKthElementByQuickSelect(arr, pos + 1, right, k - pos + left - 1); } return 0; }
Теперь давайте реализуем метод partition , который выбирает самый правый элемент в качестве опорного, помещает его в соответствующий индекс и разбивает массив таким образом, чтобы элементы с более низкими индексами были меньше, чем элемент pivot.
Аналогично, элементы с более высокими индексами будут больше, чем элемент pivot:
public int partition(Integer[] arr, int left, int right) { int pivot = arr[right]; Integer[] leftArr; Integer[] rightArr; leftArr = IntStream.range(left, right) .filter(i -> arr[i] < pivot) .map(i -> arr[i]) .boxed() .toArray(Integer[]::new); rightArr = IntStream.range(left, right) .filter(i -> arr[i] > pivot) .map(i -> arr[i]) .boxed() .toArray(Integer[]::new); int leftArraySize = leftArr.length; System.arraycopy(leftArr, 0, arr, left, leftArraySize); arr[leftArraySize+left] = pivot; System.arraycopy(rightArr, 0, arr, left + leftArraySize + 1, rightArr.length); return left + leftArraySize; }
Существует более простой, итеративный подход для достижения разделения:
public int partitionIterative(Integer[] arr, int left, int right) { int pivot = arr[right], i = left; for (int j = left; j <= right - 1; j++) { if (arr[j] <= pivot) { swap(arr, i, j); i++; } } swap(arr, i, right); return i; } public void swap(Integer[] arr, int n1, int n2) { int temp = arr[n2]; arr[n2] = arr[n1]; arr[n1] = temp; }
Это решение работает в среднем за O(n) время. Однако в худшем случае временная сложность будет O(n^2) .
2.3. Быстрый Выбор С Рандомизированным Разделением
Этот подход является небольшой модификацией предыдущего подхода. Если массив почти/полностью отсортирован и если мы выберем самый правый элемент в качестве оси, разделение левого и правого подмассивов будет очень неравномерным.
Этот метод предполагает выбор начального элемента поворота случайным образом. Однако нам не нужно менять логику разделения.
Вместо вызова partition мы вызываем метод random Partition , который выбирает случайный элемент и меняет его местами с самым правым элементом, прежде чем, наконец, вызвать метод partition .
Давайте реализуем метод random Partition :
public int randomPartition(Integer arr[], int left, int right) { int n = right - left + 1; int pivot = (int) (Math.random()) * n; swap(arr, left + pivot, right); return partition(arr, left, right); }
В большинстве случаев это решение работает лучше, чем в предыдущем случае.
Ожидаемая временная сложность рандомизированного быстрого выбора составляет O(n) .
Однако наихудшая временная сложность все еще остается O(n^2) .
3. Заключение
В этой статье мы обсудили различные решения для поиска k – го самого большого (или самого маленького) элемента в массиве уникальных чисел. Самое простое решение-отсортировать массив и вернуть элемент k th. Это решение имеет временную сложность O(n*logn) .
Мы также обсудили два варианта быстрого выбора. Этот алгоритм не является простым, но он имеет временную сложность O(n) в средних случаях.
Как всегда, полный код алгоритма можно найти на GitHub .