引言
一个数据集合的中值(Median)通常是很一个很有价值的统计指标,由于它对异常数据不敏感,所以一般会比平均值(Mean)更能体现数据集合数据的“平均水平”。然而,对于无序数据序列求中值在实现上却没有求平均值那样简单优美的O (N)复杂度的算法。最容易想到的做法是先对数据进行排序,然后取中点的值,然而这种做法的时间复杂度是O(NlogN)。有没有更快的算法呢?
本文介绍两种更快的算法:第一种是利用快速排序原理的准确的随机选择算法;第二种是一种近似算法,所获得的值虽然可能不是很精确,但一般会比第一种方法更快。
本报告的算法实现伪代码除注释部分外均引自参考文献,若只需理解实现思想,不需要钻研伪代码的细节,可先直接看正文与注释部分。本报告提供了这两种算法相对先排序后取中值算法的时间性能比较的实验结果。
随机选择算法
这一算法是一个通用的选择算法,它可以求一个可排序数据序列中第k个位置的值。其算法伪代码如下:
// 分割,即一般快排的分割方法,返回分割点
PARTITION(A, p, r)
x ß A[r]
i ß p-1
for j ß p to r-1
do if A[j] <= x
then i ß i+1
exchange A[i] <-> A[j]
exchange A[i+1] <-> A[r]
return i+1
// 随机分割,在选择分割点时引入随机性,使快排的最坏情况难以发生
RANDOMIZED-PARTITION(A, p, r)
i ß RANDOM(p, r)
exchange A[r] <-> A[i]
return PARTITION(A, p, r)
// 随机选择
RANDOMIZED-SELECT(A, p, r, i)
if p = r
then return A[p]
q ß RANDOMIZED-PARTITION(A, p, r)
k ß q – p +1
if i = k
then return A[q]
elseif i < k
then return RANDOMIZED-PARTITION(A, p, q-1, i)
else return RANDOMIZED-PARTITION(A, q+1, r, i-k)
其基本思想是利用快速排序的原理,对数据进行分割,将分割点的位置与k做比较,若相等,返回k位置的值,否则在分割点左右两个集合的其中一个中继续查找(快速排序需要对前后两个集合都进一步进行排序,而选择算法只需要对其中一个集合再处理,这是它能比排序算法更快的关键)。本算法的“随机”是在选取分割点时进行了随机化,这样可以避免出现最坏状况。本算法的算法复杂度为O(N)。
近似中值选择算法
本算法是一个近似算法,先看一个数组元素个数是3r的算法版本,其算法伪代码如下:
// 三重调整,对三个数进行调整,将中位数移到中间位置
TRIPLET_ADJUST(A, i, Step)
if (A[i] < A[j])
then
if (A[k] < A[i] then swap (A[i], A[j]);
else if (A[k] < A[j] then swap(A[j], A[k])
else
if (A[i] < A[k]) then swap(A[i], A[j]);
else if (A[k] > A[j]) then swap(A[j], A[k]);
// 中值近似,求一个数组A[0, 3r-1]的中值
APPROXIMATE_MEDIAN(A, r)
Step=1; Size=3^r;
Repeat r times
i = (Step-1)/2;
while ( i < Size) do
TRIPLET_ADJUST(A, I, Step);
i = i +(3*Step)
end while
Step = 3*Step
end repeat
return A[(Size-1)/2]
这一算法的基本思想是连续进行三重调整,即对每三个数取其中值,将取出的小区域中值组成一个新的数组再取其中值,直到最后。
以上算法只对n=3r时适用,对于一般情况如何扩展使用呢?以下是其基本思想:
(1) 对于任意的n, n = 3*t + k = 3*(t-1) + (3 + k) k∈{0, 1, 2} 对于前面的(t-1)个三重组使用以上三重调整算法,获取其中值序列,对于剩下的(3+k)个数,使用选择排序算法,获取其中值,与前面中值序列合并作为下一轮中值候选数组。
(2) 为了防止某些数每一轮被剩下,在选择三重组时从左到右、从右到左轮流进行。
其具体算法伪代码如下:
// 选择排序,就是一般的选择排序算法,没什么特别
SELECTION_SORT(A, Left, Size, Step)
for (i=Left; i < Left+(Size-1)*Step; i=i+Step)
min = i;
for (j=i+Step; j<Left+Size*Step; j=j+Step)
if (A[j].Key < A[min].Key) then min = j;
end for;
swap(A[i], A[min]);
end for
// 任何大小数组中值近似,利用三重调整与选择排序算法实现,获取A[0, Size-1]的中值
APPROXIMATE_MEDIAN_ANYN(A, Size)
LeftToRight = False; Left=0; Step=1;
while (Size > Threshod) do
LeftToRight = Not(LeftToRight);
Rem = (Size mod 3);
// 根据是从左至右还是从右至左设置调整初始位置
if (LeftToRight) then i = Left;
else i = Left + (3 + Rem)*Step;
// 对于t-1个三重组进行三重调整
repeat (Size/3-1) times
TRIPLET_ADJUST (A, i, Step);
i = i+3*Step;
end repeat;
if (LeftToRight) then Left = Left+Step;
else i=Left;
Left=Left+(1+Rem)*Step;
// 对剩下的3+Rem值进行选择排序
SELECTION_SORT(A, i, 3+Rem, Step);
if (Rem=2) then
if (LeftToRight) then swap(A[i+Step], A[i+2*Step])
else swap(A[i+2*Step], A[i+3Step]);
Step=3*Step; Size=Size/3;
end while
SELECTION_SORT(A, Left, Size, Step);
return A[Left+Step*Floor((Size-1)/2)];
可以证明,对于n=3r的情形,以上算法平均进行少于4n/3次比较和n/3次交换,最坏情形是进行小于3n/2次比较和n/2次交换。
实验结果
为了比较以上算法与先进行排序再取中值的实际性能差别,我们将以上算法用C++实现并与使用STL的sort排序后再取中值进行比较,N分别取值106, 107, 108,数组值初始化为0-N的随机值。结果如下:
不同算法运行时间比较(单位s):
N值 | 求平均值 | std::sort后求中值 | 随机选择算法 | 近似中值选择算法 |
1,000,000 | 0.01 | 0.23 | 0.07 | 0.03 |
10,000,000 | 0.09 | 2.79 | 0.6 | 0.26 |
100,000,000 | 0.92 | 32.79 | 4.68 | 2.51 |
从以上实验结果可知,近似中值选择算法一般比先排序的算法快10倍左右,随机选择算法比先排序的算法快5倍左右。近似中值选择算法所用时间为求平均值算法的时间的3倍左右。
小结
对于大数据集合,本文所介绍的随机选择算法和近似中值选择算法比快速排序分别快5倍与10倍左右;而且随机选择算法是一个通用的选择算法,可以用来方便地求k位置值。在恰当时侯值得一用。