Как найти Kth самый большой элемент в Java

Как найти Kth самый большой элемент в Java

1. Вступление

В этой статье мы представим различные решения для поискаk-го наибольшего элемента в последовательности уникальных чисел. В наших примерах мы будем использовать массив целых чисел.

Мы также поговорим о средней и наихудшей временной сложности каждого алгоритма.

2. Решения

Теперь давайте рассмотрим несколько возможных решений: одно с использованием простой сортировки, а два с использованием алгоритма быстрого выбора, полученного из быстрой сортировки.

2.1. Сортировка

Когда мы думаем о проблеме, возможно,the most obvious solution that comes to mind isto sort the array.

Определим необходимые шаги:

  • Сортировать массив в порядке возрастания

  • Поскольку последний элемент массива будет самым большим элементом, самый большой элементkth будет иметь индексxth, гдеx = length(array) – 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. QuickSelect

Это можно считать оптимизацией предыдущего подхода. Здесь мы выбираемQuickSort для сортировки. Анализируя постановку задачи, мы понимаем, чтоwe don’t actually need to sort the entire array — we only need to rearrange its contents so that the kth element of the array is the kth largest or smallest.

В QuickSort мы выбираем элемент поворота и перемещаем его в правильное положение. Мы также разбиваем массив вокруг него. In QuickSelect, the idea is to stop at the point where the pivot itself is the kth largest element.с

Мы можем оптимизировать алгоритм дальше, если не будем повторяться как для левой, так и для правой стороны поворота. Нам нужно повторить только для одного из них в соответствии с положением оси.

Давайте посмотрим на основные идеи алгоритма QuickSelect:

  • Выберите элемент pivot и разбейте массив соответственно

    • Выберите самый правый элемент в качестве оси

    • Переставьте массив таким образом, чтобы элемент сводки располагался на своем законном месте - все элементы, меньшие, чем сводка, имели бы более низкие индексы, а элементы, превышающие сводку, были бы размещены с более высокими индексами, чем сводка.

  • Если точка поворота размещена в элементеkth в массиве, выйдите из процесса, поскольку точка поворота является самым большим элементомkth.

  • Если положение поворота большеk,, тогда продолжите процесс с левым подмассивом, в противном случае повторите процесс с правым подмассивом

Мы можем написать общую логику, которую также можно использовать для поискаkth наименьшего элемента. Мы определим методfindKthElementByQuickSelect(), который будет возвращать элементkth в отсортированном массиве.

Если мы отсортируем массив в порядке возрастания, элементkth массива будет наименьшим элементомkth. Чтобы найтиkth наибольший элемент, мы можем передатьk= length(Array) – 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:

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. QuickSelect со случайным разделением

Этот подход является небольшой модификацией предыдущего подхода. Если массив почти / полностью отсортирован, и если мы выберем самый правый элемент в качестве основного, раздел левого и правого подмассивов будет сильно неравномерным.

Этот метод предлагаетpicking the initial pivot element in a random manner. Тем не менее, нам не нужно изменять логику разделения.

Вместо вызоваpartition мы вызываем методrandomPartition, который выбирает случайный элемент и меняет его местами с крайним правым элементом, прежде чем, наконец, вызвать методpartition.

Давайте реализуем методrandomPartition:

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);
}

Это решение работает лучше, чем в предыдущем случае в большинстве случаев.

Ожидаемая временная сложность рандомизированного QuickSelect составляетO(n).

Однако наихудшая временная сложность по-прежнему остаетсяO(n^2).

3. Заключение

В этой статье мы обсудили различные решения для поискаkth наибольшего (или наименьшего) элемента в массиве уникальных чисел. Самое простое решение - отсортировать массив и вернуть элементkth. Это решение имеет временную сложностьO(n*logn).

Мы также обсудили два варианта быстрого выбора. Этот алгоритм непростой, но в среднем он имеет временную сложностьO(n).

Как всегда, полный код алгоритма можно найтиover on GitHub.