本文探討序列的元素選擇問題。
一、最大值和最小值選擇算法
首先我們來看一下一個序列中最大值和最小值的選擇問題,這是一個很常見的元素選擇問題,也很簡單。對於一個n個元素的集合,很明顯至少要比較n-1次才能找出最大值或者最小值。我們使用Java來實現這個算法:
public static int getMax(int[] arr){
if(arr.length == 0)
throw new IllegalArgumentException("數組爲空");
int max = arr[0];
for(int i = 1 ; i < arr.length; i++){
if(arr[i] > max)
max = arr[i];
}
return max;
}
public static int getMin(int[] arr){
if(arr.length == 0)
throw new IllegalArgumentException("數組爲空");
int min = arr[0];
for(int i = 1 ; i < arr.length; i++){
if(arr[i] < min)
min = arr[i];
}
return min;
}
這個算法十分簡單,不做過多介紹。下面我們來思考一個問題,如何遍歷一次序列便同時獲得該序列的最大值和最小值。我們可能很快會萌生這樣的想法:遍歷數組,然後每個元素分別和之前的最大值和最小值做比較,然後更換最大值和最小值。但是這樣的算法會導致循環內進行了四次比較,有沒有優化方法?
事實上我們可以這樣做,使得循環內只需要做三次比較,而且減少了循環次數:
我們先把最大值初始化爲負無窮,把最小值初始化爲正無窮,然後對序列中兩個元素兩個元素地進行比較,先比較出這兩個元素的較大者,把較大者與之前的最大值比較,把較小者與之前的最小值比較。對於偶數個數的序列,這樣兩個兩個能在循環完結束,但是當序列的元素個數是奇數的時候,我們就要單獨比較最後一個元素。
Java代碼如下:
public static int[] getMaxAndMin(int[] arr){
if(arr.length == 0)
throw new IllegalArgumentException("數組爲空");
int[] result = new int[2];
if(arr.length == 1){
result[0] = arr[0];
result[1] = arr[0];
return result;
}
result[0] = Integer.MAX_VALUE;
result[1] = Integer.MIN_VALUE;
int i = 1;
for(i = 1 ;i < arr.length; i+=2){
//循環中的第一次比較 比較一對數字之間的大小
if(arr[i]<arr[i-1]){
//循環中的第二次比較 獲取較小值
if(result[0]>arr[i])
result[0] = arr[i];
//循環中的第三次比較 獲取較大值
if(result[1]<arr[i-1])
result[1] = arr[i-1];
}
else{
//循環中的第二次比較 獲取較大值
if(result[1]<arr[i])
result[1] = arr[i];
//循環中的第三次比較 獲取較小值
if(result[0]>arr[i-1])
result[0] = arr[i-1];
}
}
if(arr.length%2==1){
//這時i等於arr.length 我們需要比較arr[arr.length-1]和最大值最小值
if(arr[arr.length-1]<result[0]){
result[0] = arr[arr.length-1];
}
if(arr[arr.length-1]>result[1])
result[1] = arr[arr.length-1];
}
return result;
}
我們來分析一下比較次數,如果n是奇數,那麼會比較(n-1)/2*3+1次,如果是偶數,會比較n/2*3次。二、漸進時間複雜度爲Θ(n)的選擇算法
我們從直觀上看一般選擇問題比最小值最大值選擇問題會更復雜,但是實際上這兩個問題的漸進運行時間是相同的,都是Θ(n)。我們下面將給出這種漸進時間複雜度爲Θ(n)的一般選擇算法。
這實際上本質是分治法,對於尋找序列arr中索引在[p,r]的元素中第i小的元素,如果p和r相等,我們直接返回arr[p],否則我們用一個軸元素arr[0],把[p,r]分成[p,q-1]和[q+1,r],其中[p,q-1]中的元素都小於arr[0],[q+1]的元素都大於等於arr[0]。如果q恰好等於i,那麼我們直接返回,否則我們繼續劃分區間,直到區間只有一個元素,直接返回。
我們使用Java來實現這一算法:
private static void swap(int[]a , int location1 , int location2){
int temp;
temp = a[location1];
a[location1] = a[location2];
a[location2] = temp;
}
private static int randomizedPartition(int[] a, int p, int r){
int shaft = a[0];//第0個元素作爲軸元素
while(p!=r){
while(p<r&&a[p]<shaft)
p++;
while(r>p&&a[r]>=shaft)
r--;
swap(a,p,r);
}
return p;
}
public static int randomizedSelect(int[] a, int p, int r ,int i){
if(p == r)
//檢查遞歸的基本情況
return a[p];
int q = randomizedPartition(a, p, r);
//將[p,r]劃分爲[p,q-1]和[q+1,r] 其中a[q]比左邊的都大 比右邊的都小
int k = q - p;
if(i == k)
return a[q];
else if(i < k)
return randomizedSelect(a, p, q-1, i);
else
return randomizedSelect(a, q+1, r, i - k);
}
這個算法最壞情況的運行時間是Θ(n^2),在那樣情況下每次劃分都很不走運地將軸元素劃分得很極端,但是它的平均情況下,我們可以在線性時間內找到任一順序統計量。
三、最壞情況爲O(n)的選擇算法
這種選擇算法也是基於快速排序的劃分算法,採用分治法的思想,最壞情況運行時間爲O(n)。
思路如下:
1、將序列的n個元素劃分爲n/5向下取整組,每組5個元素,如果n不是5的倍數,那麼其中一組有n mod 5個元素。
2、尋找這n/5向上取整組中的每一組的中位數,使其存在於每一組的第二個元素,劃分該組另外幾個元素。
3、找到每一組中位數的中位數,劃分其他組。
4、按照中位數的中位數對序列進行劃分,讓x成爲第k小的元素。
5、如果i=k,返回x。如果i<k,則在低區遞歸調用該算法,否則在高區遞歸調用該算法。
這裏這種算法我先不做實現了,先把思路列出來,我有一些疑問。在尋找中位數的時候顯然不能將所有元素都使用插入排序排序好再來尋找,那樣的時間複雜度太高。對於5個元素尋找中位數,我們可以通過6次比較尋得中位數(這裏不寫思路了,讀者可以自己想一下),這個比較次數是可以接受的,但是對於尋找每一組元素的中位數的中位數,很顯然這又是一個選擇問題,那麼我們該用什麼樣的算法來尋找中位數的中位數呢?