1. 程式人生 > >分治演算法 求第k小元素 O(n) & O(nlog2^n)

分治演算法 求第k小元素 O(n) & O(nlog2^n)

訪問請移步至(David W. QING) https://www.qingdujun.com/ ,這裡有能“擊穿”平行宇宙的亂序並行位元組流...
---

最容易想到的演算法是採用一種排序演算法先將陣列按不降的次序排好,然後從排好序的陣列中撿出第k個元素。這樣的演算法在最壞情況下時間複雜度是O(nlog2^n)。

實際上,我們可以設計出在最壞情況下的時間複雜度為O(n)的演算法。

利用分治演算法並結合快排思想,很容易達到O(n)的時間複雜度。其核心思想在於快排中基準的選取。(根據嚴蔚敏版教材,一般直接選取第一個元素作為快排基準。但求第k小元素,則依賴於一種中值選取法,以加速剪枝)。

下面舉個例子,如何達到O(n)選取第k小的元素。

問題:如何在O(n)內,確定A[17]中第k小的元素? A[17] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

這裡給出一個分治演算法,選取基準的求解過程。

step1 設定一個值r,將A[15]分為長度為r的幾個組。(假設r = 5)

A[17] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };

                     G1               G2                   G3                  G4

分組結果,G1[5] = {0, 1, 2, 3, 4} ;G2[5] = {5, 6, 7, 8, 9};G3[5] = {10, 11, 12, 13, 14};G4[2] = {15, 16}。這裡注意第四組,只有2個元素。

step2 分別求取G1~4組別中的中值,G1 = 2, G2 = 7, G3 = 12, G4 = 15。

並由這四個數再組成一個數組G[4] = {2, 7, 12, 15}。

step3 求得G[4] = {2, 7, 12, 15}陣列中的中值,mid = 7。

那麼,mid = 7便是最終選取出來的快排劃分基準。

以下程式中,partition函式(第13行)以上部分即為上述基準選取過程。這裡需要說明的是,求得中值後,程式中並沒有開闢額外空間,而是在原有A[]基礎上進行操作的,將中值放在最前面,故需要swap以保證資料資訊不丟失。

//A[low..high]
int select_rank_k(int A[], int low, int high, int k)
{
	int r_group = ceil((high - low + 1)*1.0 / r);//ceil取上限,總共分為r_group個組
	//計算每個分組中值,存於A[]最前面
	for (int i = 1; i <= r_group; ++i) {
		sort(&A[low + (i - 1)*r], &A[(low + i*r - 1) > high ? high : (low + i*r - 1)]);
		swap(A[low + i - 1], A[low + (i-1)*r + r / 2]);
	}
	//獲得每個組的中值的中值(並置於A[low]位置,方便呼叫快排劃分函式)
	sort(&A[low], &A[low + r_group]);
	swap(A[low], A[r_group / 2]);
	int cur = partition(A, low, high);
	if (cur == k-1){
		return A[cur];
	}
	else if (cur < k){
		return select_rank_k(A, cur + 1, high, k);
	}
	else{
		return select_rank_k(A, low, cur - 1, k);
	}
}

2018年6月4日修正

swap(A[low], A[r_group / 2]); 應該更改為swap(A[low], A[low+r_group / 2]);

程式中其它執行步的時間複雜度都至多是n的倍數。如果用T(n)表示演算法在陣列長度為n的時間複雜度,則當n≥24時,有遞迴關係

其中c是常數。從上述遞推關係式出發,用數學歸納法可以證明,

所以,在最壞情況下,select_rank_k演算法的時間複雜度是O(n)。

最後,對整個問題抽象以下,並給出完整DEMO。問題:已知n元陣列A[1..n],試確定其中第k小的元素。

補充:36行需修正為swap(A[low], A[low+r_group / 2]);

#include <stdio.h>
#include <algorithm>
#include <math.h>
using namespace std;

//劃分——每次劃分唯一確定一個元素位置
int partition(int A[], int low, int high)
{
	int pivot = A[low];    //一般採用嚴蔚敏教材版本,以第1個位置為基準
	while (low < high){
		while (low < high && A[high] >= pivot){
			--high;
		}
		A[low] = A[high];  //將比基準小的元素移動到左端
		while (low < high && A[low] <= pivot){
			++low;
		}
		A[high] = A[low];  //將比基準小的元素移動到右端
	}
	A[low] = pivot;
	return low;
}

int r = 5;
//A[low..high]
int select_rank_k(int A[], int low, int high, int k)
{
	int r_group = ceil((high - low + 1)*1.0 / r);//ceil取上限,總共分為r_group個組
	//計算每個分組中值,存於A[]最前面
	for (int i = 1; i <= r_group; ++i) {
		sort(&A[low + (i - 1)*r], &A[(low + i*r - 1) > high ? high : (low + i*r - 1)]);
		swap(A[low + i - 1], A[low + (i-1)*r + r / 2]);
	}
	//獲得每個組的中值的中值(並置於A[low]位置,方便呼叫快排劃分函式)
	sort(&A[low], &A[low + r_group]);
	swap(A[low], A[r_group / 2]);
	int cur = partition(A, low, high);
	if (cur == k-1){
		return A[cur];
	}
	else if (cur < k){
		return select_rank_k(A, cur + 1, high, k);
	}
	else{
		return select_rank_k(A, low, cur - 1, k);
	}
}

int main(void)
{
	int A[15] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
	printf("%d\n", select_rank_k(A, 0, 3, 2));
	return 0;
}

Reference:陳玉福.計算機演算法設計與分析,59-61

@qingdujun

2017-11-22 北京 懷柔