在一個給定的亂序的序列中找到第k個數字,可能會想到先排序,然后輸出第k個數。這種方法簡單粗暴,時間復雜度為O(nlogn)。
還有一種方法是快速選擇,它的思想和快速排序很相似。就是先選擇一個數x,然后把這個序列分成左右兩邊,其中左邊的所有的數都<=x,右邊的數都>=x。然后比較左邊數字的個數leftNum與k的大小。如果k <= leftNum,說明我們要找到第k個數在調整后的序列的左邊那部分;否則就是在右邊的那部分。然后再用相同的方法遞歸那部分來找到第k個數。
我們在這個序列中隨便找到一個數x,接下來和快速排序一樣,把這個序列調整為左邊的部分的數都<=x,右邊部分的數都>=x。
其中指針j就為划分左右兩邊的下標位置,j的左邊(包括j這個位置)就是左部分,右邊就是右部分。
接下來計算左邊部分元素的個數leftNum = j - left + 1,與k進行比較:
- 如果k <= leftNum,說明我們要找的第k個數就在左邊部分,遞歸去在左邊部分找第k個數,尋找序列的下標范圍是[left, j];
- 否則如果k > leftNum,說明我們要找的第k個數在右邊部分,這時應該是遞歸去在右邊部分找第k - leftNum個數,相應的尋找序列的下標范圍應該為[j + 1, right]。
快速選擇算法的時間復雜度為O(n)。
可以試想假設序列有n個元素,開始的第一次尋找次數為n,然后把序列分成左右兩部分,有相關證明兩邊的期望大小各為為n/2,我們要在其中的一部分找。然后同樣的方法又分成兩部分,為n/4。以此類推,所以有
\[n + \frac{n}{2} + \frac{n}{4} + ... = n (1 + \frac{1}{2} + \frac{1}{4} + ...) = n\lim_{m->\infty }\sum_{i = 0}^{m} \frac{1}{2^{i}} = 2n\]
所以時間復雜度就為O(n)了。
相應的代碼就通過一道模板題來給出吧。
786. 第k個數
給定一個長度為 n 的整數數列,以及一個整數 k,請用快速選擇算法求出數列從小到大排序后的第 k 個數。
輸入格式
第一行包含兩個整數 n 和 k。
第二行包含 n 個整數(所有整數均在 1∼109 范圍內),表示整數數列。
輸出格式
輸出一個整數,表示數列的第 k 小數。
數據范圍
1≤n≤100000,
1≤k≤n
輸入樣例:
5 3 2 4 1 5 3
輸出樣例:
3
AC代碼如下:
1 #include <cstdio> 2 #include <algorithm> 3 using namespace std; 4 5 int solve(int *a, int left, int right, int k) { 6 if (left == right) return a[left]; // left == right,說明只有一個數字,這個數字就是我們要找到的第k個數 7 int x = a[left + right >> 1], i = left - 1, j = right + 1; 8 while (i < j) { 9 while (a[++i] < x); 10 while (a[--j] > x); 11 if (i < j) swap(a[i], a[j]); 12 } 13 14 int leftNum = j - left + 1; 15 if (k <= leftNum) return solve(a, left, j, k); 16 else return solve(a, j + 1, right, k - leftNum); 17 } 18 19 int main() { 20 int n, k; 21 scanf("%d %d", &n, &k); 22 int a[n]; 23 for (int i = 0; i < n; i++) { 24 scanf("%d", a + i); 25 } 26 printf("%d", solve(a, 0, n - 1, k)); 27 28 return 0; 29 }
參考資料
AcWing 786. 第k個數:https://www.acwing.com/video/228/