web-dev-qa-db-ja.com

nth_elementはどのように実装されますか?

StackOverflowや他の場所では、nth_elementO(n)であり、通常はIntroselectで実装されています: http://en.cppreference.com/w/cpp/algorithm/nth_element

これがどのようにして実現できるのか知りたい。私は WikipediaによるIntroselectの説明 を見て、混乱しました。アルゴリズムはどのようにQSortとMedian-of-Mediansを切り替えることができますか?

私はここにIntrosortペーパーを見つけました: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.14.5196&rep=rep1&type=pdf しかし、それは言う:

このホワイトペーパーでは、ソートの問題に集中し、後のセクションで簡単に選択の問題に戻ります。

私はSTL自体を読んで、nth_elementが実装されていますが、これは非常に高速です。

誰かがIntroselectの実装方法の疑似コードを見せてくれませんか?またはもちろん、STL以外の実際のC++コード:)

22
Jonathan Mee

あなたは2つの質問をしました

Nth_elementはどのように実装されますか?

あなたはすでに答えました:

StackOverflowやその他の場所では、nth_elementがO(n)であり、通常はIntroselectで実装されているとのことです。

これは、私のstdlib実装を見ても確認できます。 (これについては後で詳しく説明します。)

そして、あなたが答えを理解していないもの:

アルゴリズムはどのようにQSortとMedian-of-Mediansを切り替えることができますか?

Stdlibから抽出した疑似コードを見てみましょう。

_nth_element(first, nth, last)
{ 
  if (first == last || nth == last)
    return;

  introselect(first, nth, last, log2(last - first) * 2);
}

introselect(first, nth, last, depth_limit)
{
  while (last - first > 3)
  {
      if (depth_limit == 0)
      {
          // [NOTE by editor] This should be median-of-medians instead.
          // [NOTE by editor] See Azmisov's comment below
          heap_select(first, nth + 1, last);
          // Place the nth largest element in its final position.
          iter_swap(first, nth);
          return;
      }
      --depth_limit;
      cut = unguarded_partition_pivot(first, last);
      if (cut <= nth)
        first = cut;
      else
        last = cut;
  }
  insertion_sort(first, last);
}
_

参照される関数_heap_select_および_unguarded_partition_pivot_の詳細については説明しませんが、_nth_element_はintroselect 2 * log2(size)サブディビジョンステップを提供します(最良のケース)_heap_select_が起動して問題を完全に解決するまで。

15
Nobody

免責事項:std::nth_elementは標準ライブラリに実装されています。

Quicksortの仕組みがわかっている場合は、Quicksortを簡単に変更して、このアルゴリズムに必要なことを行うことができます。 Quicksortの基本的な考え方は、各ステップで配列を2つの部分に分割し、ピボットよりも小さいすべての要素が左側のサブ配列にあり、ピボット以上のすべての要素が右側のサブ配列にあるようにすることです。 。 (3項クイックソートと呼ばれるクイックソートの変更により、すべての要素がピボットと等しい3番目のサブ配列が作成されます。次に、右側のサブ配列には、ピボットよりも厳密に大きいエントリのみが含まれます。)次に、クイックソートは、左と右のサブ-配列。

bothサブ配列に再帰するのではなく、n-番目の要素を所定の場所に移動するだけの場合は、すべてのステップで、左または右のサブ配列。 (ソート済み配列のn-番目の要素にはインデックスnがあるため、これを知っているので、インデックスを比較することが問題になります。)したがって– Quicksortが最悪の場合を除いて縮退–各ステップで残りのアレイのサイズをおよそ半分にします。 (他のサブ配列を二度と見ることはありません。)したがって、平均して、各ステップで次の長さの配列を処理しています。

  1. Θ([〜#〜] n [〜#〜]
  2. Θ([〜#〜] n [〜#〜]/2)
  3. Θ([〜#〜] n [〜#〜]/4)

各ステップは、処理する配列の長さに対して線形です。 (一度ループして、ピボットとの比較方法に応じて、各要素をどのサブ配列に配置するかを決定します。)

Θ(log([〜#〜] n [〜#〜]))ステップの後で、最終的にシングルトン配列に到達して完了していることがわかります。合計すると[〜#〜] n [〜#〜](1 + 1/2 + 1/4 +…)、2になります[〜#〜] n [〜#〜]。または、平均的なケースでは、ピボットが常に正確に中央値になることを期待できないため、something([〜#〜] n [〜#〜]のようなものになります。

12
5gon12eder

[〜#〜] stl [〜#〜] (バージョン3.3だと思います)のコードは次のとおりです。

template <class _RandomAccessIter, class _Tp>
void __nth_element(_RandomAccessIter __first, _RandomAccessIter __nth,
                   _RandomAccessIter __last, _Tp*) {
  while (__last - __first > 3) {
    _RandomAccessIter __cut =
      __unguarded_partition(__first, __last,
                            _Tp(__median(*__first,
                                         *(__first + (__last - __first)/2),
                                         *(__last - 1))));
    if (__cut <= __nth)
      __first = __cut;
    else 
      __last = __cut;
  }
  __insertion_sort(__first, __last);
}

それを少し単純化しましょう:

template <class Iter, class T>
void nth_element(Iter first, Iter nth, Iter last) {
  while (last - first > 3) {
    Iter cut =
      unguarded_partition(first, last,
                          T(median(*first,
                                   *(first + (last - first)/2),
                                   *(last - 1))));
    if (cut <= nth)
      first = cut;
    else 
      last = cut;
  }
  insertion_sort(first, last);
}

ここで私が行ったのは、二重下線と_Uppercaseなどを削除することでした。これは、ユーザーがマクロとして合法的に定義できるものからコードを保護するためだけです。また、テンプレートタイプの推定に役立つはずの最後のパラメーターを削除し、簡潔にするためにイテレータータイプの名前を変更しました。

ご覧のとおり、残りの範囲に残る要素が4つ未満になるまで範囲を繰り返し分割し、単純に並べ替えます。

さて、なぜO(n)なのでしょうか。まず、最大3つの要素があるため、最大3つの要素の最終的な並べ替えはO(1)です。ここで残っているのは、繰り返されるパーティショニングです。パーティション化自体はO(n)です。ただし、ここでは、すべてのステップで次のステップでタッチする必要がある要素の数が半分になるため、O(n) + O(n/2) + O(n/4) + O(n/8)は、合計するとO(2n)未満です。 O(2n) = O(n)なので、平均的には線形の複雑さになります。

8
Ulrich Eckhardt