计算中值——如何快速找到一个数组的中值

本文探讨了如何使用快速分区和Lomuto划分算法,避免排序求中值,针对无序数组设计了一种高效算法。通过递归地缩小搜索范围,实现在平均情况下时间复杂度为O(n),展示了实例和代码实现,以及最优和最差情况的时间复杂度分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在统计学中,中值往往比平均值更能体现一组数据的特点,因为不会被两边的极端数据影响。

那么要找到一个无序数组的中值,可以先将数组中的数据排序,然后直接返回数组中间的数据即可,但是我们要想到,仅仅只是为了找到数组的中值,就对其进行排序,可想而知时间复杂度会很高,因此这就需要一个更好的算法来求出中值。

算法原理

首先我们可以想一想,如果已知一个数组的中值的位置为k且中值的值也已知,那么从数组的开头开始遍历,如果数组元素大于中值,那么去掉该元素之后,中值在数组中的位置依旧为k;若数组元素小于中值,那么去掉该元素之后,中值在数组中的位置就变成了k-1。这一点需要想明白,不明白的可以在纸上比划比划。

这种思想,我们可以发现其实减治法中的减可变规模。具体可以参考我的另一篇博文:《减治法》

因此,我们可以采用快速分区的思想,但是对于一个无序数组而言,我们并不知道中值是多少,因此这里我们可以假设数组的第一个元素为中值,然后从第二个元素开始,若大于第一个元素,则直接去看下一个元素;若小于第一个元素,则将该元素

先上伪代码

因为伪代码没有其他语言的语法限制,能够有助于我们更好地将注意力放在算法上,所以这里我们先用伪代码来讲述一下主要思想:

首先需要一个划分算法,这里介绍Lomuto划分算法:

algorithm lomutoPartition(A[l..r])
//采用Lomuto算法,用第一个元素作为中轴对子数组进行划分
//输入:数组A[0..n-1]的一个子数组A[l..r],它由左右两边的索引l和r(l <= r)定义
//输出:A[l..r]的划分和中轴的新位置
p <- A[l]
s <- l
for i <- l+1 to r do
	if A[i] < p
		s <- s + 1
		swap(A[s], A[i])
swap(A[l], A[s])
return s

注意:这里的划分算法并不是排序,只是将小于p的元素放到p的左边,大于p的元素放到p的右边,同时s表示的即为元素p在数组中是第s小的元素。

然后使用快速选择算法:

algorithm quickSelect(A[l..r], k)
//用基于划分的递归算法解决选择问题
//输入:可排序数组A[0..n-1]的子数组A[l..r]和整数k(1 <= k <= r-l+1)
//输出:A[l..r]中第k小元素的值
s <- lomutoPartition(A[l..r])
if s = l+k-1 return A[s]
else if s > l+k-1 quickSelect(A[l..s-1], k)
else quickSelect(A[s+1..r], l+k-1-s)

因为已经知道了第一个元素是数组的第s小的元素,则若s刚好等于k,那么就直接返回即可;若s大于k,说明第k小的元素在左侧,那么将s后面的元素删除之后,原来第k小的元素依旧是剩下数组中的第k小的元素,因此就有了quickSelect(A[l..s-1], k)这段;若s小于k,说明第k小的元素在右侧,这个时候我们要注意了!比如一个数组是1、2、3,我要找到第3小的元素,那么假设s=2,k=3,去掉s之前的元素之后,数组就只剩下一个3了,因此我要找的元素就变成了第1小的元素,即k-s,也即伪代码中的quickSelect(A[s+1..r], l+k-1-s)

这里我们可以用一个例子来体会一下:

在数组{4,1,10,9,7,12,8,2,15}中,求第k=5小的元素。

则算法运行步骤为:

数组 {4,1,10,9,7,12,8,2,15} 分区, 中轴=4
分区得:{1,2},{4},{ 9,7,12,8,10,15}, s1=3

因s1<k, 在{ 9,7,12,8,10,15} 中找 k-s1=2
分区得:{7,8}, {9}, {12,10,15}, s2=3

因s2>k-s1, 在{7,8}中继续找 k-s1=2,
分区得:{7}, {8}, s3=1

因s3<k-s1, 在{8}中找 k-s1-s3=1
得,s=k=5, 中值是8

代码实现

那么我们就用代码来实现这一思路,这里我用的是C++:

快速分区部分:

int lomutoPartition(int* A, int low, int high) {
	int p = A[low];
	int s = low;
	for (int i = low + 1; i < high; i++) {
		if (A[i] < p) {
			swap(A[i], A[++s]);
		}
	}
	swap(A[s], A[low]);
	return s;
}

int quickSelect(int* A, int low, int high, int n) {
	int s = lomutoPartition(A, low, high);
	if (s == n - 1) return A[s];  //具体代码中因为是从0开始,所以第k小元素就是第k-1小元素
	else if (s > n - 1) quickSelect(A, low, s, n);
	else quickSelect(A, s + 1, high, n);
}

然后在main函数中启动:

int main() {
	int A[10] = { 7, 4, 98, 65, 34, 23, 54, 2, 1, 37 };
	cout << "第3小的元素为:" << quickSelect(A, 0, 10, 3) << endl;
	cout << "第8小的元素为:" << quickSelect(A, 0, 10, 8) << endl;
	return 0;
}

运行结果如下:

在这里插入图片描述

时间复杂度分析

最优情况:即进行一次划分之后就直接找到了第k小的元素,则复杂度为
C b e s t ( n ) = n − 1 ∈ O ( n ) C_{best}(n)=n-1\in O(n) Cbest(n)=n1O(n)

最差情况:即数组每次划分都是划分成1个小于p,剩下的大于p,或者反过来,则此时的复杂度为
C w o r s t ( n ) = ( n − 1 ) + ( n − 2 ) + . . . + 1 = n ( n − 1 ) 2 ∈ O ( n 2 ) C_{worst}(n)=(n-1)+(n-2)+...+1=\frac{n(n-1)}{2}\in O(n^2) Cworst(n)=(n1)+(n2)+...+1=2n(n1)O(n2)

可以看出,最优情况和最差情况的复杂度差距还是比较大的。

参考资料

《算法设计与分析基础》第三版以及老师的课件

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花无凋零之时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值