Как найти K-й по величине элемент в несортированном массиве длины n в O (n)?

Я считаю, что есть способ найти K-й самый большой элемент в несортированном массиве длины n в O(n). Или, возможно, это" ожидаемый " O(n) или что-то еще. Как мы можем это сделать?

30 ответов


Это называется найти статистика k-го порядка. Существует очень простой рандомизированный алгоритм (называемый quickselect) С O(n) среднее время, O(n^2) наихудшее время и довольно сложный неслучайный алгоритм (называемый introselect) С O(n) худший случай. Есть некоторая информация о Википедия, но это не очень хорошо.

все, что вам нужно в эти powerpoint слайды. Просто, чтобы извлечь основной алгоритм O(n) алгоритм наихудшего случая (introselect):

Select(A,n,i):
    Divide input into ⌈n/5⌉ groups of size 5.

    /* Partition on median-of-medians */
    medians = array of each group’s median.
    pivot = Select(medians, ⌈n/5⌉, ⌈n/10⌉)
    Left Array L and Right Array G = partition(A, pivot)

    /* Find ith element in L, pivot, or G */
    k = |L| + 1
    If i = k, return pivot
    If i < k, return Select(L, k-1, i)
    If i > k, return Select(G, n-k, i-k)

Это также очень хорошо подробно описано во введении к книге алгоритмов Кормена и др.


если вы хотите true , а не O(kn) или что-то в этом роде, тогда вы должны использовать quickselect (это в основном quicksort, где вы выбрасываете раздел, который вас не интересует). Мой проф имеет отличную запись, с анализом времени выполнения: (ссылка)

алгоритм QuickSelect быстро находит k-й наименьший элемент несортированного массива n элементы. Это RandomizedAlgorithm, так что мы вычисляем в худшем случае!--31-->ожидается бег времени.

вот алгоритм.

QuickSelect(A, k)
  let r be chosen uniformly at random in the range 1 to length(A)
  let pivot = A[r]
  let A1, A2 be new arrays
  # split into a pile A1 of small elements and A2 of big elements
  for i = 1 to n
    if A[i] < pivot then
      append A[i] to A1
    else if A[i] > pivot then
      append A[i] to A2
    else
      # do nothing
  end for
  if k <= length(A1):
    # it's in the pile of small elements
    return QuickSelect(A1, k)
  else if k > length(A) - length(A2)
    # it's in the pile of big elements
    return QuickSelect(A2, k - (length(A) - length(A2))
  else
    # it's equal to the pivot
    return pivot

что время работы этого алгоритма? Если противник переворачивает монеты для нас, мы можем обнаружить, что ось всегда является самым большим элементом и k всегда 1, давая время

T(n) = Theta(n) + T(n-1) = Theta(n2)

но если выбор действительно случайный, ожидаемое время определяется

T(n) <= Theta(n) + (1/n) ∑i=1 to nT(max(i, n-i-1))

где мы делаем не совсем разумное предположение, что рекурсия всегда попадает в большую из A1 или A2.

предположим, что T(n) <= an для некоторых a. Тогда мы получаем

T(n) 
 <= cn + (1/n) ∑i=1 to nT(max(i-1, n-i))
 = cn + (1/n) ∑i=1 to floor(n/2) T(n-i) + (1/n) ∑i=floor(n/2)+1 to n T(i)
 <= cn + 2 (1/n) ∑i=floor(n/2) to n T(i)
 <= cn + 2 (1/n) ∑i=floor(n/2) to n ai

и теперь каким-то образом мы должны получить ужасную сумму справа от знака плюс, чтобы поглотить cn слева. Если мы просто свяжем его как 2(1/n) ∑i=n/2 to n an, мы получим примерно 2(1/n)(n/2)an = an. Но это слишком большой - нет места, чтобы поставить еще один cn. Итак, давайте расширим сумму, используя формула арифметического ряда:

i=floor(n/2) to n i  
 = ∑i=1 to n i - ∑i=1 to floor(n/2) i  
 = n(n+1)/2 - floor(n/2)(floor(n/2)+1)/2  
 <= n2/2 - (n/4)2/2  
 = (15/32)n2

где мы пользуемся тем, что n "достаточно большой", чтобы заменить уродливый floor(n/2) факторов с гораздо чище (и меньше) n/4. Теперь мы можем продолжить с

cn + 2 (1/n) ∑i=floor(n/2) to n ai,
 <= cn + (2a/n) (15/32) n2
 = n (c + (15/16)a)
 <= an

предоставил a > 16c.

это дает T(n) = O(n). Это явно Omega(n), так что мы получить T(n) = Theta(n).


быстрый Google на этом ("KTH самый большой массив элементов") вернул это:http://discuss.joelonsoftware.com/default.asp?interview.11.509587.17

"Make one pass through tracking the three largest values so far." 

(это было специально для 3d крупнейших)

и этот ответ:

Build a heap/priority queue.  O(n)
Pop top element.  O(log n)
Pop top element.  O(log n)
Pop top element.  O(log n)

Total = O(n) + 3 O(log n) = O(n)

вам нравится quicksort. Выберите элемент наугад и засуньте все выше или ниже. На этом этапе вы узнаете, какой элемент вы действительно выбрали, и если это K-й элемент, который вы сделали, иначе вы повторите с bin (выше или ниже), что K-й элемент упадет. Статистически говоря, время, необходимое для поиска k-го элемента растет с n, о(n).


компаньон программиста для анализа алгоритмов дает версию, которая is O (n), хотя автор утверждает, что постоянный фактор настолько высок, вы, вероятно, предпочтете наивный метод сортировки списка, а затем выбора.

Я ответил на письмо Вашего вопроса :)


стандартная библиотека C++ имеет почти точно, что функции вызов nth_element, хотя он изменяет свои данные. Он ожидал линейного времени выполнения, O (N), а также выполняет частичную сортировку.

const int N = ...;
double a[N];
// ... 
const int m = ...; // m < N
nth_element (a, a + m, a + N);
// a[m] contains the mth element in a

хотя и не очень уверен в сложности O(n), но он обязательно будет между O(n) и nLog(n). Также обязательно будет ближе к O(n), чем nLog (n). Функция написана на Java

public int quickSelect(ArrayList<Integer>list, int nthSmallest){
    //Choose random number in range of 0 to array length
    Random random =  new Random();
    //This will give random number which is not greater than length - 1
    int pivotIndex = random.nextInt(list.size() - 1); 

    int pivot = list.get(pivotIndex);

    ArrayList<Integer> smallerNumberList = new ArrayList<Integer>();
    ArrayList<Integer> greaterNumberList = new ArrayList<Integer>();

    //Split list into two. 
    //Value smaller than pivot should go to smallerNumberList
    //Value greater than pivot should go to greaterNumberList
    //Do nothing for value which is equal to pivot
    for(int i=0; i<list.size(); i++){
        if(list.get(i)<pivot){
            smallerNumberList.add(list.get(i));
        }
        else if(list.get(i)>pivot){
            greaterNumberList.add(list.get(i));
        }
        else{
            //Do nothing
        }
    }

    //If smallerNumberList size is greater than nthSmallest value, nthSmallest number must be in this list 
    if(nthSmallest < smallerNumberList.size()){
        return quickSelect(smallerNumberList, nthSmallest);
    }
    //If nthSmallest is greater than [ list.size() - greaterNumberList.size() ], nthSmallest number must be in this list
    //The step is bit tricky. If confusing, please see the above loop once again for clarification.
    else if(nthSmallest > (list.size() - greaterNumberList.size())){
        //nthSmallest will have to be changed here. [ list.size() - greaterNumberList.size() ] elements are already in 
        //smallerNumberList
        nthSmallest = nthSmallest - (list.size() - greaterNumberList.size());
        return quickSelect(greaterNumberList,nthSmallest);
    }
    else{
        return pivot;
    }
}

я реализовал поиск КТН minimimum в n элементов неразобравшись с помощью динамического программирования, в частности метод турнире. Время выполнения-O(n + klog (n)). Используемый механизм указан как один из методов на странице Википедии об алгоритме выбора (как указано в одном из сообщений выше). Вы можете прочитать об алгоритме, а также найти код (java) на моей странице блога Поиск K-Го Минимума. Кроме того, логика может сделать частичное упорядочение списка-вернуть первый K min (или max) в O(klog (n)) времени.

хотя код предоставил результат KTH minimum, аналогичная логика может быть использована для поиска KTH maximum в O(klog (n)), игнорируя предварительную работу по созданию турнирного дерева.


вы можете сделать это в O(n + kn) = O (n) (для константы k) для времени и O (k) для пространства, отслеживая k самых больших элементов, которые вы видели.

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

решение кучи приоритетов Уоррена более аккуратное.


найдите медиану массива в линейном времени, затем используйте процедуру разбиения точно так же, как в quicksort, чтобы разделить массив на две части, значения слева от медианы меньше ( ) медианы, что тоже можно сделать в линейном времени, Теперь перейдите к той части массива, где находится элемент kth, Сейчас рецидив будет: T (n) = T (n/2) + cn что дает мне o (n) overal.


сексуальный quickselect в Python

def quickselect(arr, k):
    '''
     k = 1 returns first element in ascending order.
     can be easily modified to return first element in descending order
    '''

    r = random.randrange(0, len(arr))

    a1 = [i for i in arr if i < arr[r]] '''partition'''
    a2 = [i for i in arr if i > arr[r]]

    if k <= len(a1):
        return quickselect(a1, k)
    elif k > len(arr)-len(a2):
        return quickselect(a2, k - (len(arr) - len(a2)))
    else:
        return arr[r]

Ниже приведена ссылка на полную реализацию с довольно обширным объяснением того, как работает алгоритм поиска KTH-элемента в несортированном алгоритме. Основная идея состоит в том, чтобы разбить массив, как в QuickSort. Но чтобы избежать крайних случаев(например, когда наименьший элемент выбирается в качестве оси на каждом шаге, так что алгоритм вырождается в O (n^2) Время выполнения), применяется специальный выбор оси, называемый алгоритмом медианы медианы. Все решение работает в o (n) время в худшем и в средний случай.

вот ссылка на полную статью (речь идет о поиске кй маленький элемент, но принцип тот же для нахождения Kth большой):

Поиск k-го наименьшего элемента в Несортированном массиве


в этой статье Поиск k-го по величине элемента в списке из n элементов следующий алгоритм будет забрать O(n) времени в худшем случае.

  1. разделите массив на n / 5 списков по 5 элементов каждый.
  2. найдите медиану в каждом суб-массиве из 5 элементов.
  3. рекурсивно найдите медиану всех медиан, назовем ее M
  4. разбить массив на два суб-массива 1-й суб-массив содержит элементы больше M , предположим, что этот суб-массив a1, в то время как другой суб-массив содержит элементы меньшие, чем M., назовем этот суб-массив a2.
  5. если k
  6. если k-1 = / a1/, верните M.
  7. если k> |a1 / + 1, возврат выбора (a2, k-a1 - 1).

анализ: как предложено в оригинальной статье:

мы используем медиану для разделения списка на две половины(первая половина, если k <= n/2 , а вторая половина иначе). Этот алгоритм принимает время cn на первом уровне рекурсии для некоторой постоянной c, cn/2 at следующий уровень (так как мы рекурсии в список размером N/2), cn/4 в третий уровень, и так далее. Общее время занято cn + cn/2 + cn/4 + .... = 2cn = o(n).

почему размер раздела берется 5, а не 3?

как уже упоминалось в оригинале статьи:

деление списка на 5 уверяет в худшем случае раскол 70 на 30. По крайней мере половина медианы превышает медиану из медиан, следовательно хотяб половина блоков n / 5 имеет по крайней мере 3 элемента, и это дает 3n/10 split, что означает, что другой раздел-7n / 10 в худшем случае. Что дает T(n) = T(n/5)+T(7n/10)+O(n). Since n/5+7n/10 < 1, в в худшем случае время работыO(n).

теперь я попытался реализовать данный алгоритм в виде:

public static int findKthLargestUsingMedian(Integer[] array, int k) {
        // Step 1: Divide the list into n/5 lists of 5 element each.
        int noOfRequiredLists = (int) Math.ceil(array.length / 5.0);
        // Step 2: Find pivotal element aka median of medians.
        int medianOfMedian =  findMedianOfMedians(array, noOfRequiredLists);
        //Now we need two lists split using medianOfMedian as pivot. All elements in list listOne will be grater than medianOfMedian and listTwo will have elements lesser than medianOfMedian.
        List<Integer> listWithGreaterNumbers = new ArrayList<>(); // elements greater than medianOfMedian
        List<Integer> listWithSmallerNumbers = new ArrayList<>(); // elements less than medianOfMedian
        for (Integer element : array) {
            if (element < medianOfMedian) {
                listWithSmallerNumbers.add(element);
            } else if (element > medianOfMedian) {
                listWithGreaterNumbers.add(element);
            }
        }
        // Next step.
        if (k <= listWithGreaterNumbers.size()) return findKthLargestUsingMedian((Integer[]) listWithGreaterNumbers.toArray(new Integer[listWithGreaterNumbers.size()]), k);
        else if ((k - 1) == listWithGreaterNumbers.size()) return medianOfMedian;
        else if (k > (listWithGreaterNumbers.size() + 1)) return findKthLargestUsingMedian((Integer[]) listWithSmallerNumbers.toArray(new Integer[listWithSmallerNumbers.size()]), k-listWithGreaterNumbers.size()-1);
        return -1;
    }

    public static int findMedianOfMedians(Integer[] mainList, int noOfRequiredLists) {
        int[] medians = new int[noOfRequiredLists];
        for (int count = 0; count < noOfRequiredLists; count++) {
            int startOfPartialArray = 5 * count;
            int endOfPartialArray = startOfPartialArray + 5;
            Integer[] partialArray = Arrays.copyOfRange((Integer[]) mainList, startOfPartialArray, endOfPartialArray);
            // Step 2: Find median of each of these sublists.
            int medianIndex = partialArray.length/2;
            medians[count] = partialArray[medianIndex];
        }
        // Step 3: Find median of the medians.
        return medians[medians.length / 2];
    }

просто для завершения, другой алгоритм использует приоритет Очередь и занимает время O(nlogn).

public static int findKthLargestUsingPriorityQueue(Integer[] nums, int k) {
        int p = 0;
        int numElements = nums.length;
        // create priority queue where all the elements of nums will be stored
        PriorityQueue<Integer> pq = new PriorityQueue<Integer>();

        // place all the elements of the array to this priority queue
        for (int n : nums) {
            pq.add(n);
        }

        // extract the kth largest element
        while (numElements - k + 1 > 0) {
            p = pq.poll();
            k++;
        }

        return p;
    }

оба этих алгоритма могут быть протестированы как:

public static void main(String[] args) throws IOException {
        Integer[] numbers = new Integer[]{2, 3, 5, 4, 1, 12, 11, 13, 16, 7, 8, 6, 10, 9, 17, 15, 19, 20, 18, 23, 21, 22, 25, 24, 14};
        System.out.println(findKthLargestUsingMedian(numbers, 8));
        System.out.println(findKthLargestUsingPriorityQueue(numbers, 8));
    }

как ожидается, выход: 18 18


повторите список. если текущее значение больше, чем сохраненное наибольшее значение, сохраните его как наибольшее значение и увеличьте 1-4 вниз и 5 выпадет из списка. Если нет,сравните его с номером 2 и сделайте то же самое. Повторите, сверив его со всеми 5 сохраненными значениями. это должно сделать это в O (n)


Я хотел бы предложить один вариант ответа

Если мы возьмем первые k элементов и отсортировать их в связанный список значений K

теперь для всех других значений даже в худшем случае, если мы делаем сортировка вставкой для остальных N-K значений даже в худшем случае количество сравнений будет K*(Н-К) и для пред значений K сортируются пусть будет к*(к-1) так получается (НК-К), О(П)

ура


объяснение алгоритма медианы медиан для поиска k-го наибольшего целого числа из n можно найти здесь: http://cs.indstate.edu/~spitla/презентация.формат PDF

реализация в C++ ниже:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int findMedian(vector<int> vec){
//    Find median of a vector
    int median;
    size_t size = vec.size();
    median = vec[(size/2)];
    return median;
}

int findMedianOfMedians(vector<vector<int> > values){
    vector<int> medians;

    for (int i = 0; i < values.size(); i++) {
        int m = findMedian(values[i]);
        medians.push_back(m);
    }

    return findMedian(medians);
}

void selectionByMedianOfMedians(const vector<int> values, int k){
//    Divide the list into n/5 lists of 5 elements each
    vector<vector<int> > vec2D;

    int count = 0;
    while (count != values.size()) {
        int countRow = 0;
        vector<int> row;

        while ((countRow < 5) && (count < values.size())) {
            row.push_back(values[count]);
            count++;
            countRow++;
        }
        vec2D.push_back(row);
    }

    cout<<endl<<endl<<"Printing 2D vector : "<<endl;
    for (int i = 0; i < vec2D.size(); i++) {
        for (int j = 0; j < vec2D[i].size(); j++) {
            cout<<vec2D[i][j]<<" ";
        }
        cout<<endl;
    }
    cout<<endl;

//    Calculating a new pivot for making splits
    int m = findMedianOfMedians(vec2D);
    cout<<"Median of medians is : "<<m<<endl;

//    Partition the list into unique elements larger than 'm' (call this sublist L1) and
//    those smaller them 'm' (call this sublist L2)
    vector<int> L1, L2;

    for (int i = 0; i < vec2D.size(); i++) {
        for (int j = 0; j < vec2D[i].size(); j++) {
            if (vec2D[i][j] > m) {
                L1.push_back(vec2D[i][j]);
            }else if (vec2D[i][j] < m){
                L2.push_back(vec2D[i][j]);
            }
        }
    }

//    Checking the splits as per the new pivot 'm'
    cout<<endl<<"Printing L1 : "<<endl;
    for (int i = 0; i < L1.size(); i++) {
        cout<<L1[i]<<" ";
    }

    cout<<endl<<endl<<"Printing L2 : "<<endl;
    for (int i = 0; i < L2.size(); i++) {
        cout<<L2[i]<<" ";
    }

//    Recursive calls
    if ((k - 1) == L1.size()) {
        cout<<endl<<endl<<"Answer :"<<m;
    }else if (k <= L1.size()) {
        return selectionByMedianOfMedians(L1, k);
    }else if (k > (L1.size() + 1)){
        return selectionByMedianOfMedians(L2, k-((int)L1.size())-1);
    }

}

int main()
{
    int values[] = {2, 3, 5, 4, 1, 12, 11, 13, 16, 7, 8, 6, 10, 9, 17, 15, 19, 20, 18, 23, 21, 22, 25, 24, 14};

    vector<int> vec(values, values + 25);

    cout<<"The given array is : "<<endl;
    for (int i = 0; i < vec.size(); i++) {
        cout<<vec[i]<<" ";
    }

    selectionByMedianOfMedians(vec, 8);

    return 0;
}

появилась алгоритм выбора Вирта, который имеет более простую реализацию, чем QuickSelect. Алгоритм выбора Вирта медленнее, чем QuickSelect, но с некоторыми улучшениями он становится быстрее.

подробнее. Используя оптимизацию MODIFIND Владимира Забродского и выбор оси медианы-3 и уделяя некоторое внимание заключительным шагам части разбиения алгоритма, я придумал следующий алгоритм (образно названный "LefSelect"):

#define F_SWAP(a,b) { float temp=(a);(a)=(b);(b)=temp; }

# Note: The code needs more than 2 elements to work
float lefselect(float a[], const int n, const int k) {
    int l=0, m = n-1, i=l, j=m;
    float x;

    while (l<m) {
        if( a[k] < a[i] ) F_SWAP(a[i],a[k]);
        if( a[j] < a[i] ) F_SWAP(a[i],a[j]);
        if( a[j] < a[k] ) F_SWAP(a[k],a[j]);

        x=a[k];
        while (j>k & i<k) {
            do i++; while (a[i]<x);
            do j--; while (a[j]>x);

            F_SWAP(a[i],a[j]);
        }
        i++; j--;

        if (j<k) {
            while (a[i]<x) i++;
            l=i; j=m;
        }
        if (k<i) {
            while (x<a[j]) j--;
            m=j; i=l;
        }
    }
    return a[k];
}

в тестах, которые я сделал здесь, LefSelect на 20-30% быстрее, чем QuickSelect.


Решение Haskell:

kthElem index list = sort list !! index

withShape ~[]     []     = []
withShape ~(x:xs) (y:ys) = x : withShape xs ys

sort []     = []
sort (x:xs) = (sort ls `withShape` ls) ++ [x] ++ (sort rs `withShape` rs)
  where
   ls = filter (<  x)
   rs = filter (>= x)

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


вот реализация C++ рандомизированного QuickSelect. Идея состоит в том, чтобы случайным образом выбрать элемент pivot. Чтобы реализовать рандомизированный раздел, мы используем случайную функцию rand () для генерации индекса между l и r, замены элемента на случайно сгенерированный индекс с последним элементом и, наконец, вызываем стандартный процесс раздела, который использует последний элемент в качестве pivot.

#include<iostream>
#include<climits>
#include<cstdlib>
using namespace std;

int randomPartition(int arr[], int l, int r);

// This function returns k'th smallest element in arr[l..r] using
// QuickSort based method.  ASSUMPTION: ALL ELEMENTS IN ARR[] ARE DISTINCT
int kthSmallest(int arr[], int l, int r, int k)
{
    // If k is smaller than number of elements in array
    if (k > 0 && k <= r - l + 1)
    {
        // Partition the array around a random element and
        // get position of pivot element in sorted array
        int pos = randomPartition(arr, l, r);

        // If position is same as k
        if (pos-l == k-1)
            return arr[pos];
        if (pos-l > k-1)  // If position is more, recur for left subarray
            return kthSmallest(arr, l, pos-1, k);

        // Else recur for right subarray
        return kthSmallest(arr, pos+1, r, k-pos+l-1);
    }

    // If k is more than number of elements in array
    return INT_MAX;
}

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

// Standard partition process of QuickSort().  It considers the last
// element as pivot and moves all smaller element to left of it and
// greater elements to right. This function is used by randomPartition()
int partition(int arr[], int l, int r)
{
    int x = arr[r], i = l;
    for (int j = l; j <= r - 1; j++)
    {
        if (arr[j] <= x) //arr[i] is bigger than arr[j] so swap them
        {
            swap(&arr[i], &arr[j]);
            i++;
        }
    }
    swap(&arr[i], &arr[r]); // swap the pivot
    return i;
}

// Picks a random pivot element between l and r and partitions
// arr[l..r] around the randomly picked element using partition()
int randomPartition(int arr[], int l, int r)
{
    int n = r-l+1;
    int pivot = rand() % n;
    swap(&arr[l + pivot], &arr[r]);
    return partition(arr, l, r);
}

// Driver program to test above methods
int main()
{
    int arr[] = {12, 3, 5, 7, 4, 19, 26};
    int n = sizeof(arr)/sizeof(arr[0]), k = 3;
    cout << "K'th smallest element is " << kthSmallest(arr, 0, n-1, k);
    return 0;
}

наихудшая временная сложность вышеуказанного решения по-прежнему O(n2).В худшем случае рандомизированные функция всегда может выбрать угловой элемент. Ожидаемая временная сложность вышеуказанного рандомизированного QuickSelect равна Θ (n)


как насчет такого подхода

сохранить buffer of length k и tmp_max, получение tmp_max равно O (k) и выполняется n раз, поэтому что-то вроде O(kn)

enter image description here

это правильно или я что-то пропустила ?

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


  1. создать очередь приоритетов.
  2. вставить все элементы в кучу.
  3. вызов poll () k раз.

    public static int getKthLargestElements(int[] arr)
    {
        PriorityQueue<Integer> pq =  new PriorityQueue<>((x , y) -> (y-x));
        //insert all the elements into heap
        for(int ele : arr)
           pq.offer(ele);
        // call poll() k times
        int i=0;
        while(i&lt;k)
         {
           int result = pq.poll();
         } 
       return result;        
    }
    

это реализация в Javascript.

если вы отпустите ограничение, которое вы не можете изменить массив, вы можете предотвратить использование дополнительной памяти с помощью двух индексов для идентификации " текущего раздела "(В классическом стиле quicksort - http://www.nczonline.net/blog/2012/11/27/computer-science-in-javascript-quicksort/).

function kthMax(a, k){
    var size = a.length;

    var pivot = a[ parseInt(Math.random()*size) ]; //Another choice could have been (size / 2) 

    //Create an array with all element lower than the pivot and an array with all element higher than the pivot
    var i, lowerArray = [], upperArray = [];
    for (i = 0; i  < size; i++){
        var current = a[i];

        if (current < pivot) {
            lowerArray.push(current);
        } else if (current > pivot) {
            upperArray.push(current);
        }
    }

    //Which one should I continue with?
    if(k <= upperArray.length) {
        //Upper
        return kthMax(upperArray, k);
    } else {
        var newK = k - (size - lowerArray.length);

        if (newK > 0) {
            ///Lower
            return kthMax(lowerArray, newK);
        } else {
            //None ... it's the current pivot!
            return pivot;
        }   
    }
}  

если вы хотите проверить, как он работает, вы можете использовать этот вариант:

    function kthMax (a, k, logging) {
         var comparisonCount = 0; //Number of comparison that the algorithm uses
         var memoryCount = 0;     //Number of integers in memory that the algorithm uses
         var _log = logging;

         if(k < 0 || k >= a.length) {
            if (_log) console.log ("k is out of range"); 
            return false;
         }      

         function _kthmax(a, k){
             var size = a.length;
             var pivot = a[parseInt(Math.random()*size)];
             if(_log) console.log("Inputs:", a,  "size="+size, "k="+k, "pivot="+pivot);

             // This should never happen. Just a nice check in this exercise
             // if you are playing with the code to avoid never ending recursion            
             if(typeof pivot === "undefined") {
                 if (_log) console.log ("Ops..."); 
                 return false;
             }

             var i, lowerArray = [], upperArray = [];
             for (i = 0; i  < size; i++){
                 var current = a[i];
                 if (current < pivot) {
                     comparisonCount += 1;
                     memoryCount++;
                     lowerArray.push(current);
                 } else if (current > pivot) {
                     comparisonCount += 2;
                     memoryCount++;
                     upperArray.push(current);
                 }
             }
             if(_log) console.log("Pivoting:",lowerArray, "*"+pivot+"*", upperArray);

             if(k <= upperArray.length) {
                 comparisonCount += 1;
                 return _kthmax(upperArray, k);
             } else if (k > size - lowerArray.length) {
                 comparisonCount += 2;
                 return _kthmax(lowerArray, k - (size - lowerArray.length));
             } else {
                 comparisonCount += 2;
                 return pivot;
             }
     /* 
      * BTW, this is the logic for kthMin if we want to implement that... ;-)
      * 

             if(k <= lowerArray.length) {
                 return kthMin(lowerArray, k);
             } else if (k > size - upperArray.length) {
                 return kthMin(upperArray, k - (size - upperArray.length));
             } else 
                 return pivot;
     */            
         }

         var result = _kthmax(a, k);
         return {result: result, iterations: comparisonCount, memory: memoryCount};
     }

остальной код просто чтобы создать некоторую игровую площадку:

    function getRandomArray (n){
        var ar = [];
        for (var i = 0, l = n; i < l; i++) {
            ar.push(Math.round(Math.random() * l))
        }

        return ar;
    }

    //Create a random array of 50 numbers
    var ar = getRandomArray (50);   

Теперь запустите тесты несколько раз. Из-за математики.случайный() он будет производить каждый раз разные результаты:

    kthMax(ar, 2, true);
    kthMax(ar, 2);
    kthMax(ar, 2);
    kthMax(ar, 2);
    kthMax(ar, 2);
    kthMax(ar, 2);
    kthMax(ar, 34, true);
    kthMax(ar, 34);
    kthMax(ar, 34);
    kthMax(ar, 34);
    kthMax(ar, 34);
    kthMax(ar, 34);

если вы проверите его несколько раз, вы даже эмпирически увидите, что количество итераций в среднем составляет O(n) ~= constant * n и значение k не влияет на алгоритм.


Я придумал этот алгоритм и, кажется, O (n):

предположим, что k=3, и мы хотим найти 3-й по величине элемент в массиве. Я бы создал три переменные и сравнил каждый элемент массива с минимумом этих трех переменных. Если элемент массива больше нашего минимума, мы заменим переменную min значением элемента. Мы продолжаем то же самое до конца массива. Минимум наших трех переменных является третьим по величине элементом в матрица.

define variables a=0, b=0, c=0
iterate through the array items
    find minimum a,b,c
    if item > min then replace the min variable with item value
    continue until end of array
the minimum of a,b,c is our answer

и, чтобы найти K-й по величине элемент, нам нужно K переменных.

пример: (k=3)

[1,2,4,1,7,3,9,5,6,2,9,8]

Final variable values:

a=7 (answer)
b=8
c=9

может кто-то пожалуйста, проверьте это и дайте мне знать, что мне не хватает?


вот реализация предложенного алгоритма eladv(я также поставил здесь реализацию со случайным поворотом):

public class Median {

    public static void main(String[] s) {

        int[] test = {4,18,20,3,7,13,5,8,2,1,15,17,25,30,16};
        System.out.println(selectK(test,8));

        /*
        int n = 100000000;
        int[] test = new int[n];
        for(int i=0; i<test.length; i++)
            test[i] = (int)(Math.random()*test.length);

        long start = System.currentTimeMillis();
        random_selectK(test, test.length/2);
        long end = System.currentTimeMillis();
        System.out.println(end - start);
        */
    }

    public static int random_selectK(int[] a, int k) {
        if(a.length <= 1)
            return a[0];

        int r = (int)(Math.random() * a.length);
        int p = a[r];

        int small = 0, equal = 0, big = 0;
        for(int i=0; i<a.length; i++) {
            if(a[i] < p) small++;
            else if(a[i] == p) equal++;
            else if(a[i] > p) big++;
        }

        if(k <= small) {
            int[] temp = new int[small];
            for(int i=0, j=0; i<a.length; i++)
                if(a[i] < p)
                    temp[j++] = a[i];
            return random_selectK(temp, k);
        }

        else if (k <= small+equal)
            return p;

        else {
            int[] temp = new int[big];
            for(int i=0, j=0; i<a.length; i++)
                if(a[i] > p)
                    temp[j++] = a[i];
            return random_selectK(temp,k-small-equal);
        }
    }

    public static int selectK(int[] a, int k) {
        if(a.length <= 5) {
            Arrays.sort(a);
            return a[k-1];
        }

        int p = median_of_medians(a);

        int small = 0, equal = 0, big = 0;
        for(int i=0; i<a.length; i++) {
            if(a[i] < p) small++;
            else if(a[i] == p) equal++;
            else if(a[i] > p) big++;
        }

        if(k <= small) {
            int[] temp = new int[small];
            for(int i=0, j=0; i<a.length; i++)
                if(a[i] < p)
                    temp[j++] = a[i];
            return selectK(temp, k);
        }

        else if (k <= small+equal)
            return p;

        else {
            int[] temp = new int[big];
            for(int i=0, j=0; i<a.length; i++)
                if(a[i] > p)
                    temp[j++] = a[i];
            return selectK(temp,k-small-equal);
        }
    }

    private static int median_of_medians(int[] a) {
        int[] b = new int[a.length/5];
        int[] temp = new int[5];
        for(int i=0; i<b.length; i++) {
            for(int j=0; j<5; j++)
                temp[j] = a[5*i + j];
            Arrays.sort(temp);
            b[i] = temp[2];
        }

        return selectK(b, b.length/2 + 1);
    }
}

это похоже на стратегию quickSort, где мы выбираем произвольный поворот и приводим меньшие элементы влево, а большие вправо

    public static int kthElInUnsortedList(List<int> list, int k)
    {
        if (list.Count == 1)
            return list[0];

        List<int> left = new List<int>();
        List<int> right = new List<int>();

        int pivotIndex = list.Count / 2;
        int pivot = list[pivotIndex]; //arbitrary

        for (int i = 0; i < list.Count && i != pivotIndex; i++)
        {
            int currentEl = list[i];
            if (currentEl < pivot)
                left.Add(currentEl);
            else
                right.Add(currentEl);
        }

        if (k == left.Count + 1)
            return pivot;

        if (left.Count < k)
            return kthElInUnsortedList(right, k - left.Count - 1);
        else
            return kthElInUnsortedList(left, k);
    }


вы можете найти K-й наименьший элемент в O (n) времени и постоянном пространстве. Если рассматривать массив только для целых чисел.

подход состоит в том, чтобы выполнить двоичный поиск по диапазону значений массива. Если у нас есть min_value и max_value в целочисленном диапазоне, мы можем выполнить двоичный поиск в этом диапазоне. Мы можем написать функцию компаратора, которая скажет нам, является ли какое-либо значение kth-наименьшим или меньшим, чем KTH-наименьшее или больше, чем kth-наименьшее. Выполните двоичный поиск, пока не достигнете kth-наименьшее число

здесь код

класс решение:

def _iskthsmallest(self, A, val, k):
    less_count, equal_count = 0, 0
    for i in range(len(A)):
        if A[i] == val: equal_count += 1
        if A[i] < val: less_count += 1

    if less_count >= k: return 1
    if less_count + equal_count < k: return -1
    return 0

def kthsmallest_binary(self, A, min_val, max_val, k):
    if min_val == max_val:
        return min_val
    mid = (min_val + max_val)/2
    iskthsmallest = self._iskthsmallest(A, mid, k)
    if iskthsmallest == 0: return mid
    if iskthsmallest > 0: return self.kthsmallest_binary(A, min_val, mid, k)
    return self.kthsmallest_binary(A, mid+1, max_val, k)

# @param A : tuple of integers
# @param B : integer
# @return an integer
def kthsmallest(self, A, k):
    if not A: return 0
    if k > len(A): return 0
    min_val, max_val = min(A), max(A)
    return self.kthsmallest_binary(A, min_val, max_val, k)

существует также один алгоритм, который превосходит алгоритм быстрого выбора. Это называется алгоритм Floyd-Rivets (FR).

Оригинальная статья:https://doi.org/10.1145/360680.360694

загружаемая версия: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.309.7108&rep=rep1&type=pdf

статья в Википедии https://en.wikipedia.org/wiki/Floyd%E2%80%93Rivest_algorithm

I пытался реализовать алгоритм quickselect и FR в C++. Также я сравнил их со стандартными реализациями библиотеки C++ std:: nth_element (который в основном является гибридом quickselect и heapselect). Результат был quickselect и nth_element работал в среднем сравнительно, но алгоритм FR работал ок. в два раза быстрее, чем они.

пример кода, который я использовал для алгоритма FR:

template <typename T>
T FRselect(std::vector<T>& data, const size_t& n)
{
    if (n == 0)
        return *(std::min_element(data.begin(), data.end()));
    else if (n == data.size() - 1)
        return *(std::max_element(data.begin(), data.end()));
    else
        return _FRselect(data, 0, data.size() - 1, n);
}

template <typename T>
T _FRselect(std::vector<T>& data, const size_t& left, const size_t& right, const size_t& n)
{
    size_t leftIdx = left;
    size_t rightIdx = right;

    while (rightIdx > leftIdx)
    {
        if (rightIdx - leftIdx > 600)
        {
            size_t range = rightIdx - leftIdx + 1;
            long long i = n - (long long)leftIdx + 1;
            long long z = log(range);
            long long s = 0.5 * exp(2 * z / 3);
            long long sd = 0.5 * sqrt(z * s * (range - s) / range) * sgn(i - (long long)range / 2);

            size_t newLeft = fmax(leftIdx, n - i * s / range + sd);
            size_t newRight = fmin(rightIdx, n + (range - i) * s / range + sd);

            _FRselect(data, newLeft, newRight, n);
        }
        T t = data[n];
        size_t i = leftIdx;
        size_t j = rightIdx;
        // arrange pivot and right index
        std::swap(data[leftIdx], data[n]);
        if (data[rightIdx] > t)
            std::swap(data[rightIdx], data[leftIdx]);

        while (i < j)
        {
            std::swap(data[i], data[j]);
            ++i; --j;
            while (data[i] < t) ++i;
            while (data[j] > t) --j;
        }

        if (data[leftIdx] == t)
            std::swap(data[leftIdx], data[j]);
        else
        {
            ++j;
            std::swap(data[j], data[rightIdx]);
        }
        // adjust left and right towards the boundaries of the subset
        // containing the (k - left + 1)th smallest element
        if (j <= n)
            leftIdx = j + 1;
        if (n <= j)
            rightIdx = j - 1;
    }

    return data[leftIdx];
}

template <typename T>
int sgn(T val) {
    return (T(0) < val) - (val < T(0));
}

Я бы сделал вот что:

initialize empty doubly linked list l
for each element e in array
    if e larger than head(l)
        make e the new head of l
        if size(l) > k
            remove last element from l

the last element of l should now be the kth largest element

Вы можете просто хранить указатели на первый и последний элемент в связанном списке. Они меняются только при обновлении списка.

обновление:

initialize empty sorted tree l
for each element e in array
    if e between head(l) and tail(l)
        insert e into l // O(log k)
        if size(l) > k
            remove last element from l

the last element of l should now be the kth largest element

Сначала мы можем построить BST из несортированного массива, который занимает O(n) время, и из BST мы можем найти K-й наименьший элемент в O(log(n)), который по всем подсчетам порядка O (n).