在一个由n个元素组成的集合中,第i个顺序统计量是该集合中第i小的元素。一个中位数是它所属集合的“中点元素”。当n为奇数时,中位数是唯一的,位于i=(n+1)/2处;当n为偶数时,存在两个中位数,分别位于i=n/2和i=n/2+1处。如果不考虑n的奇偶性,中位数总是出现在i=⌊(n+1)/2⌋处(下中位数)和i=⌈(n+2)/2⌉(上中位数)。
1、最小值和最大值
在一个有n个元素的集合中,需要做n-1次(上界)比较才能找到最小值或最大值。
int MiniValue(const int *A, int len) { int minValue = A[0]; for (int i = 1; i < len; ++i) { if (A[i] < minValue) { minValue = A[i]; } } return minValue; }
那么如何同时找到最小值和最小值呢?
比较简单的思路是:只要分别独立找出最大值和最小值,各需要n-1次比较,共需2n-2次比较。下面给出一个算法,只需要最多3⌊n/2⌋次比较就可以同时找到最大值和最小值。
思路是:记录已知的最大值和最小值,对输出元素成对地进行处理。
(1)首先,将一对输入元素相互比较,然后将较小的与当前最小值比较,较大的与当前最大值比较,这样每对元素共需3次比较。
(2)如果n是奇数,将最小值和最大值的初值都设为第一个元素的值,然后成对地处理余下的元素;如果n是偶数,就对前两个元素做一次比较,决定最大值和最小值的初值,然后成对地处理余下的元素。
1 void MinMaxValue(const int *A, int len, int &minValue, int &maxValue) 2 { 3 int i, tmpMin, tmpMax; 4 5 if (len % 2 == 0 && len != 0) 6 { 7 minValue = A[0] < A[1] ? A[0] : A[1]; 8 maxValue = A[0] + A[1] - minValue; 9 i = 2; 10 } 11 else 12 { 13 minValue = A[0]; 14 maxValue = A[0]; 15 i = 1; 16 } 17 18 for (; i < len; i += 2) 19 { 20 if (A[i] < A[i + 1]) 21 { 22 tmpMin = A[i]; 23 tmpMax = A[i + 1]; 24 } 25 else 26 { 27 tmpMin = A[i + 1]; 28 tmpMax = A[i]; 29 } 30 31 if (tmpMin < minValue) 32 { 33 minValue = tmpMin; 34 } 35 36 if (maxValue < tmpMax) 37 { 38 maxValue = tmpMax; 39 } 40 } 41 }
如果n是奇数,共进行3⌊n/2⌋次比较。如果n是偶数,共进行3(n-2)/2+1=3n/2-2次比较。
2、期望为线性时间的选择算法
下面介绍一种解决选择问题的分治算法。
#include <iostream> using namespace std; int RandomizedPartition(int *A, int low, int high) { int key = A[high], tmp; int i = low - 1, j; for (j = low; j < high; ++j) { if (A[j] <= key) { i = i + 1; tmp = A[i]; A[i] = A[j]; A[j] = tmp; } } A[high] = A[i + 1]; A[i + 1] = key; return i + 1; } int RandomizedSelect(int *A, int low, int high, int i) { if (low == high) { return A[low]; } int q = RandomizedPartition(A, low, high); int k = q - low + 1; if (i == k) { return A[q]; } else if (i < k) { return RandomizedSelect(A, low, q - 1, i); } else { return RandomizedSelect(A, q + 1, high, i - k); } } //test int main() { int A[] = {10, 9, 5, 4, 3, 2, 1, 8, 7, 6}; for (int i = 1; i <= 10; ++i) { cout << RandomizedSelect(A, 0, 9, i) << ' '; } cout << endl; return 0; }
说明:
(1)RandomizedSelect的最坏运行时间为,因为每次划分时可能总是按余下的元素中最大的来进行划分,而划分操作需要时间。
(2)与快速排序不同的是,快速排序会递归处理划分的两边,而RandomizedSelect只处理划分的一边,这一差异体现在性能上就是快速排序的期望运行时间为,而RandomizedSelect的期望运行时间为。
3、最坏情况为线性时间的选择算法
下面介绍一个最坏情况运行时间为O(n)的选择算法Select。
步骤1:将输入数组的n个元素划分为⌊n/5⌋组,每组5个元素,且至多只有一组由剩下的n mod 5个元素组成。
步骤2:寻找这⌈n/5⌉组中每一组的中位数:首先对每组元素进行插入排序,然后确定每组有序元素的中位数。
步骤3:对步骤2中找出的⌈n/5⌉个中位数,递归调用Select以找出其中位数x。
步骤4:利用修改过的Partition版本,按中位数的中位数x对输入数组进行划分。
步骤5:k为划分的低区元素个数+1。如果i=k,返回x;如果i<k,则在低区递归调用Select来找出第i小的元素;如果i>k,则在高区递归查找第i-k小的元素。
说明:Select算法通过对输入数组的递归划分来找出所需元素,在该算法中能够保证得到对数组的一个好的划分。Select使用的也是快速排序的确定性划分算法Partition,做的修改是将划分的主元也作为输入参数。