2011-07-12 62 views
2

Top K最小選擇算法 - O(n + k log n)vs O(n log k)我對Top K算法提出這個問題。我認爲O(n + k log n)應該更快,因爲例如,如果嘗試插入k = 300和n = 100000000例如,我們可以看到O(n + k log n)更小。但是,當我使用C++進行基準測試時,它顯示出O(n log k)快了2倍以上。下面是完整的基準程序:對於k << N

#include <iostream> 
#include <vector> 
#include <algorithm> 
#include <iterator> 
#include <ctime> 
#include <cstdlib> 
using namespace std; 

int RandomNumber() { return rand(); } 
vector<int> find_topk(int arr[], int k, int n) 
{ 
    make_heap(arr, arr + n, greater<int>()); 

    vector<int> result(k); 

    for (int i = 0; i < k; ++i) 
    { 
     result[i] = arr[0]; 
     pop_heap(arr, arr + n - i, greater<int>()); 
    } 

    return result; 
} 

vector<int> find_topk2(int arr[], int k, int n) 
{ 
    make_heap(arr, arr + k, less<int>()); 

    for (int i = k; i < n; ++i) 
    { 
     if (arr[i] < arr[0]) 
     { 
    pop_heap(arr, arr + k, less<int>()); 
    arr[k - 1] = arr[i]; 
    push_heap(arr, arr + k, less<int>()); 
     } 
    } 

    vector<int> result(arr, arr + k); 

    return result; 
} 


int main() 
{ 
    const int n = 220000000; 
    const int k = 300; 

    srand (time(0)); 
    int* arr = new int[n]; 

    generate(arr, arr + n, RandomNumber); 

    // replace with topk or topk2 
    vector<int> result = find_topk2(arr, k, n); 

    copy(result.begin(), result.end(), ostream_iterator<int>(cout, "\n")); 


    return 0; 
} 

find_topk的做法是建立大小爲n的完整堆,在O(N),然後刪除的堆k倍澳頂部元件(log n)的。 find_topk2的方法是構建一個大小爲k(O(k))的堆,使得max元素位於頂部,然後從k位置到n,比較是否有任何元素小於頂部元素,如果是彈出頂部元素,並推入新的元素,這意味着n次O(log k)。 這兩種方法的寫法都非常相似,所以我不相信任何實現細節(如創建臨時對象等)都會導致算法以外的算法和數據集(隨機)。

我實際上可以剖析基準測試的結果,並且可以看到find_topk實際上比find_topk2多次調用比較運算符。但是我更關心理論複雜性的推理。所以有兩個問題。

  1. 無視執行或基準,期待O(n + k log n)應該比O(n log k)好嗎?如果我錯了,請解釋爲什麼以及如何推理,以便我可以看到O(n log k)實際上更好。
  2. 如果我沒有錯,期望沒有1.那麼爲什麼我的基準顯示不然?
+0

我希望你知道nth_element/partial_sort算法,並只爲了好玩。 –

+0

'partial_sort' /'partial_sort_copy'在這個syntetic測試中的運行時間與您的'find_topk2'完全相同 – sehe

+0

yes我知道find_topk2可以通過partial_sort進行替換。但我正在這樣做,使算法更加明確。 – ryaner

回答

4

因爲您需要假設您的變量如何相互縮放,所以您可以毫無疑義地將極限無窮大。

如果例如。 (n log k)變爲O(n log n),並且O(n + k log n)變爲O(n + n ^(1/2)log n)= O (n),哪個更好。如果k〜log n,則O(n log k)= O(n log log n)和O(n + k log n)= O(n),這是更好的。請注意,日誌日誌2^1024 = 10,因此隱藏在O(n)中的常量可能大於任何實際的n的日誌日誌n。如果k =常數,則O(n log k)= O(n)和O(n + k log n)= O(n),這是相同的。

但是常量起着很大的作用:例如,構建堆可能涉及讀取數組3次,而構建長度爲k的優先級隊列只需要一次通過數組,並且一個小的常量時間記錄k用於查找。因此,哪個「更好」是不清楚的,儘管我的快速分析傾向於表明O(n + k log n)在對k的溫和假設下表現更好。例如,如果k是一個非常小的常量(比如說k = 3),那麼我就準備好打賭make_heap方法比真實世界數據上的優先級隊列更差。

明智地使用漸近分析,最重要的是,在繪製結論之前剖析您的代碼。

1

您正在比較兩個最差情況的上限。對於第一種方法,最壞的情況幾乎與平均情況相同。對於第二種情況,如果輸入是隨機的,那麼當你將一堆以上的物品放入堆中時,由於它不會取代任何最上面的K,所以立即扔掉新值的機會是相當高,所以對此的最壞情況估計是悲觀的。

如果您比較掛鐘時間而不是比較,您可能會發現堆積大堆的算法往往不會贏得很多種族,因爲它們具有可怕的存儲局部性 - 而現代微處理器上的常量因素很大程度上受到什麼級別的影響你最終努力工作的內存 - 發現你的數據在真正的內存芯片中(或者更糟糕的是,在磁盤上),而不是一定程度的緩存會讓你減慢很多 - 這是一種遺憾,因爲我真的很喜歡堆排序。

0

請記住,您現在可以使用std :: nth_element,而不必使用堆並自己執行操作。由於默認的比較運算符是std :: less <>(),所以可以這樣說:

std :: nth_element(myList.begin(),myList.begin()+ k,myList.end() );

現在,myList從位置0到k將是最小的k個元素。

相關問題