在 n 個數當中找第k小元素 (BFPRT算法,最壞情況為線性時間的選擇問題)


題目描述

問題描述:

        在 n 個數當中找第k小元素。

輸入:

        第一行輸入n的值,第二行輸入n個數,第三行輸入k的值。

輸出:

       n 個數中的第k小元素。

要求

       你的算法最壞情況下應該在線性時間內完成。

示例1 

輸入:

5

8 1 3 6 9

3

輸出: 6

 

示例 2

輸入:

10

72 6 57 88 60 42 83 73 48 85

5

輸出: 60

 

思路分析

       對於常規解法,我們隨機在數組中選擇一個數作為划分值(pivot),然后進行快排的partation過程(將小於pivot的數放到數組左邊,大於pivot的數放到數組右邊),划分完之后pivot的下標為i,然后判斷k與等於i的相對關系,如果k正好在等於i,那么數組第k小的數就是pivot,如果k小於i,那么我們遞歸對左邊再進行上述過程,如果k大於i,那我們遞歸對右邊再進行上述過程。常規解法的應用及代碼實現見這篇文章

        對於最好的情況:每次所選的pivot划分之后正好在數組的正中間,那么遞歸方程為T(n) = T(n/2) + n,解得T(n) = O(n),所以此時此算法是O(n)線性復雜度的。

        對於最壞情況:每次所選的pivot划分之后都好在數組最邊上,那么時間復雜度為O(n2)。

       BFPRT算法就是在這個pivot上做文章,BFPRT算法能夠保證每次所選的pivot划分之后在數組的中間位置,那么時間復雜度就是O(n)。

BFPRT算法流程

       這題規定了要在線性時間內完成第k小元素的選擇,在算法導論這本書里面的第九章有講解過這種問題,算法的基本思想是修改快速排序算法中的主元選取方法,降低算法在最壞情況下的時間復雜度。

  下述步驟來自《算法導論(第3版)》第9.3節。

       在快速排序中,我們始終選擇第一個元素或者最后一個元素作為pivot,而在此算法中,每次選擇五分中位數的中位數作為pivot,這樣做的目的就是使得划分比較合理,從而避免了最壞情況的發生。通過執行下列步驟,算法Select可以確定一個有個不同元素的輸入數組中第i小的元素:

(1) 將n個元素划為組,每組5個,至多只有一組由剩下的n mod 5個元素組成。

(2) 尋找這個組中每一個組的中位數,這個過程可以用插入排序,然后確定每組有序元素的中位數。  

(3) 對第2步中找出的個中位數,重復步驟1和步驟2,遞歸下去,直到剩下一個數字。

(4) 最終剩下的數字即為主元pivot,用快速排序的划分思想,把小於pivot的數全放左邊,大於它的數全放右邊。跟快速排序不同的是,這里只是划分,並沒有排序。

(5) 判斷pivot的位置與k的大小,有選擇的對左邊或右邊遞歸。

  1 #include <iostream>
  2 #include <string.h>
  3 #include <stdio.h>
  4 #include <time.h>
  5 #include <algorithm>
  6  
  7 using namespace std;
  8  
  9 //插入排序
 10 void InsertSort(int a[], int l, int r)
 11 {
 12     for(int i = l + 1; i <= r; i++)
 13     {
 14         if(a[i - 1] > a[i])
 15         {
 16             int t = a[i];
 17             int j = i;
 18             while(j > l && a[j - 1] > t)
 19             {
 20                 a[j] = a[j - 1];
 21                 j--;
 22             }
 23             a[j] = t;
 24         }
 25 }
 26 }
 27  
 28 //尋找中位數的中位數
 29 int FindMid(int a[], int l, int r)
 30 {
 31     if(l == r) return l;
 32     int i = 0;
 33     int n = 0;
 34     for(i = l; i < r - 5; i += 5)
 35     {
 36         InsertSort(a, i, i + 4);
 37         n = i - l;
 38         //插入排序之后,a[i+2]就是a[i,...,i+5]的中位數
 39         //把中位數都放到前面 
 40         swap(a[l + n / 5], a[i + 2]);
 41     }
 42  
 43     //處理剩余元素
 44     int num = r - i + 1;
 45     if(num > 0)
 46     {
 47         InsertSort(a, i, i + num - 1);
 48         n = i - l;
 49         swap(a[l + n / 5], a[i + num / 2]);
 50     }
 51     n /= 5;
 52     if(n == l) 
 53         return l;
 54     
 55     //前n個數就是上述找出來的每一組的中位數 
 56     return FindMid(a, l, l + n);
 57 }
 58  
 59 //進行划分過程,就是一趟快速排序的過程,返回划分后的基准數的下標i 
 60 int Partition(int a[], int l, int r, int p)
 61 {
 62     swap(a[p], a[l]);
 63     int i = l;
 64     int j = r;
 65     int pivot = a[l];
 66     while(i < j)
 67     {
 68         while(a[j] >= pivot && i < j)
 69             j--;
 70         while(a[i] <= pivot && i < j)
 71             i++;
 72         swap(a[j], a[i]);
 73     }
 74     swap(a[l], a[i]);
 75     
 76     return i;
 77 }
 78  
 79 int Select(int a[], int l, int r, int k)
 80 {
 81     int p = FindMid(a, l, r);       //尋找中位數的中位數
 82     int i = Partition(a, l, r, p);  //划分之后的下標 
 83  
 84     int m = i - l + 1;
 85     if(m == k) 
 86         return a[i];
 87     if(m > k)  
 88         return Select(a, l, i - 1, k);
 89         
 90     return Select(a, i + 1, r, k - m);
 91 }
 92  
 93 int main()
 94 {
 95     int n, k;
 96     scanf("%d", &n);
 97     int *a = new int[n];
 98     for(int i = 0; i < n; i++)
 99         scanf("%d", &a[i]);
100     scanf("%d", &k);
101     printf("%d", Select(a, 0, n - 1, k));
102     
103     delete[] a;
104     return 0;
105 }

 

復雜度分析

 

思考與引申

        快速排序的 Partition 划分思想可以用於計算某個位置的數值等問題,可以實現 O(n)復雜度的選擇問題,之所以這種選擇算法具有線性時間,是因為沒有進行排序,並且每次都有選擇的只對左右其中的一邊進行遞歸處理,而排序需要進行比較,並且快速排序左右兩邊都需要進行遞歸處理,即使是在平均情況下,排序也需要 O(nlogn)的時間復雜度,而這個線性時間的選擇算法沒有使用排序就解決了選擇問題。

優缺點

        但缺點也很明顯,最主要的就是內存問題,在海量數據的情況下,很有可能沒辦法一次性將數據全部加載入內存,這個時候這個方法就無法完成使命了。此時可以利用堆來解決,維護一個大小為 K 的小頂堆,依次將數據放入堆中,當堆的大小滿了的時候,只需要將堆頂元素與下一個數比較:如果大於堆頂元素,則將當前的堆頂元素拋棄,並將該元素插入堆中。遍歷完全部數據,Top K 的元素也自然都在堆里面了。但是使用堆解決這個問題,時間花費為 O(nlogk)。

 

參考

《算法導論 (第3版)》 第9.3節

bfprt算法解析

知乎 - BFPRT算法原理

 

相關習題

劍指 Offer 40. 最小的k個數


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM