实验一 分治与递归
由带权中位数的定义,若一个数左边的数都小于它,右边的数都大于它,那么如果左边数的权重和小于0.5,右边数的权重和也小于0.5,那么这个数就是带权中位数;否则,带权中位数在权重和大于0.5的那一边。
因此,我们可以用快速排序中的 p a r t i t i o n partition partition 算法,用双指针使下标为 p i v o t pivot pivot 的数左边均为小于 n u m [ p i v o t ] num[pivot] num[pivot] 的数,右边均为大于 n u m [ p i v o t ] num[pivot] num[pivot] 的数,然后再通过左右权值之和判断是否为带权中位数,若不是则判断带权中位数应在哪一边寻找,并继续递归寻找。
void WeightMedian(int length, vector<int>num, vector<double>weight, int index) {
//类似快排算法进行划分
int pivot = index, l = index, r = index + length; //划分基准pivot取index
while (l < r) {
do l++; while (num[l] < num[pivot]);
do r--; while (num[r] > num[pivot]);
if (l < r) {
swap(num[l], num[r]);
swap(weight[l], weight[r]);
}
}
swap(num[r], num[pivot]);
swap(weight[r], weight[pivot]);
pivot = r;
//对划分的左右子数组进行权重计算
double cntl = 0, cntr = 0;
for (int i = 0; i < pivot; i++) cntl += weight[i];
for (unsigned int i = pivot + 1; i < weight.size(); i++) cntr += weight[i];
if (cntl < 0.5 && cntr < 0.5) { //左右数组权重都小于0.5,num[pivot]即为答案
cout << num[pivot];
return;
}
if (cntl >= 0.5) { //左边权重大于0.5,递归对左子数组求解
WeightMedian(pivot - index, num, weight, index);
return;
}
if (cntr >= 0.5) { //右边权重大于0.5,递归对右子数组求解
WeightMedian(length - pivot + index - 1, num, weight, pivot + 1);
return;
}
}
上述方法最坏时间为 O ( n 2 ) O(n^2) O(n2),并不符合题意。例如,当划分总是在(子)数组两端时,此时就是最坏的情况。所以为了加速算法,我们需要寻找一个合适的划分基准 p i v o t pivot pivot,而不是如上面算法一样默认以左端第一个数位划分基准 p i v o t pivot pivot。
下面讨论的算法可以在最坏情况下用
O
(
n
)
O(n)
O(n)时间就完成带权中位数的选择,如果能在线性时间内找到一个划分基准,使得按照这个划分基准所划分出的两个子数组的长度都至少为原数组长度的
ϵ
\epsilon
ϵ倍(
0
<
ϵ
<
1
0<\epsilon<1
0<ϵ<1是某个正常数),那么就可以在最坏情况下
O
(
n
)
O(n)
O(n)时间完成任务。
T
(
n
)
⩽
T
(
ϵ
n
)
+
O
(
n
)
⇒
T
(
n
)
=
O
(
n
)
T(n)\leqslant T(\epsilon n)+O(n)\Rightarrow T(n)=O(n)
T(n)⩽T(ϵn)+O(n)⇒T(n)=O(n)
按以下步骤可以找到满足要求的划分标准:
- 将n个输入元素划分成 ⌈ n / 5 ⌉ \lceil n/5\rceil ⌈n/5⌉个组,每组5个元素,只可能有一个组不是5个元素。用任意一种排序算法,将每组中的元素排好序,并取出每组的中位数,共 ⌈ n / 5 ⌉ \lceil n/5\rceil ⌈n/5⌉个。
- 递归找到\lceil n/5\rceil
个
元
素
的
中
位
数
。
如
果
个元素的中位数。如果
个元素的中位数。如果\lceil n/5\rceil$是偶数,就找到两个中位数中较大的一个。以这个元素作为划分基准。
下面证明此划分标准所划分出的两个子数组的长度都至少为原数组长度的 ϵ \epsilon ϵ倍:
只要等于基准的元素不太多,利用这个基准划分的两个子数组的大小就不会相差太远。为了简化问题,先设所有元素互不相同。在这种情况下,找出基准 x x x至少比 3 × ⌊ n / 5 × 1 / 2 − 1 ⌋ = 3 ⌊ ( n − 5 ) / 10 ⌋ 3\times \lfloor n/5 \times 1/2 -1 \rfloor=3\lfloor (n-5)/10\rfloor 3×⌊n/5×1/2−1⌋=3⌊(n−5)/10⌋个元素大(向下取整是因为最后一组可能有1,2,3,4,5个元素),因为在除了最后一组中的每一组都有两个元素小于本组的中位数,而 ⌊ n / 5 ⌋ \lfloor n/5 \rfloor ⌊n/5⌋个中位数又有 ⌊ ( n − 5 ) / 10 ⌋ \lfloor (n-5)/10\rfloor ⌊(n−5)/10⌋个元素小于基准 x x x。当 n ⩾ 75 n\geqslant 75 n⩾75时, 3 ⌊ ( n − 5 ) / 10 ⌋ ⩾ n / 4 3\lfloor (n-5)/10\rfloor\geqslant n/4 3⌊(n−5)/10⌋⩾n/4。所以按此基准划分所得的两个子数组的长度都至少缩短1/4。
void WeightMedian(int length, vector<int>num, vector<double>weight, int index) {
int l = index, r = index + length - 1; //定义l(left)与r(right)
//确定划分基准pivot
while (r - l >= 5) {
int count = 0;
for (int i = l; i <= r; i += 5) { //寻找[n/5]组每组的中位数
if (i + 4 <= r) {
for (int j = 0; j < 3; j++) //三次冒泡,确定第三小的元素,即中位数
for (int k = i + 4; k > i + j; k--)
if (num[k] < num[k - 1]) {
swap(num[k - 1], num[k]);
swap(weight[k - 1], weight[k]);
}
swap(num[l + count], num[i + 2]); //将找到的中位数放到数组l + count位置
swap(weight[l + count], weight[i + 2]);
count++;
}
}
r = l + count - 1; //r更新为l + count - 1,进行递归
}
int pivot;
if (r > l) {
for (int i = 0; i < (r - l) / 2 + 1; i++)
for (int j = r - i; j > l; j--)
if (num[j] < num[j - 1]) {
swap(num[j - 1], num[j]);
swap(weight[j - 1], weight[j]);
}
pivot = l + (r - l) / 2 + 1; //划分基准pivot即为最后num[l~r]的中位数
}
else pivot = l;
//类似快速排序算法,按照pivot对数组进行划分
l = index, r = index + length; //重新设置l与r
swap(num[l], num[pivot]); //为了方便,将划分基准pivot放在(子)数组首部
swap(weight[l], weight[pivot]);
pivot = l;
while (l < r) {
do l++; while (num[l] < num[pivot]);
do r--; while (num[r] > num[pivot]);
if (l < r) {
swap(num[l], num[r]);
swap(weight[l], weight[r]);
}
}
if (pivot != r) {
swap(num[r], num[pivot]);
swap(weight[r], weight[pivot]);
pivot = r;
}
//对划分的左右子数组进行权重计算
double cntl = 0, cntr = 0;
for (int i = 0; i < pivot; i++) cntl += weight[i];
for (unsigned int i = pivot + 1; i < weight.size(); i++) cntr += weight[i];
if (cntl < 0.5 && cntr < 0.5) { //左右数组权重都小于0.5,num[pivot]即为答案
cout << num[pivot];
return;
}
if (cntl >= 0.5) { //左边权重大于0.5,递归对左子数组求解
WeightMedian(pivot - index, num, weight, index);
return;
}
if (cntr >= 0.5) { //右边权重大于0.5,递归对右子数组求解
WeightMedian(length - pivot + index - 1, num, weight, pivot + 1);
return;
}
}
下面分析算法 W e i g h t M e d i a n WeightMedian WeightMedian的时间复杂度:
- 第一步确定划分基准 p i v o t pivot pivot, f o r for for循环执行了 5 / n 5/n 5/n次,每次需要 O ( 1 ) O(1) O(1)时间,因此这一步共需 O ( n ) O(n) O(n)
- 第二步按照基准进行划分,这里也需要 O ( n ) O(n) O(n)时间
- 第三步计算左右权值,需要 O ( n ) O(n) O(n)时间
- 第四步递归计算,上面已经证明当 n ⩾ 75 n\geqslant75 n⩾75时, 3 ⌊ ( n − 5 ) / 10 ⌋ ⩾ n / 4 3\lfloor (n-5)/10\rfloor\geqslant n/4 3⌊(n−5)/10⌋⩾n/4,划分后子数组长度至少缩短1/4,因此至少需要 T ( 3 n / 4 ) T(3n/4) T(3n/4)时间
因此,可以得到关于
T
(
n
)
T(n)
T(n)的递归式
T
(
n
)
⩽
{
C
1
n
<
75
C
2
n
+
T
(
3
n
/
4
)
n
⩾
75
T(n)\leqslant \left\{\begin{array}{lc} C_1 & n < 75 \\ C_2n+T(3n/4)&n\geqslant 75\\ \end{array} \right.
T(n)⩽{C1C2n+T(3n/4)n<75n⩾75
式中,
C
1
C_1
C1为
n
<
75
n<75
n<75时需要的常数时间,
C
2
n
C_2n
C2n为前三步所需的时间,解此递归式可得
T
(
n
)
=
O
(
n
)
T(n)=O(n)
T(n)=O(n).