2015-03-19 457 views
11

有很多在計算器上索賠和其他地方nth_elementO(n)的,它通常與Introselect實現:http://en.cppreference.com/w/cpp/algorithm/nth_elementnth_element是如何實現的?

我想知道如何可以做到這一點。我看着Wikipedia's explanation of Introselect,這讓我更加困惑。算法如何在QSort和Mediology中間值之間切換?

我發現內省排序文件位置:http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.14.5196&rep=rep1&type=pdf但是,上面寫着:

在本文中,我們專注於分類問題,在後面的部分返回到選擇問題只是暫時。

我試過通過STL本身來了解nth_element是如何實現的,但那真是太快了。

有人可以向我展示如何實現Introselect的僞代碼?或者甚至更好,實際的C++代碼,當然除了STL以外:)

+0

您是否注意到根據cppreference,算法在平均情況下只應該是'O(n)'。它沒有聲明最壞的情況。這意味着快速選擇將是可行的,因爲它平均具有「O(n)」。 – Nobody 2015-03-20 09:05:08

+0

@沒有人可以指出除了使用Introselect以外的東西可能不是最壞的情況* O(n)*。維基百科和Introselect論文聲稱Introselect在*最糟糕的情況下是* O(n)*。 – 2015-03-20 11:22:08

+1

正如你所說,introselect是快速選擇和中位數的組合。 QuickSelect在平均和最好的情況下速度非常快,但是最差的情況是二次方,而中位數的中位數保證是總是線性時間算法的稍慢。 Introselect通過快速選擇嘗試快速運行,如果不能運行,則會回退到速度較慢但保證的線性時間算法,從而限制其最壞情況運行時間,使其在變得比線性更差之前。也許你應該澄清一下你的問題,因爲看起來你對這個問題有疑惑:STL,Introsort,Complexities? – Nobody 2015-03-20 12:02:14

回答

10

你問了兩個問題,有名無實的一個

如何nth_element實施?

,你已經回答了:

有很多在計算器上索賠和其他地方nth_element是O(n),它通常與Introselect實現。

我也可以通過查看我的stdlib實現來確認。 (更多關於這個版本)。

而且一個地方,你不明白的答案:

如何快速排序中位數中位數的和之間的算法開關?

讓我們看看在僞代碼,我從我的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_selectunguarded_partition_pivot,我們可以清楚地看到細節,即nth_element給introselect 2 * log2(size)細分步驟(在快速選擇最佳情況下需要兩倍),直到heap_select開始並解決問題。

+1

請注意,提到的libstdC++實現實際上並不是內定的,儘管代碼命名如此。 Introselect回落到O(n)中位數中值方法,而該實現回退到基於O(nlogn)堆的解決方案。 (更多信息見這個錯誤報告:https://gcc.gnu.org/bugzilla/show_bug.cgi?id = 35968) – Azmisov 2016-11-29 22:24:31

+0

@Azmisov:這有點涉及[我的舊評論](https://stackoverflow.com /問題/ 29145520 /如何-IS-第n個元件實現的/ 29146718?noredirect = 1個#comment46544991_29145520)。無論如何,謝謝你指出。雖然它是在代碼中清楚地寫出來的,但毫無戒心的讀者可能會忽略這樣一個事實:heap_select是O(n log n) – Nobody 2016-11-29 23:04:11

5

聲明:我不知道std::nth_element是如何在任何標準庫中實現的。

如果您知道Quicksort的工作方式,您可以輕鬆修改它以完成此算法所需的工作。 Quicksort的基本思想是在每一步中,將數組分成兩部分,使得所有小於主元的元素都在左子數組中,並且所有等於或大於主元的元素都在右子數組中。 (修改Quicksort稱爲三元Quicksort,創建第三個子數組,其元素與主元相等,然後右子數組只包含嚴格大於主元的元素。)然後Quicksort遞歸排序左和右子元素-arrays。

如果你只想在ñ個元素移動到位,而不是遞歸到子陣列,你可以在每一步告訴你是否需要將下降到左邊或右邊子陣列。 (你知道這是因爲n排序數組中的第n個元素的索引爲n所以它成爲一個比較索引的問題。)所以 - 除非你的Quicksort受到最壞情況的退化 - 你將剩下的大小減半數組在每一步。 (你從來不看其他子陣列一次。)因此,平均來說,你正在處理中的每個步驟以下長度的數組:

  1. Θ(ñ
  2. Θ(ñ/2)
  3. Θ(ñ/4)
  4. ...

每一步在處理數組的長度上都是線性的。 (你循環一次,並根據它與樞軸的比較來決定每個元素應該去哪個子數組。)

你可以看到,在經過Θ(log(N))步後,我們最終會到達一個單例數組並完成。如果你總結N(1 + 1/2 + 1/4 + ...),你會得到2 N。或者,在平均情況下,因爲我們不能希望這個關鍵點總是正好是中位數,所以這個數字大約是Θ(N)。

+0

我假設你知道Quicksort是* O(nlogn)*? http://en.wikipedia.org/wiki/Quicksort這會希望如何實現比* O(nlogn)*性能更好的效果? – 2015-03-20 11:58:32

+1

@JonathanMee:排序需要對分區的兩側進行排序,而選擇只需要在所選元素所在的一側工作。因此,排序必須適用於所有'n'元素'log n'次,而選擇只需處理收縮子集(如上面的答案中所述)。 – Nobody 2015-03-20 12:08:58

+0

@Nobody有趣的是,我研究了這個,實際上這是Introsort實現之前的事情。然而,gcc和Visual Studio現在已經進入了Introsort。大概是因爲一個糟糕的選擇,Quicksort急需一個不好的地方。 – 2015-03-20 12:12:21

6

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東西,這只是爲了保護用戶可以合法定義爲宏的代碼。我還刪除了最後一個參數,該參數僅用於模板類型推導,並且爲了簡潔起見將其重命名爲迭代器類型。

正如您現在應該看到的那樣,它會重複劃分範圍,直到剩餘範圍內剩餘的元素少於四個,然後對其進行簡單排序。

現在,爲什麼是O(n)?首先,最多三個元素的最終排序是O(1),因爲最多三個元素。現在,剩下的是重複分區。分區本身是O(n)。儘管如此,每一步都會減少下一步需要觸及的元素的數量,因此您有O(n)+ O(n/2)+ O(n/4)+ O(n/8),這就是小於O(2n),如果你總結。由於O(2n)= O(n),因此平均具有線性複雜性。

+1

請注意,這似乎是一個純粹的快速排序實現,沒有算法切換進行,因此它不是內部選擇。 – Nobody 2015-03-20 08:44:06

+0

我不明白你想說什麼,@沒人。 – 2015-03-20 08:47:35

+0

你回答標題,但不是問題'有人可以向我展示如何實現Introselect的僞代碼?或者更好的是,實際的C++代碼當然不是STL:)'。除此之外,我認爲我不清楚每個分區的大小是多少。我不確定'unguarded_pa​​rtition',但是選定的pivot只是當前範圍的第一個,中間和最後一個元素的中間值,它只能保證每個元素至少有一個元素位於該元素的每一側,因此需要線性分割很多分區腳步。 – Nobody 2015-03-20 08:54:35