O(n)で長さnのソートされていない配列でk番目に大きい要素を見つける方法は?


220

O(n)の長さnのソートされていない配列でk番目に大きい要素を見つける方法があると思います。または、おそらく「期待される」O(n)か何かです。どうすればこれを行うことができますか?


49
ちなみに、ここで説明するほとんどすべてのアルゴリズムは、k == nの場合、O(n ^ 2)またはO(n log n)になります。つまり、kのすべての値について、それらの1つがO(n)であるとは思いません。私はこれを指摘するために改造しましたが、とにかく知っておくべきだと思いました。
Kirk Strauser 2008年

19
選択アルゴリズムは、kの任意の固定値に対してO(n)にすることができます。つまり、nの任意の値に対してO(n)であるk = 25の選択アルゴリズムを使用でき、nとは無関係のkの特定の値に対してこれを実行できます。アルゴリズムがO(n)でなくなったのは、k = nやk = n / 2など、kの値がnの値に依存している場合です。ただし、これは、25項目のリストに対してk = 25アルゴリズムを実行した場合、突然O(n)ではなくなることを意味しません。これは、O表記が特定のアルゴリズムではなく、アルゴリズムのプロパティを記述するためです。それの実行。
タイラーマクヘンリー

1
この質問は、2番目に大きい要素を見つける一般的なケースとして、アマゾンのインタビューで尋ねられました。面接担当者が面接を主導したところで、元の配列を破棄できるか(つまり、並べ替えできるか)を尋ねなかったので、複雑な解決策を考え出しました。
Sambatyon、

4
これは、ジョンベントレーによるプログラミングパールの列11(ソーティング)の質問9です。
Qiang Xu

3
@KirkStrauser:k == nまたはk == n-1の場合、それは簡単になります。単一のトラバーサルで最大または2番目の最大を取得できます。したがって、ここで提供されるアルゴリズムは、{1,2、n-1、n}に属さないkの値に対して実際に使用されます
Aditya Joshee

回答:


173

これは、k次の統計量の検索と呼ばれます。平均時間と最悪の場合の時間をとる非常に単純なランダム化アルゴリズム(quickselectと呼ばれる)とO(n)O(n^2)最悪の場合の時間をとるかなり複雑な非ランダム化アルゴリズム(introselectと呼ばれる)がありO(n)ます。ウィキペディアにはいくつかの情報がありますが、あまり良くありません。

あなたが必要とするすべてはこれらのパワーポイントのスライドにあります。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 2016年

7
@eladv Pleseが壊れたリンクを修正しました。
maxx777 2017年

1
@MishaMoroshkoリンクが修正されました
alfasin

118

そのようなものO(n)ではなく、真のアルゴリズムO(kn)が必要な場合は、quickselectを使用する必要があります(基本的に、興味のないパーティションを破棄するクイックソートです)。私の教授は、ランタイム分析を使って素晴らしい記事を書いています:(リファレンス

QuickSelectアルゴリズムは、要素の並べ替えられていない配列のk番目に小さい要素をすばやく見つけnます。これは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)時間の問題を解決できます。
John Kurlak、2013年

の意味はk > length(A) - length(A2)何ですか?
WoooHaaaa 2013年

これはO(n)ではなく、関数を再帰的T(n)として再度呼び出しています。再帰関数T(n)の内部にはすでにO(n)があるため、明らかに考えなければ、全体的な複雑度はO(n)よりも大きくなります。
user1735921 2014

3
@MrROY ピボットとその周りにA分割したことを考えると、それはわかっています。したがって、はと同等です。これは、がのどこかにあるときに当てはまります。A1A2length(A) == length(A1)+length(A2)+1k > length(A)-length(A2)k > length(A1)+1kA2
フィリペゴンサルベス2014

@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の有意な値に対して減少しません
ジミー

2
しかし、その二重リンクされたリストで挿入ポイントを見つけることはO(k)です。
カークストラウザー2008年

1
そして、kが固定されている場合、O(k)= O(1)
タイラーマクヘンリー

1
@warren:Big-Oは概算ですが、常に過大評価です。たとえば、Quicksortは最悪のケースなので、実際にはO(n ^ 2)です。これはO(n + k log n)です。
Claudiu 2010

1
kを定数として扱うことはできません。k = nである可能性があります。この場合、時間の複雑性はO(nlogn)です
sabbir

11

あなたはクイックソートが好きです。ランダムに要素を選択し、すべてを高くしたり低くしたりします。この時点で、実際に選択した要素がわかります。それが実行したk番目の要素である場合は、k番目の要素が該当するビン(上位または下位)で繰り返します。統計的には、時間k番目の要素がn、O(n)と共に成長するのを見つけるのに必要です。


2
これがクイック選択とはFWIWです。
rogerdpack

6

プログラマーのコンパニオンとアルゴリズム分析のバージョン O(n)ですが、作成者は定数係数が非常に高いと述べていますが、おそらく単純なsort-the-list-then-selectメソッドを使用することをお勧めします。

私はあなたの質問の手紙に答えました:)


2
すべてのケースで実際には当てはまりません。私は中央値の中央値を実装し、それを.NETの組み込みのSortメソッドと比較したところ、カスタムソリューションは桁違いに高速に実行されました。さて、本当の問題は次のとおりです。特定の状況でそれが問題になるかどうか 1行と比較して100行のコードを記述してデバッグすると、そのコードが何回も実行され、ユーザーが実行時間の違いに気付き始め、操作が完了するのを待つのに不快感を覚える場合にのみ効果があります。
Zoran Horvat 2013

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)です。うわー、事実が間違っています。
カークストラウザー08/10/30

5
いいえ、この回答に事実上の誤りはありません。これは機能し、C ++標準では予想される線形実行時間が必要です。
David Nehme 2008年

私はインタビューでO(k)の空き容量を想定するように依頼されました。nth_elementにはスペースo(n)が必要だと思ったので、私は彼にO(n)ソリューションを伝えることができませんでした。私は間違っていますか?基になるアルゴリズムはnth_elementに基づくクイックソートではありませんか?
マニッシュバフナ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。しかし、余分なスペースを使用する必要はありません。
ヘンガメ2015

4

動的プログラミング、特にトーナメントメソッドを使用して、n個の並べ替えられていない要素でk番目の最小値を見つけることを実装しました。実行時間はO(n + klog(n))です。使用されているメカニズムは、Wikipediaページの選択アルゴリズムに関するメソッドの1つとしてリストされています(上記の投稿の1つに示されています)。アルゴリズムについて読んだり、コード(java)をブログページFinding Kth Minimumで見つけることができます。さらに、ロジックはリストの部分的な順序付けを行うことができます-O(klog(n))時間で最初のK min(またはmax)を返します。

コードは最小のk番目の結果を提供しましたが、トーナメントツリーを作成するために行われた事前作業を無視して、O(klog(n))で最大のk番目を見つけるために同様のロジックを使用できます。


3

O(n + kn)= O(n)(定数kの場合)は時間で、O(k)は空間で、これまでに見た最大のk個の要素を追跡することで実行できます。

配列内の各要素について、最大のkのリストをスキャンし、それが大きい場合は最小の要素を新しい要素に置き換えることができます。

ただし、Warrenの優先ヒープソリューションの方が簡潔です。


3
これは、最小のアイテムを要求されるO(n ^ 2)の最悪のケースになります。
エリー

2
「最小の項目」はk = nであることを意味するため、kはもはや一定ではありません。
タイラーマクヘンリー

または、これまでに見た最大のkのヒープ(または逆ヒープ、またはバランスツリー)を保持することもできますO(n log k)。kが大きい場合は、依然としてO(nlogn)に縮退します。kの値が小さい場合は問題なく機能すると思いますが、ここで説明した他のアルゴリズムよりも高速である可能性があります[???]
rogerdpack

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番目返します最大の要素を。
ガムプション

小規模なベンチマークから、大規模な配列であっても、この手動の実装を使用するよりも(numpy.sortfor numpy arrayまたはsortedforリストを使用して)ソートする方が高速です。
Næreen

2

線形時間で配列の中央値を見つけ、クイックソートとまったく同じようにパーティションプロシージャを使用して、配列を2つの部分に分割します。中央値の左側の値は中央値よりも小さく(<)、右側の中央値は(>)より大きく(>) 、それも直線的に行うことができます。今度は、配列のk番目の要素が存在する部分に移動します。ここで、再帰は次のようになります。T(n)= T(n / 2)+ cnこれにより、全体的にO(n)が得られます。


中央値を見つける必要はありません。中央値がなくても、アプローチは問題ありません。
ヘンガメ2015

2
そして、どのように線形時間で中央値を見つけるのですか?... :)
rogerdpack 2016

2

以下は、完全な実装へのリンクであり、ソートされていないアルゴリズムでK番目の要素を見つけるアルゴリズムがどのように機能するかを非常に広範囲に説明しています。基本的な考え方は、QuickSortのように配列を分割することです。ただし、極端なケースを回避するために(たとえば、アルゴリズムがO(n ^ 2)実行時間に縮退するように、すべてのステップで最小要素がピボットとして選択される場合)、中央値中央アルゴリズムと呼ばれる特別なピボット選択が適用されます。ソリューション全体は、最悪の場合および平均の場合、O(n)時間で実行されます。

ここに記事全体へのリンクがあります(Kthの最小要素を見つけることについてですが、Kthの最大要素を見つけるための原理は同じです):

並べ替えられていない配列でK番目に小さい要素を見つける


2

この論文のとおり、n個のアイテムリストからK番目に大きいアイテムを見つけると、次のアルゴリズムではO(n)最悪の場合時間がかかります。

  1. 配列をそれぞれ5つの要素のn / 5リストに分割します。
  2. 5つの要素の各サブ配列で中央値を見つけます。
  3. すべての中央値の中央値を再帰的に見つけて、Mと呼びましょう
  4. 配列を2つのサブ配列に分割します。最初のサブ配列にはMより大きい要素が含まれます。このサブ配列はa1であるとしましょう。他のサブ配列にはMより小さい要素が含まれています。このサブ配列をa2と呼びます。
  5. k <= | a1 |の場合、選択(a1、k)を返します。
  6. k− 1 = | a1 |の場合、Mを返します。
  7. k> | a1 |の場合 + 1、選択を返します(a2、k −a1 − 1)。

分析:元の論文で示唆されているように:

中央値を使用して、リストを2つの半分に分割します(前半の場合k <= n/2は前半、後半の場合は後半)。このアルゴリズムは時間がかかりcn、いくつかの定数の再帰の最初のレベルではccn/2(我々は、サイズN / 2のリストに再帰的ため)、次のレベルでcn/4第3のレベルで、など。合計所要時間はcn + cn/2 + cn/4 + .... = 2cn = o(n)です。

パーティションサイズが3ではなく5になるのはなぜですか?

元の論文で述べたように:

リストを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];
    }

完了のために、別のアルゴリズムは優先キューを使用し、時間がかかります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

2

このちょっとしたアプローチはどうですか

a buffer of length kとa を維持し、tmp_maxtmp_maxの取得はO(k)であり、n回実行されるので、O(kn)

ここに画像の説明を入力してください

それは正しいですか、何か不足していますか?

クイックセレクトの平均的なケースや中央値統計法の最悪のケースに勝るものはありませんが、理解と実装はかなり簡単です。


1
私はそれが好きで、理解しやすいです。あなたが指摘したように複雑さはO(nk)ですが。
Hajjat

1

リストを反復処理します。現在の値が格納されている最大値より大きい場合は、その値を最大値として格納し、1〜4を下げ、5をリストから外します。そうでない場合は、2と比較して、同じことを行います。繰り返し、5つすべての保存された値と照合します。これはO(n)で実行する必要があります


その「バンプ」は、配列を使用している場合はO(n)であり、より良い構造を使用している場合はO(log n)(と思います)です。
カークストラウザー08/10/30

O(log k)である必要はありません-リストがリンクリストである場合、新しい要素を一番上に追加して最後の要素をドロップするのは、O(2)に似ています
Alnitak

バンプは、配列に基づくリストの場合はO(k)、適切にリンクされたリストの場合はO(1)になります。いずれにせよ、この種の質問は一般に、nと比較して影響が最小限であると想定しており、nの要素をこれ以上導入しません。
ボビンス2008年

バンプがリングバッファを使用する場合もO(1)になります
Alnitak

1
とにかく、コメントのアルゴリズムは不完全であり、新しい(たとえば)2番目に大きいnの要素を考慮することができません。nの各要素をハイスコア表の各要素と比較する必要がある最悪の場合の動作はO(kn)ですが、それでもおそらく質問ではO(n)を意味します。
ボビンス、2008年

1

私は一つの答えを提案したいと思います

最初のk個の要素を取り、それらをk個の値のリンクされたリストにソートする場合

最悪の場合でも他のすべての値について、残りのnk値に対して挿入ソートを実行すると、最悪の場合でも比較の数はk *(nk)になり、前のk値をソートするにはk *(k- 1)o(n)である(nk-k)となる

乾杯


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 2017

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

これは、ランダム化された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
ヘンガメ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での実装です。

配列を変更できないという制約を解除すると、2つのインデックスを使用して「現在のパーティション」を識別するための余分なメモリの使用を防ぐことができます(クラシッククイックソートスタイル-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としましょう。配列の中で3番目に大きいアイテムを見つけたいとします。3つの変数を作成し、配列の各項目をこれら3つの変数の最小値と比較します。配列項目が最小値より大きい場合、min変数を項目値で置き換えます。配列の最後まで同じことを続けます。3つの変数の最小値は、配列の3番目に大きい項目です。

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-smallestであるか、kth-smallestより小さいか、kth-smallestより大きいかを通知するコンパレーター関数を作成できます。k番目に小さい数に達するまでバイナリ検索を実行します

これはそのためのコードです

クラスSolution:

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

また、クイック選択アルゴリズムよりも優れたアルゴリズムが1つあります。それはフロイドリベット(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(基本的に、クイックセレクトとヒープセレクトのイントロセレクトハイブリッド)と比較しました。結果はクイック選択で、nth_elementは平均して同等に実行されましたが、FRアルゴリズムはおよそ実行されました。それらと比較して2倍の速さ。

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番目に大きい要素より大きくなる可能性がありますが、そのリストに追加されることはありません。これが機能するためには、アイテムのリストを昇順でソートする必要があります。
エリー

あなたは正しいです、私はこれをもう少し考えなければならないでしょうね。:-)
ジャスパーベッカーズ2008年

解決策は、eがhead(l)とtail(l)の間にあるかどうかを確認し、正しい場合は正しい位置に挿入することです。これをO(kn)にします。min要素とmax要素を追跡するバイナリツリーを使用する場合は、O(n log k)にすることができます。
Jasper Bekkers、2008年

-1

最初に、O(n)時間を要するソートされていない配列からBSTを構築し、BSTからO(log(n))のk番目に小さい要素を見つけることができます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.