【LeetCode & 劍指offer刷題】查詢與排序題2:40 最小的k個數(對應Kth Largest Element in an Array)
阿新 • • 發佈:2019-01-05
【LeetCode & 劍指offer 刷題筆記】目錄(持續更新中...)
40 最小的k個數
題目描述
輸入n個整數,找出其中最小的K個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4, /* //暴力法:sort, O(nlogn) //方法一:使用自帶的stl函式 #include <algorithm> using namespace std;C++ 暴力法:直接sort,O(nlogn),leetcode用時12ms /*掌握 方法一:基於partition函式(快排中有用到,stl中也有,但是還是自己實現較好) 多次partition直到樞軸位置為k即可 缺點:會改變輸入陣列的元素位置 leetcode 耗時4ms,若用pivot = left的做法,則耗時20ms 平均O(n),O(1) 每次partition 平均O(n),次數未知,真的是O(n)嗎,存疑? 思考:最好情況為O(n),最壞情況為O(n^2)(如對於倒序排列的陣列) 通過優化partition,比如三數中值樞軸法或隨機初始化樞軸法,可以改善時間複雜度 */ #include <cstdlib> class Solution { public : int findKthLargest ( vector < int >& a , int k ) { if ( a . empty () || k <= 0 || k > a . size ()) return 0 ; //處理異常輸入 int left = 0 , right = a . size ()- 1 ; int pivot_pos ; while ( left <= right ) //類似二分查詢法 { pivot_pos = partition ( a , left , right ); //如果要求最大的第k個數,可以對partition函式進行改造 if ( pivot_pos < k - 1 ) left = pivot_pos + 1 ; else if ( pivot_pos > k - 1 ) right = pivot_pos - 1 ; else return a [ pivot_pos ]; } } private : int partition ( vector < int >& a , int left , int right ) { srand ( time ( nullptr )); //以當前時間為隨機生成器的種子 int pivotpos = rand ()%( right - left + 1 ) + left ; //產生【left,right】之間的數 swap ( a [ pivotpos ], a [ left ]); //將樞軸暫時放入起始位置 int pivot = left ; //樞軸位置 while ( left < right ) { //改造為從大到小partition,注意符號的變化 while ( left < right && a [right] <= a[pivot ]) right --; //找到本次掃描中第一個不滿足樞軸規律的高位數 while ( left < right && a [ left ] >= a [ pivot ]) left ++; //找到本次掃描中第一個不滿足樞軸規律的低位數 swap ( a [ left ], a [ right ]); //交換以使滿足樞軸規律 } //最後結果是left和right均指向樞軸位置 swap ( a [ left ], a [ pivot ]); //將樞軸移動到位 return left ; //返回樞軸位置 } }; 方法二:維護一個堆或者平衡二叉查詢樹儲存這k個數 /*掌握 改進:維護一個大小為k的 小頂堆 ,掃描輸入資料,不斷更新小頂堆的內容 最後堆頂元素即可n個數中第k大的數 leetcode耗時4ms 每次堆調整平均時間複雜度為O(logk),共n次調整,故時間複雜度為O(nlogk) O(nlogk), O(k) */ #include <algorithm> #inlude <queue> class Solution { public : int findKthLargest ( vector < int >& a , int k ) { if ( a . empty () || k <= 0 || k > a . size ()) return 0 ; //處理非法輸入(可依題目返回適當值) priority_queue < int , vector < int >, greater < int >> minheap ; //構建小頂堆 for ( int i = 0 ; i < a . size (); i ++) { if ( i <= k - 1 ) minheap . push ( a [ i ]); //將前k個元素插入容器 else { if ( a[i] > minheap.top ()) //如果當前元素大於容器中最小的元素,則將該元素push進容器 { minheap . pop (); minheap . push ( a [ i ]);//每次堆調整複雜度為O(logk) } } } return minheap . top (); //返回堆頂元素,即為n個數中第k大的數(如果要返回前k個數,需將最後的minheap全部pop) } }; /*瞭解 用stl中make_heap函式,構建大頂堆,然後逐次輸出堆頂元素 (原理與用priority_queue相同,不過沒有額外空間) 用時11ms O(nlogn + klogn),O(1) */ #include <algorithm> class Solution { public : int findKthLargest ( vector < int >& nums , int k ) { make_heap ( nums . begin (), nums . end ()); //構建大頂堆, 用nums儲存,與下面區別就是節省了空間 for ( int i = 0 ; i < k - 1 ; i ++) { pop_heap ( nums . begin (), nums . end ()); //將堆頂元素移至末尾,重新調整使(begin~end-1)的元素滿足堆規律 nums . pop_back (); //移除末尾元素 } return nums [ 0 ]; } }; //用stl中sort函式,用時12ms /* #include <algorithm> class Solution { public: int findKthLargest(vector<int>& nums, int k) { sort(nums.rbegin(), nums.rend()); //nums.rbegin()返回指向容器最後元素的逆向迭代器(因為sort預設按從小到大排序), //sort將rbegin()指向位置當做第一個元素,故可以實現從大到小排序 return nums[k-1]; //第k個數,注意這裡索引為k-1 } }; */ /* //用stl中nth_element函式。?為什麼沒有sort()或者堆排序快 //用時14ms #include <algorithm> class Solution { public: int findKthLargest(vector<int>& nums, int k) { //nth_element(nums.begin(), nums.begin()+k-1, nums.end(),customMore); nth_element(nums.begin(), nums.begin()+k-1, nums.end(),greater<int>()); //原理為快排 //這裡直接用STL裡的函式,比較函式設定為greater(預設為小數在前),注意中間(k-1)表示第k個最大的數 return nums[k-1]; //第k個數,注意這裡索引為k-1 } }; */ /* // 用自定義函式物件排序 struct { bool operator()(int a, int b) const { return a > b; } }customMore; */ 相關:
Top K問題的兩種解決思路
Top K問題在資料分析中非常普遍的一個問題(在面試中也經常被問到),比如:從20億個數字的文字中,找出最大的前100個。解決Top K問題有兩種思路,
- 最直觀:小頂堆(大頂堆 -> 最小100個數), 該方法沒有修改輸入資料,且非常適合海量資料的輸入,不用一次性讀入記憶體,可以藉助硬碟一邊讀一邊處理,平均時間複雜度為O(nlogk)。
- 較高效:基於partition函式的解法, 平均時間複雜度為O(n),但是會修改輸入陣列。
- 若切分後的左子陣列的長度 > k,則第k大元素必出現在左子陣列中;
- 若切分後的左子陣列的長度 = k-1,則第k大元素為pivot;
- 若上述兩個條件均不滿足,則第k大元素必出現在右子陣列中。