一、問題描述
給定一個數組,數組中的數據無序,在一個數組中找出其第k個最小的數,例如對於數組x,x = {3,2,1,4,5,6},則其第2個最小的數爲2。
二、解題思路
本算法跟快排的思想相似,首先在數組中選取一個數centre作爲樞紐,將比centre小的數,放到centre的前面將比centre大的數,放到centre的後面。如果此時centre的位置剛好爲k,則centre爲第k個最小的數;如果此時centre的位置比k前,則第k個最小數一定在centre後面,遞歸地在其右邊尋找;如果此時centre的位置比k後,則第k個最小數一定在centre後面,遞歸地在其左邊尋找。
注意:centre的位置=其下標值+1,因爲數組中的第一個元素的下標爲0。
從上面的描述中,我們可以看到這個算法運用了減治的方法求解。減治的思想與分治非常相似,同樣是在一次操作中,削減問題的規模,只是分治把每個子問題求解後,要合併每個子問題的解才能得到問題,而減治的方法,卻不用合併子問題的解,子問題的解,直接就是原問題的解。舉個例子來說,就像快排和二分查找算法,前者是分治,後者是減治。因爲快排要等到所有的子數組都排完序,原數組纔有序,而二分查找卻不用,它每執行一次查找,直接丟棄一半的數組,而不用合併子問題的解。不過也有不少書,把他們都歸爲分治法。
三、代碼實現
考慮到代碼的通用性,使用了模板函數,如果看不懂模板函數,則只需要忽略template<typename T>,並把T看作是一個類型即可。代碼如下:
//返回數組中的第k個最小元素的啓動函數,注意會破壞原數組
template<typename T>
T FindTheKMin(T *x, int x_size, int k);
//實現查找數組中第K個最小元的功能函數
template<typename T>
T TheKMin(T *x, int left, int right, int k);
template<typename T>
T FindTheKMin(T *x, int x_size, int k)
{
//判斷k的值是否過大,即超過數組的大小
//若是則返回第0個元素,主要是爲了防止無效的遞歸
if(x_size < k)
return x[0];
return TheKMin(x, 0, x_size-1, k);
}
template<typename T>
T TheKMin(T *x, int left, int right, int k)
{
//取數組最後一個元素爲樞紐
T centre = x[right];
int i = left;
int j = right - 1;
while(true)
{
//從前向後掃描,找到第一個小於樞紐的值,
//在到達數組末尾前,必定結束循環,因爲最後一個值爲centre
while(x[i] < centre)
++i;
//從後向前掃描,此時要檢查下標,防止數組越界
while(j >= left && x[j] > centre)
--j;
//如果沒有完成一趟交換,則交換
if(i < j)
Swap(x[i], x[j]);
else
break;
}
//把樞紐放在正確的位置
Swap(x[i], x[right]);
//如果此時centre的位置剛好爲k,則centre爲第k個最小的數
if(i+1 == k)
return x[i];
else if(i+1 < k)
{
//如果此時centre的位置比k前,遞歸地在其右邊尋找
TheKMin(x, i+1, right, k);
}
else
{
//如果此時centre的位置比k後,遞歸地在其左邊尋找
TheKMin(x, left, i-1, k);
}
}
代碼說明:
在上面的代碼中,我們要注意,TheKMin函數的最後的if-else,這個算法不同於快排,當樞紐不是要找到元素時,它只會選擇其中一個方向的子數組繼續尋找,而不像快排那樣,會在兩個方向的子數組中繼續。從上面的代碼來看,其運行速度應該在使用相同選取樞紐的策略的快排之上,時間複雜度爲O(N)。
同時,當K值不合理時,我們只能返回第0個元素,這點有一點的不合理,但是,我不知道該返回一個什麼樣的合適的值,因爲它是泛型的。
其實,這段代碼有兩個缺陷,第一個,就是在查找時,破壞了數組原來的數據(交換了位置);第二個是,當類型T的複製和構造開銷較大時,直接多次交換兩個元素,可能會帶來相當大。
另一種實現
下面,再來看看另一種實現,算法的思想和策略相同,但是使用了一個跟蹤數組track,用來跟蹤使用第一種方法下的數據的交換情況,利用跟蹤數組的元素交換代替原數組中元素的交換,解決了上面提到的兩個問題。它的實現如下:
//返回數組中的第中個最小元素的下標的啓動函數,不破壞原數組
template<typename T>
int IndexOfKMin(const T *x, int x_size, int k);
//實現查找數組中第K個最小元下標的功能函數
template<typename T>
int TheKMin(const T *x, int *track, int left, int right, int k);
template<typename T>
int IndexOfKMin(const T *x, int x_size, int k)
{
//判斷k的值是否過大,即超過數組的大小
//若是則返回下標-1,主要是爲了防止無效的遞歸
if(x_size < k)
return -1;
//創建一個跟蹤數組,其內容爲原數組中元素的下標,
//用於記錄元素的交換(即代替元素的交換)
//按順序以track數組中的數據爲下標訪問元素,訪問順序與上一方法相同
int *track = new int[x_size];
for(int i = 0; i < x_size; ++i) //初始化跟蹤數組,其值與下標值相對應
track[i] = i;
int i = TheKMin(x, track, 0, x_size-1, k);
delete []track;
return i;
}
template<typename T>
int TheKMin(const T *x, int *track, int left, int right, int k)
{
//取數組最後一個元素爲樞紐
T centre = x[track[right]];
int i = left;
int j = right - 1;
while(true)
{
//從前向後掃描,找到第一個小於樞紐的值,
//在到達數組末尾前,必定結束循環,因爲最後一個值爲centre
//注意此時的數據的下標不是i,而是track[i]
while(x[track[i]] < centre)
++i;
//從後向前掃描時要檢查下標,防止數組越界
while(j >= left && x[track[j]] > centre)
--j;
//如果沒有完成一趟交換,則交換,注意,是交換跟蹤數組的值
if(i < j)
Swap(track[i], track[j]);
else
break;
}
//把樞紐放在正確的位置
Swap(track[i], track[right]);
//如果此時centre的位置剛好爲k,則centre爲第k個最小的數,
//返回其在真實數組中的下標,即track[i]
if(i+1 == k)
return track[i];
else if(i+1 < k)
{
//如果此時centre的位置比k前,遞歸地在其右邊尋找
TheKMin(x, track, i+1, right, k);
}
else
{
//如果此時centre的位置比k後,遞歸地在其左邊尋找
TheKMin(x, track, left, i-1, k);
}
}
代碼說明:
從上面的代碼,我們可以看出,這個函數是返回數組中的第k個最小元的下標,所以當k不合理時,就可以返回-1來表示這個錯誤,同時,它使用了一個跟蹤數組,track數組中的內容,實質是原數組中數據的一個索引,利用跟蹤數組的元素的交換來代替了原數組元素的交換,因爲該跟蹤數組的數據類型是int,所以其交換速度相當快,從而解決了上面提到的兩個問題。
從上面的代碼,我們也可以看到,其時間複雜度與前面的實現是一樣的,也爲O(N),但是,這個實現方法卻帶來了一定的空間開銷,它開闢了一個與原數組元素個數相等的一維數組,用於跟蹤原數組中的元素的交換情況。
至於在實際中,要使用哪一種算法,取決於使用者的需要!