假設給定一個數組A,要求你找到數組的中點,并且將離中點最近的k個數組成員抽取出來。例如數組A={7, 14, 10, 12, 2, 11, 29, 3, 4},該數組的中點是10,如果令k=5,那么里中點最近的5個數就是{7,14,10,12,11}.
要求對任意一個數組,設計一個時間復雜度為O(n)的算法,該算法能找出距離數組中點最近的K個元素。
這道題的難度是比較大的,在面試中出現的概率應該不大,如果你能在一小時內把它解決并給出代碼,那么你的水平在BAT中當個技術經理以上的職位應該是沒有太大問題的。
解決這個問題要分兩步走:
1, 找到一個算法,能夠讓我們在O(n)的時間內查找到數組的中點。
2, 假設我們找到了中點m, 那么我們把計算數組前k個元素與中點m的距離,也就是 d(i) = |A[i] - m|, 把這k個元素與中點的距離構建一個大堆。接著在剩下的n - k 個元素中,我們逐次計算他們與中點m的距離,然后用這個距離與大堆中的最大值相比較,如果這個距離大過大堆返回的最大值,那么就忽略這個元素,如果它的距離比大堆的最大值小,那么就把大堆中的最大值去掉,將當前元素加入大堆。
當完成步驟2之后,大堆中的k個元素就是距離終點最近的K個元素。在第二步中,我們需要把元素加入一個大堆,對于一個含有k個元素的大堆而言,加入一個元素的復雜度是O(lgk),所以執行第二步所需的時間復雜度是O(n * lgk)。 由于k是一個常數,所以第二步相對于變量n來說,也是線性的。
現在問題在于第一步的實現,也就是如何通過線性時間的算法找到一個數組的中點,事實上,只要找到第一步的算法,那么整個問題就可以解決了,本質上第二步是多余的,所以本文的重點將在對一步的分析上。我們看看如何設計一個算法,使得我們能在O(n)的時間復雜度內,在數組中找到任意第k大的元素。顯然,數組中點是數組中第n/2大的元素,于是要解決問題,我們只要找到第(n/2-k/2)大的元素,然后再找到(n/2+k/2)大的元素,最后查找所有處于這兩者之間的元素,那么問題就解決了。
我們看看如何在O(n)的時間內,找到數組中第i大的元素,它的算法步驟如下:
1, 把n個元素分成若干個小組,每個小組有5個元素,于是總共能分成n/5組
2, 對每個小組中的五個元素進行排序,然后取出他們的中點。
3, 利用本算法遞歸的去查找步驟2中所有中點組成的集合中的中點,假定得到的中點為x。
4, 根據步驟4得到的x把數組劃分為兩部分,把所有小于x的元素放到x的左邊,把所有大于x的元素放到x的右邊。假設x的左邊有k-1個元素,那么x就是第k小的元素,在x的右邊就有n-k個元素。
5, 如果i == k, 那么直接返回x, 如果 i < k, 那么我們在x左邊元素集合中,遞歸的使用該算法去查找第i 小的元素。如果i > k, 那么我們在x的右邊元素集合中,使用該算法去查找第(i - k)小的元素。
該算法的實現代碼如下:
int select(int[] A, int i) {
//在數組中查找第i小的元素
if (A.length == 1) {
return A[0];
}
/*
* 步驟1,將數組分成小組,每個小組含有5個元素,最后一組很可能沒有5個元素
*/
ArrayList<int[]> arrCollection = new ArrayList<int[]>();
int p = 0;
int cnt = 5;
int[] arr = null;
while (p < A.length) {
if (cnt >= 4) {
int len = Math.min(5, A.length - p);
arr = new int[len];
cnt = 0;
arrCollection.add(arr);
} else {
cnt++;
}
arr[cnt] = A[p];
p++;
}
/*
* 步驟2,把每個小組中的元素排序,并取出每個小組中的中點
*/
int[] medians = new int[arrCollection.size()];
for (int j = 0; j < arrCollection.size(); j++) {
Arrays.sort(arrCollection.get(j));
arr = (int[])arrCollection.get(j);
medians[j] = arr[arr.length / 2];
}
/*
* 步驟3,遞歸的去獲取中點集合中的中點
*/
int x = select(medians, medians.length/2);
/*
* 步驟4,把小于x的元素放到x左邊,把大于x的元素放到x的右邊
*/
int[] B = new int[A.length];
int begin = 0, end = A.length - 1;
int pos = 0;
while (pos < A.length) {
if (A[pos] < x) {
B[begin] = A[pos];
begin++;
}
if (A[pos] > x) {
B[end] = A[pos];
end--;
}
pos++;
}
B[begin] = x;
A = B;
/*
* 執行步驟5,如果x左邊的元素是k-1個,同時i == k, 那么返回x
* 如果i < k, 那么遞歸的去在左邊k-1個元素中查找第i小的元素
* 如果i > k, 那么遞歸的在右邊n-k個元素中,查找第(i - k)小的元素
*/
if (i == begin) {
return x;
} else if (i < begin) {
arr = Arrays.copyOf(A, begin);
return select(arr, i);
} else {
arr = Arrays.copyOfRange(A, begin+1, A.length);
return select(arr, i - begin - 1);
}
}
我們簡單對算法進行分析一下:
第一步是把元素分成若干個小組,一次循環就可以完成,所以復雜度是O(n);
第二步是找到每個小組中的中點,由于每個小組只有5個元素,所以第二步的時間復雜度也是O(n);
第三步是在所有中點的集合中再找出中點,這一步涉及到算法的遞歸,所以我們暫時不做分析。
第四步是把數組元素分成兩部分,所需時間復雜度也是O(n)。
第五步是在數組中的某一個子集中再次遞歸的運行算法。
我們看看執行第3步后,也就是取到中點的中點,假設中點的中點為x,然后我們會把數組元素根據x分成兩部分:
我們看上圖,假設中間點x是我們找到的中點集合中的中點,這樣右下角陰影部分的節點,他們的值肯定是大于x的。因為我們把所有節點分成若干個小組,每個小組有5個點,最后一個小組有可能不足5個元素,所有去掉包含x的那個小組,以及最后一個小組,那么就有 (1/2 * n/5) 個小組中,下半部的三個節點都會大于x.于是大于x的元素的個數就至少有:
3* [ (1/2 *n/5) - 2] = 3n/10 - 6 個。
如果我們要找的元素處于前半部分,那么前半部分的元素個數最多有 n - (3n/10 - 6) = 7n/10 + 6 個,如果處于后半部分,那么要處理的元素就有3n/10 - 6 個,也就是說,當我們執行第5步的時候,遞歸處理的元素個數最多不超過7n/10 + 6個。
我們不深入更加細致的復雜度分析,可以保證的是,算法的最終復雜度是O(n), 也就是說依靠上面算法實現,我們可以在線性時間內從數組中查找處于任意位置的元素。
根據給定的數組A = {7, 14, 10, 12, 2, 11, 29, 3, 4}, 它排序后為:{2, 3, 4, 7, 10, 11, 12, 14, 29},于是第0小的元素是2,第8小的元素是29.如果運行代碼:
int y = select(A, 8);
那么我們得到的y值就是29。對代碼更詳細的講解和調試演示,請參看視頻:
更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公眾號: