如何在O(n)中长度为n的未排序数组中找到第k个最大元素?


220

我相信有一种方法可以找到O(n)中长度为n的未排序数组中的第k个最大元素。或者也许是“预期” O(n)之类的东西。我们应该怎么做?


49
顺便说一下,当k == n时,此处描述的几乎所有算法都变为O(n ^ 2)或O(n log n)。也就是说,对于k的所有值,我都不认为其中一个是O(n)。我因指出这一点而感到沮丧,但以为您仍然应该知道。
Kirk Strauser

19
对于k的任何固定值,选择算法可以为O(n)。也就是说,对于k的任何值,您都可以使用k = 25的选择算法,对于任何n的值,您都可以使用O(n)的选择算法,并且对于与n无关的k的任何特定值,可以执行选择算法。算法不再为O(n)的情况是k的值对n的值有某种依赖性时,例如k = n或k = n / 2。但是,这并不意味着如果您碰巧在25个项目的列表上运行k = 25算法,则它突然不再是O(n),因为O表示法描述了算法的属性,而不是特定的运行它。
泰勒·麦克亨利

1
在亚马逊面试中,有人问我这个问题,作为寻找第二大元素的一般案例。顺便说一下,面试官主持面试的方式并没有问我是否可以销毁原始数组(即对其进行排序),所以我想出了一个复杂的解决方案。
Sambatyon 2011年

4
这是乔恩·本特利(Jon Bentley)在《编程珍珠》的第11列(排序)中的问题9。
徐强2012年

3
@KirkStrauser:如果k == n或k == n-1,则它变得微不足道。我们可以在一次遍历中获得最大或第二个最大。因此,此处提供的算法将实际用于不属于{1,2,n-1,n}的k值
Aditya Joshee

Answers:


173

这称为查找第k阶统计量。有一个非常简单的随机算法(称为quickselect),它占用了O(n)平均时间,O(n^2)最坏情况下的时间,还有一个相当复杂的非随机算法(称为introselect),它占用了O(n)最坏情况的时间。Wikipedia上有一些信息,但这不是很好。

您需要的所有内容都在这些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)

在Cormen等人的《算法介绍》一书中,它也做了非常详细的介绍。


6
谢谢你的幻灯片。
Kshitij Banerjee 2014年

5
为什么必须使用5号尺寸?为什么不能使用3号尺寸?
Joffrey Baratheon

11
@eladv幻灯片链接已损坏:(
Misha Moroshko '16

7
@eladv Plese修复断开的链接。
maxx777

1
@MishaMoroshko链接已固定
-alfasin

118

如果您想要一个真正的O(n)算法,而不是O(kn)类似的东西,那么您应该使用quickselect(基本上是quicksort,您将不感兴趣的分区丢弃了)。我的教授写的很好,并进行了运行时分析:(参考资料

快速选择算法快速找到未排序的n元素数组中的第k个最小元素。这是一个RandomizedAlgorithm,因此我们计算最坏情况下的预期运行时间。

这是算法。

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 an2(1/n)(n/2)an = ancn

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)


12
在一般情况下,快速选择仅为O(n)。在最坏的情况下,中位数中值算法可用于解决O(n)时间中的问题。
约翰·库拉克

是什么意思k > length(A) - length(A2)
WoooHaaaa 2013年

这不是O(n),您将以递归方式再次调用函数T(n)。递归函数T(n)内已经有一个O(n),因此显然不加考虑,总体复杂度将大于O(n)。
user1735921 2014年

3
@MrROY鉴于我们已A分解A1A2围绕数据透视图,我们知道length(A) == length(A1)+length(A2)+1。因此,k > length(A)-length(A2)等价于k > length(A1)+1,当k位于中的某处时为真A2
FilipeGonçalves2014年

@FilipeGonçalves,如果数据透视表中没有重复的元素,则为是。len(A1)+ len(A2)+ K-duplicate = len(A)
d1val 2014年

16

对此的快速Google(“第k个最大元素数组”)返回了以下内容: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)

15
好吧,它的实际O(n)+ O(k log n)对于K的显着值不会减少
Jimmy,

2
但是在该双链表中找到插入点是O(k)。
Kirk Strauser

1
如果k是固定的,则O(k)= O(1)
泰勒·麦克亨利

1
@warren:Big-O是近似值,但是您总是过于近似。例如,Quicksort实际上为O(n ^ 2),因为那是最坏的情况。这个是O(n + k log n)。
Claudiu

1
您不能将k视为常数。在这种情况下,时间复杂度为O(nlogn)
sabbir

11

你喜欢快速排序。随机选择一个元素,然后将所有元素推高或调低。在这一点上,您将知道您实际上选择了哪个元素,并且如果完成的是第k个元素,否则您将对bin(较高或较低)重复一次,则第k个元素将落入。从统计上讲,时间需要找到第k个元素随n(O(n))增长。


2
这就是quickselect,FWIW。
rogerdpack '16

6

程序员的算法分析同伴给出的版本 O(n),尽管作者指出常数因子是如此之高,但您可能更喜欢朴素的先排序后列表再选择方法。

我回答了你的问题信:)


2
并非在所有情况下都是如此。我已经实现了中位数中位数,并将其与.NET中的内置Sort方法进行了比较,自定义解决方案的运行速度实际上快了一个数量级。现在真正的问题是:在给定的情况下,这对您是否重要?与要执行一次编码相比,编写和调试100行代码只有在执行该代码的次数如此之多,以至于用户开始注意到运行时间的差异并在等待操作完成时感到不舒服时,才会有回报。
Zoran Horvat

5

尽管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

1
不,它具有预期的平均 O(n)运行时间。例如,快速排序平均为O(nlogn),最差情况为O(n ^ 2)。哇,事实直截了当!
Kirk Strauser

5
不,这个答案没有任何错误。它可以工作,并且C ++标准要求预期的线性运行时间。
David Nehme,

在面试中要求我假设O(k)的空间可用性非常大。我无法告诉他O(n)解决方案,因为我认为nth_element需要空间o(n)。我错了吗?底层算法不是基于nth_element的quicksort吗?
Manish Baphna 2011年

4

尽管不是很确定O(n)的复杂性,但是可以确定它在O(n)和nLog(n)之间。另外,请确保比nLog(n)更靠近O(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;
    }
}

不错的编码,+ 1。但是没有必要使用额外的空间。
亨伽美

4

我实现了使用动态编程,特别是锦标赛方法,在n个未排序元素中找到第k个极小值。执行时间为O(n + klog(n))。所使用的机制被列为Wikipedia页面上有关选择算法的一种方法(如上面发布的内容之一所示)。您可以在我的博客页面Finding Kth Minimum上了解有关该算法的信息,也可以找到代码(java)。另外,逻辑可以对列表进行部分排序-在O(klog(n))时间中返回第一个K min(或max)。

尽管所提供的代码的结果是第k个最小值,但是可以采用类似的逻辑在O(klog(n))中找到第k个最大值,而忽略了创建锦标赛树的前功。


3

您可以通过跟踪您已经看到的k个最大元素,以O(n + kn)= O(n)(对于常数k)(对于时间,O(k))作为空间来实现。

对于数组中的每个元素,您都可以扫描k个最大的列表,并用新的最小元素替换最小的元素。

沃伦的优先堆解决方案虽然更整洁。


3
这将是O(n ^ 2)的最坏情况,要求您提供最小的项目。
Elie

2
“最小项目”是指k = n,因此k不再恒定。
Tyler McHenry,2009年

或者也许保留到目前为止您已经看到的最大k的堆(或反向堆或平衡树)...如果k很大O(n log k),仍然退化为O(nlogn)。我认为它对于k的较小值会很好,但是...可能比这里提到的某些其他算法更快[???]
rogerdpack '16

3

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]

不错的解决方案,除了它会返回未排序列表中的第k个最小元素。反转列表推导中的比较运算符a1 = [i for i in arr if i > arr[r]]a2 = [i for i in arr if i < arr[r]],将返回第k个最大元素。
gumption

从一个小基准,即使是在大型阵列,这是更快的排序(与numpy.sort用于numpy arraysorted用于列表),比使用本手册执行。
Næreen

2

在线性时间内找到数组的中位数,然后使用与快速排序完全相同的分区过程将数组分为两部分,中位数左侧的值小于(<)小于中位数,而右侧的值大于(>) ,这也可以在线性时间完成,现在,转到数组中第k个元素所在的部分,现在重复出现:T(n)= T(n / 2)+ cn,得出O(n)总和。


无需找到中位数。没有中位数,您的方法仍然可以。
Hengameh 2015年

2
我敢问,如何找到线性时间的中位数?... :)
rogerdpack '16

2

下面是完整实现的链接,其中有相当广泛的解释,该算法如何在未排序的算法中找到第K个元素。基本思想是像在QuickSort中一样对数组进行分区。但是为了避免极端情况(例如,在每个步骤中选择最小的元素作为枢轴,以便算法退化为O(n ^ 2)运行时间),应用了特殊的枢轴选择,称为中位数算法。在最坏的情况下和在平均情况下,整个解决方案的运行时间为O(n)。

这是全文的链接(关于找到第K个最小元素,但原理与找到第K个最大元素相同):

在未排序数组中查找第K个最小元素


2

根据本文,在n个项目列表中找到第K个最大的项目,以下算法O(n)在最坏的情况下会花费时间。

  1. 将数组划分为n / 5个,每个包含5个元素。
  2. 在每个包含5个元素的子数组中找到中位数。
  3. 递归查找所有中位数的中位数,我们称其为M
  4. 将数组划分为两个子数组,第一个子数组包含大于M的元素,可以说此子数组为a1,而其他子数组包含的元素小于M,请将该子数组称为a2。
  5. 如果k <= | a1 |,则返回选择(a1,k)。
  6. 如果k-1 = | a1 |,则返回M。
  7. 如果k> | a1 | +1,返回选择(a2,k −a1 − 1)。

分析:如原始论文中所建议:

我们使用中位数将列表分为两半(前半部分为if k <= n/2,后半部分为not)。该算法cn在第一个递归级别上花费一些常量时间ccn/2在下一个级别上花费时间(因为我们递归大小为n / 2的列表),cn/4在第三级上花费时间,依此类推。总耗时为cn + cn/2 + cn/4 + .... = 2cn = o(n)

为什么分区大小取5而不是3?

如原始文件所述

将列表除以5可确保最差情况下的分割为70-30。中位数的至少一半大于中位数,因此n / 5块中的至少一半具有至少3个元素,这产生了 3n/10分割,表示另一个分区在最坏的情况下为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];
    }

只是为了完成,另一种算法利用了Priority Queue并花费了时间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


@rogerdpack我提供了我关注的链接。
akhil_mittal '16

2

这种方法怎么样

维持a buffer of length k和a tmp_max,得到tmp_max为O(k)并完成n次,所以类似O(kn)

在此处输入图片说明

是对还是我错过了什么?

尽管它没有击败快速选择的平均情况和中值统计方法的最坏情况,但是它非常易于理解和实施。


1
我喜欢它,更容易理解。正如您指出的,复杂度为O(nk)。
Hajjat

1

遍历列表。如果当前值大于存储的最大值,则将其存储为最大值,然后将1-4降低并从列表中减去5。如果不是,则将其与2进行比较并执行相同的操作。重复,并对照所有5个存储值进行检查。这应该在O(n)中完成


如果使用数组,则“凹凸”为O(n),如果使用更好的结构,则为O(log n)(我认为)。
Kirk Strauser

它不必是O(log k)-如果列表是链接列表,则将新元素添加到顶部并删除最后一个元素更像O(2)
Alnitak

对于数组支持的列表,凹凸将为O(k),对于适当链接的列表,凹凸将为O(1)。无论哪种方式,此类问题通常都假定与n相比影响最小,并且不会引入n的更多因素。
2008年

如果凹凸块使用环形缓冲区,那么它也将是O(1)
Alnitak

1
无论如何,评论的算法是不完整的,它无法考虑到其中n是新的(例如)第二大元素的元素。必须将n中的每个元素与高分表中的每个元素进行比较的最坏情况是O(kn)-但是就问题而言,这仍然意味着O(n)。
2008年

1

我想提出一个答案

如果我们采用前k个元素并将其排序为k个值的链接列表

现在,即使对于最坏的情况,对于其他所有值,如果我们对其余的nk值进行插入排序,即使在最坏的情况下,比较次数也将为k *(nk),对于要排序的前k个值,将其设为k *(k- 1)所以它是(nk-k),即o(n)

干杯


1
排序需要花费nlogn的时间...该算法应在线性时间内运行
MrDatabase

1

可以在以下位置找到对中位数算法的解释,以从n中找到第k个最大整数:http : //cs.indstate.edu/~spitla/presentation.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;
}

此解决方案不起作用。您需要先对数组进行排序,然后再返回5个元素大小写的中位数。
Agnishom Chattopadhyay

1

还有Wirth的选择算法,它的实现比QuickSelect更简单。Wirth的选择算法比QuickSelect慢,但经过一些改进,它变得更快。

更详细地。使用弗拉基米尔·扎布罗德斯基(Vladimir Zabrodsky)的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比QuickSelect快20-30%。


1

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方法来发现分区的大小而无需实际计算,即可实现中值解决方案的中值。


1

这是Randomized QuickSelect的C ++实现。这个想法是随机选择一个枢轴元素。要实现随机分区,我们使用随机函数rand()在l和r之间生成索引,将随机生成的索引处的元素与最后一个元素交换,最后调用将最后一个元素用作枢轴的标准分区过程。

#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)


不错的编码。感谢您的分享,+ 1
Hengameh 2015年

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

0

这是Javascript中的实现。

如果放开约束,你不能修改数组,你可以用两个指标来识别“当前分区”(在经典的快速排序风格防止利用额外的内存- 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);   

现在,运行几次测试。由于Math.random(),每次都会产生不同的结果:

    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)〜=常数* n,并且k的值不会影响算法。


0

我想出了这个算法,似乎是O(n):

假设k = 3,我们想找到数组中的第三大项目。我将创建三个变量,并将数组的每个项目与这三个变量中的最小值进行比较。如果数组item大于最小值,我们将min变量替换为item值。我们继续做同样的事情,直到数组结束。我们三个变量中的最小值是数组中的第三大项。

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

有人可以查看一下,让我知道我在想什么吗?


0

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

0

它类似于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);
    }


0

您可以找到O(n)时间和恒定空间中的第k个最小元素。如果我们认为该数组仅用于整数。

该方法是对数组值的范围进行二进制搜索。如果我们的min_value和max_value都在整数范围内,则可以对该范围进行二进制搜索。我们可以编写一个比较器函数,该函数将告诉我们任何值是最小的kth或小于最小的kth还是大于最小的kth。进行二进制搜索,直到达到第k个最小数字

这是该代码

类解决方案:

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)

0

还有一种算法优于快速选择算法。这就是所谓的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

Wikipedia文章https://en.wikipedia.org/wiki/Floyd%E2%80%93Rivest_algorithm

我试图在C ++中实现quickselect和FR算法。我还将它们与标准C ++库实现std :: nth_element(基本上是quickselect和heapselect的内省混合)进行了比较。结果是快速选择,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));
}

-1

我要做的是:

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

如果e小于head(l)怎么办?它可能仍然大于第k个最大元素,但永远不会添加到该列表中。您需要按升序对项目列表进行排序,以使其起作用。
Elie

您是对的,我想我需要再考虑一下。:-)
Jasper Bekkers,

解决方法是检查e是否在head(l)和tail(l)之间,然后将其插入正确的位置。使这个O(kn)。当使用跟踪最小和最大元素的二叉树时,可以将其设为O(n log k)。
Jasper Bekkers,

-1

首先,我们可以从耗时O(n)的未排序数组构建BST,然后从BST中可以找到O(log(n))的第k个最小元素,其总数达到O(n)的数量级。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.