1. 程式人生 > 實用技巧 >在 n 個數當中找第k小元素 (BFPRT演算法,最壞情況為線性時間的選擇問題)

在 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(nlogn)。

參考

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

bfprt演算法解析

知乎 -BFPRT演算法原理

相關習題

劍指 Offer 40. 最小的k個數