【问题标题】:How to find the kth largest element in an unsorted array of length n in O(n)?如何在 O(n) 中找到长度为 n 的未排序数组中的第 k 个最大元素?
【发布时间】:2010-09-20 02:08:11
【问题描述】:

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

【问题讨论】:

  • 顺便说一下,当 k==n 时,这里描述的几乎所有算法都会变成 O(n^2) 或 O(n log n)。也就是说,对于所有 k 值,我不认为其中一个是 O(n)。我因为指出这一点而被修改了,但我认为无论如何你都应该知道。
  • 对于任何固定的 k 值,选择算法都可以是 O(n)。也就是说,您可以有一个 k=25 的选择算法,对于任何 n 值都是 O(n),并且您可以对与 n 无关的任何特定 k 值执行此操作。算法不再是 O(n) 的情况是当 k 的值对 n 的值有一定的依赖关系时,例如 k=n 或 k=n/2。然而,这并不意味着如果您碰巧在 25 个项目的列表上运行 k=25 算法,它突然不再是 O(n),因为 O 表示法描述了算法的属性,而不是特定的运行它。
  • 我在一次亚马逊采访中被问到这个问题,作为寻找第二大元素的一般案例。顺便问一下面试官主持面试的时候我没有问我是否可以破坏原始数组(即排序它),所以我想出了一个复杂的解决方案。
  • 这是 Jon Bentley 的 Programming Pearls 第 11 列(排序)中的问题 9。
  • @KirkStrauser :如果 k==n 或 k==n-1 那么它就变得微不足道了。我们可以在单次遍历中获得最大值或第二个最大值。所以这里提供的算法实际上将用于不属于 {1,2, n-1, n} 的 k 值

标签: performance algorithm big-o


【解决方案1】:

这称为找到k-th order statistic。有一个非常简单的随机算法(称为 quickselect),平均时间为 O(n),最坏情况时间为 O(n^2),还有一个非常复杂的非随机算法(称为 introselect)以O(n) 最坏情况时间。 Wikipedia有一些信息,但不是很好。

你需要的一切都在these powerpoint slides。只是为了提取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 等人的《算法简介》一书中也非常详细地介绍了这一点。

【讨论】:

  • 为什么它必须在尺寸 5 上工作?为什么它不能与 3 号一起使用?
  • 如果您注意到,这几乎是快速排序,除了当枢轴位于“k-th”位置时您停止。
  • @eladv 幻灯片链接已损坏 :(
  • @eladv 请修复损坏的链接。
  • @MishaMoroshko 链接已修复
【解决方案2】:

如果您想要一个真正的O(n) 算法,而不是O(kn) 或类似的算法,那么您应该使用快速选择(它基本上是快速排序,您可以丢弃您不感兴趣的分区)。我的教授写了一篇很棒的文章,包括运行时分析:(reference)

QuickSelect 算法快速找到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(n<sup>2</sup>)

但如果选择确实是随机的,则预期运行时间由下式给出

T(n) &lt;= Theta(n) + (1/n) ∑<sub>i=1 to n</sub>T(max(i, n-i-1))

我们做出了不完全合理的假设,即递归总是落在A1A2 中的较大者。

让我们猜测T(n) &lt;= 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) ∑<sub>i=n/2 to n</sub> 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 “足够大”来用更干净(更小)的n/4 替换丑陋的floor(n/2) 因素。现在我们可以继续

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

提供a &gt; 16c

这给出了T(n) = O(n)。显然是Omega(n),所以我们得到T(n) = Theta(n)

【讨论】:

  • 快速选择在平均情况下只有 O(n)。在最坏情况下,可以使用中位数算法在 O(n) 时间内解决问题。
  • k &gt; length(A) - length(A2)是什么意思?
  • 这不是 O(n),您再次以递归方式调用该函数,T(n)。递归函数T(n)里面已经有一个O(n)了,所以显然不假思索,整体复杂度会大于O(n)。
  • @MrROY 鉴于我们在枢轴周围将A 拆分为A1A2,我们知道length(A) == length(A1)+length(A2)+1。所以,k &gt; length(A)-length(A2) 等价于k &gt; length(A1)+1,当k 位于A2 的某个位置时,这是正确的。
  • @FilipeGonçalves,是的,如果枢轴中没有重复的元素。 len(A1) + len(A2) + K-duplicate = len(A)
【解决方案3】:

在那个('第 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)

【讨论】:

  • 嗯,它实际上是 O(n)+ O( k log n),对于显着的 K 值不会减少
  • 但是在该双向链表中找到插入点是 O(k)。
  • 如果k是固定的,O(k) = O(1)
  • @warren:Big-O 是近似的,但你总是过度近似。例如,快速排序实际上是 O(n^2),因为这是最坏的情况。这个是 O(n + k log n)。
  • 您不能将 k 视为常数。有可能 k = n 在这种情况下时间复杂度是 O(nlogn)
【解决方案4】:

你确实喜欢快速排序。随机选择一个元素并将所有元素推高或推低。此时你会知道你实际选择了哪个元素,如果它是你完成的第 k 个元素,否则你用 bin 重复(更高或更低),第 k 个元素会落入。从统计学上讲,时间需要找到第 k 个元素随 n 增长,O(n)。

【讨论】:

  • 这就是快速选择,FWIW。
【解决方案5】:

A Programmer's Companion to Algorithm Analysis 给出了一个 O(n) 的版本,尽管作者指出常数因子是如此之高,但您可能更喜欢朴素的 sort-the-list-then-选择方法。

我回答了你的问题:)

【讨论】:

  • 并非在所有情况下都是如此。我已经实现了中位数,并将其与 .NET 中的内置排序方法进行了比较,自定义解决方案的运行速度确实快了一个数量级。现在真正的问题是:在特定情况下,这对你是否重要。与一行代码相比,编写和调试 100 行代码只有当代码将被执行很多次以致用户开始注意到运行时间的差异并在等待操作完成时感到不适时才会有所回报。
【解决方案6】:

C++ 标准库几乎与 function 调用 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(nlogn),最坏情况为 O(n^2)。哇,有些事情实际上是错误的!
  • 不,这个答案实际上没有任何问题。它可以工作,并且 C++ 标准需要预期的线性运行时间。
  • 我在采访中被要求假设 O(k) 的空间可用性和“n”非常大。我无法告诉他 O(n) 解决方案,因为我认为 nth_element 需要空间 o(n)。我错了吗?底层算法不是基于 nth_element 的快速排序吗?
【解决方案7】:

虽然不太确定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。但是没有必要使用额外的空间。
【解决方案8】:

我使用动态编程,特别是锦标赛方法,实现了在 n 个未排序元素中找到第 k 个最小值。执行时间为 O(n + klog(n))。使用的机制被列为维基百科页面上关于选择算法的方法之一(如上面的帖子之一所示)。您可以在我的博客页面Finding Kth Minimum 上阅读该算法并找到代码(java)。此外,该逻辑可以对列表进行部分排序 - 在 O(klog(n)) 时间内返回第一个 K min(或 max)。

虽然代码提供了第 k 个最小值,但可以使用类似的逻辑在 O(klog(n)) 中找到第 k 个最大值,忽略创建锦标赛树所做的准备工作。

【讨论】:

    【解决方案9】:

    你可以在 O(n + kn) = O(n)(对于常数 k)(对于时间)和 O(k)(对于空间)中做到这一点,方法是跟踪你见过的 k 个最大元素。

    对于数组中的每个元素,您可以扫描 k 个最大的列表,如果最小的元素更大,则将其替换为新元素。

    Warren 的优先堆解决方案更简洁。

    【讨论】:

    • 这将是 O(n^2) 的最坏情况,您被要求提供最小的项目。
    • “最小项”表示k=n,所以k不再是常数。
    • 或者也许保留一个你迄今为止看到的最大 k 的堆(或反向堆,或平衡树)O(n log k)...在大 k 的情况下仍会退化为 O(nlogn) .我认为它适用于较小的 k 值但是...可能比这里提到的其他一些算法更快 [???]
    【解决方案10】:

    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 个 smallest 元素。反转列表推导中的比较运算符 a1 = [i for i in arr if i &gt; arr[r]]a2 = [i for i in arr if i &lt; arr[r]],将返回第 k 个 最大 元素。
    • 从一个小的基准测试,即使在大数组上,排序(numpy.sort 用于numpy arraysorted 用于列表)比使用此手动实现要快。
    【解决方案11】:

    根据本文Finding the Kth largest item in a list of n items,以下算法在最坏情况下将花费O(n) 时间。

    1. 将数组分成 n/5 个列表,每个列表包含 5 个元素。
    2. 找出每个包含 5 个元素的子数组的中位数。
    3. 递归求所有中位数的中位数,我们称之为M
    4. 将数组划分为两个子数组 第一个子数组包含大于 M 的元素,假设这个子数组是 a1 ,而其他子数组包含小于 M 的元素,我们称这个子数组a2.
    5. 如果 k
    6. 如果 k− 1 = |a1|,则返回 M。
    7. 如果 k> |a1| + 1,返回选择(a2,k -a1 - 1)。

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

    我们使用中位数将列表分成两半(前半部分, 如果 k &lt;= n/2 ,则为后半部分)。该算法取 时间cn 在第一级递归,对于一些常量ccn/2 在 下一个级别(因为我们在大小为 n/2 的列表中递归),cn/4 在 第三级,以此类推。总耗时为cn + cn/2 + cn/4 + .... = 2cn = o(n)

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

    如原paper中所述:

    将列表除以 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 &lt; 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

    【讨论】:

      【解决方案12】:

      在线性时间内找到数组的中位数,然后使用与快速排序完全相同的分区过程将数组分成两部分,中位数左侧的值小于( ) 中位数,这也可以在 lineat 时间内完成,现在,转到第 k 个元素所在的数组部分, 现在递归变为: T(n) = T(n/2) + cn 这给了我 O (n) 的整体。

      【讨论】:

      • 不需要求中位数。没有中位数,您的方法仍然可以。
      • 你如何找到线性时间的中位数,我敢问吗? ... :)
      【解决方案13】:

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

      这里是全文链接(就是找第K个smallest元素,但是找第K个largest的原理是一样的):

      Finding Kth Smallest Element in an Unsorted Array

      【讨论】:

        【解决方案14】:

        这种方法怎么样

        维护一个buffer of length k 和一个tmp_max,得到 tmp_max 是 O(k) 并且完成 n 次,所以类似于 O(kn)

        是对的还是我错过了什么?

        虽然它没有超过快速选择的平均情况和中位数统计方法的最坏情况,但它很容易理解和实现。

        【讨论】:

        • 我喜欢,更容易理解。尽管正如您所指出的,复杂性是 O(nk)。
        【解决方案15】:

        遍历列表。如果当前值大于存储的最大值,则将其存储为最大值并将 1-4 向下和 5 从列表中删除。如果不是,将其与 2 号进行比较并做同样的事情。重复,对照所有 5 个存储值检查它。这应该在 O(n) 中完成

        【讨论】:

        • 如果你使用数组,那个“bump”是 O(n),如果你使用更好的结构,这个“bump”是 O(log n)(我认为)。
        • 它不必是 O(log k) - 如果列表是链表,那么将新元素添加到顶部并删除最后一个元素更像 O(2)
        • 对于一个数组支持的列表来说,凹凸是 O(k),对于一个适当的链表来说是 O(1)。无论哪种方式,这类问题通常假设它与 n 相比影响最小,并且不再引入 n 的因素。
        • 如果凹凸使用环形缓冲区,它也将是 O(1)
        • 无论如何,评论的算法是不完整的,它没有考虑到一个新的(例如)第二大元素n。最坏情况下,n 中的每个元素都必须与高分表中的每个元素进行比较,是 O(kn) - 但就问题而言,这仍然可能意味着 O(n)。
        【解决方案16】:

        我想提出一个答案

        如果我们取前 k 个元素并将它们排序到 k 个值的链表中

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

        干杯

        【讨论】:

        • 排序需要 nlogn 时间...算法应该在线性时间内运行
        【解决方案17】:

        可以在此处找到用于查找 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 元素案例的中值之前对数组进行排序。
        【解决方案18】:

        还有Wirth's selection algorithm,它的实现比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];
        }
        

        在我对here 进行的基准测试中,LefSelect 比 QuickSelect 快 20-30%。

        【讨论】:

          【解决方案19】:

          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 方法在不实际计算分区的情况下发现分区的大小来实现中位数解的中位数。

          【讨论】:

            【解决方案20】:

            这里是随机快速选择的 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)。在最坏情况下,随机函数可能总是选择一个角元素。上述随机快速选择的预期时间复杂度为 Θ(n)

            【讨论】:

            • 不错的编码。感谢分享,+1
            【解决方案21】:
            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;        
              }
              

            【讨论】:

              【解决方案22】:

              还有一种算法优于快速选择算法。它被称为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

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

              【讨论】:

              • 看起来是双轴快速选择
              【解决方案23】:

              这是 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) ~= constant * n,并且 k 的值不会影响算法。

              【讨论】:

                【解决方案24】:

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

                假设 k=3,我们想要找到数组中的第三大项。我将创建三个变量并将数组的每个项目与这三个变量中的最小值进行比较。如果数组 item 大于我们的最小值,我们将用 item 值替换 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
                

                有人可以查看这个并让我知道我缺少什么吗?

                【讨论】:

                  【解决方案25】:

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

                  【讨论】:

                    【解决方案26】:

                    它类似于快速排序策略,我们选择任意枢轴,并将较小的元素放在其左侧,将较大的元素放在右侧

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

                    【讨论】:

                      【解决方案27】:
                      【解决方案28】:

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

                      方法是对数组值的范围进行二进制搜索。如果我们有一个 min_value 和一个 max_value 都在整数范围内,我们可以在该范围内进行二进制搜索。 我们可以编写一个比较器函数,它会告诉我们任何值是否是第 k 小或小于第 k 小或大于第 k 小。 进行二分查找,直到找到第 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)
                      

                      【讨论】:

                      • 由于您使用递归,我不认为空间复杂度是恒定的
                      【解决方案29】:

                      第k大元素意味着,我们需要对数组进行排序,然后从数组末尾倒数。例如

                       const array= [2, 32, 12, 3, 78, 99, 898, 8, 1] // we need to sort this
                       const sortedArray= [1,  2,  3,   8, 12, 32, 78, 99, 898] 
                      

                      第 5 个最大元素意味着从最后倒数 5 个元素,即 12。

                      默认情况下,大多数语言都实现快速排序或合并排序,因为它们是最优化的排序。我用快速排序解决了这个问题。快速排序对数组进行就地排序,它不会给我们返回一个新数组,并且像合并排序一样它是递归的。快速排序的缺点,在最坏的情况下,它的时间复杂度是 O(n**2) "n square"。

                      快速排序是分而治之的算法,通过解决其所有较小的组件来解决问题。我们选择最后一个元素作为枢轴元素(有些算法选择第一个元素作为枢轴,有些算法在开始时选择最后一个元素)。枢轴元素是分区元素。这是我们的数组=[2, 32, 12, 3, 78, 99, 898, 8, 1]

                      我们使用 2 个指针,i,j 从第一个元素开始。 "i" 跟踪枢轴的最终位置。

                      i=j=2 //starting point
                      

                      "j" 将扫描数组,并将每个元素与枢轴进行比较。如果“j”小于pivot,我们将交换“i”和“j”并将“i”和“j”向前移动。当“j”到达枢轴时,我们将“i”与枢轴交换。在我们的示例中,枢轴是1,1 是较小的数字,“j”将到达pivot=1,而无需交换“i”、“j”。请记住,“i”是枢轴的占位符。所以当“j”到达pivot时,1和2会互换。

                      此操作的目的是查找小于其左侧枢轴的所有元素。请注意,枢轴左侧的所有元素都小于枢轴,但左侧未排序。然后我们将数组从枢轴分成2个子数组,并递归应用快速排序。

                      const quickSort = function (array, left, right) {
                        // if left=right, it means we have only one item, it is already sorted
                        if (left < right) {
                          const partitionIndex = partition(array, left, right);
                          quickSort(array, left, partitionIndex - 1); 
                          quickSort(array, partitionIndex + 1, right);
                        }
                      };
                      

                      使用“i”和“j”指针,这就是我们找到分区索引的方式

                      const partition = function (array, left, right) {
                        const pivotElement = array[right];
                        let partitionIndex = left;
                        for (let j = left; j < right; j++) {
                          if (array[j] < pivotElement) {
                            swap(array, partitionIndex, j);
                            partitionIndex++;
                          }
                        }
                        // if none of the "j" values is smaller than pivot, when "j" reaches the pivot, we swap "i'th" element with pivot
                        swap(array, partitionIndex, right);
                        return partitionIndex;
                      };
                      

                      这是一个简单的交换函数实现:

                      const swap = function (array, i, j) {
                        const temp = array[i];
                        array[i] = array[j];
                        array[j] = temp;
                      };
                      

                      【讨论】:

                        【解决方案30】:
                            function nthMax(arr, nth = 1, maxNumber = Infinity) {
                              let large = -Infinity;
                              for(e of arr) {
                                if(e > large && e < maxNumber ) {
                                  large = e;
                                } else if (maxNumber == large) {
                                  nth++;
                                }
                              }
                              return nth==0 ? maxNumber: nthMax(arr, nth-1, large);
                            }
                        
                            let array = [11,12,12,34,23,34];
                        
                            let secondlargest = nthMax(array, 1);
                        
                            console.log("Number:", secondlargest);

                        【讨论】:

                          猜你喜欢
                          • 2013-01-31
                          • 1970-01-01
                          • 1970-01-01
                          • 1970-01-01
                          • 2020-10-27
                          • 2018-06-30
                          • 2016-03-20
                          • 1970-01-01
                          相关资源
                          最近更新 更多