线性时间选择——寻找第k小的数

以下引用自:《算法设计与分析java版》

2.9 线性时间选择
  本节讨论与排序问题类似的元素选择问题。元素选择问题的一般提法是:给定线性序集中n个元素和一个整数k,1≤k≤n,要求找出这n个元素中第k小的元素,即如果将这n个元素依其线性序排列时,排在第k个的元素即为要找的元素。当k=1时,就是要找最小元素;当k=n时,就是要找最大元素;当k=(n+1)/2时,称为找中位数。
  
  在某些特殊情况下,很容易设计出解选择问题的线性时间算法。例如,找n个元素的最小元素和最大元素显然可以在O(n)时间完成。如果k≤n/logn,通过堆排序算法可以在O(n+ klogn)= O(n)时间内找出第k小元素。当k≥n- n/logn时也一样。
  
  一般的选择问题,特别是中位数的选择问题似乎比找最小元素要难。但事实上,从渐近阶的意义,上看,它们是一样的。一般的选择问题也可以在O(n)时间内得到解决。下面要讨论解–般的选择问题的分治算法randomizedSelect。该算法实际上是模仿快速排序算法设计出来的。其基本思想也是对输人数组进行递归划分。与快速排序算法不同的是,它只对划分出的子数组之一进行递归处理。

题目一:在一个无序数组中,找出其中的第一小的值和第二小的值。

通过变量 min1 min2 很容易就可以完成算法。其中,min1是最小的元素,min2是次小元素。

void Select_2Min2(int* br, int n)
{
	if (NULL == br || n < 2) return;
	int min1 = br[0] < br[1] ? br[0] : br[1];	// 第一小:最小  
	int min2 = br[0] + br[1] - min1;			// 第二小:次小
	for (int i = 2; i < n; i++)
	{
		if (br[i] < min2)
		{
			if (br[i] < min1) 					// ( , min1)   min1 = br[i]
			{
				min2 = min1;	// 更新次小值
				min1 = br[i];	// 更新最小值
			}
			else	min2 = br[i];				// [min1,min2)  min2 = br[i]
		}
	}

	cout << "min1: " << min1 << "min2: " << min2 << endl;
}
题目二:在一个无序数组中,找出其中的第k小的值。

对于此类问题,我们可以继续按照题目一的解法,通过设置k个变量,分别为 min1 ... mink 解决,但未免过于麻烦。我们也可以采用排序的方式,使之无序数组有序,而后返回对应的索引即可。但是我们其实只是需要第k小这一个元素罢了,不需要专门为其进行一次排序。

因此,我们可以使用类快排的方法。

快速排序每一趟排序都会将原数组分成两部分,基准左边基准右边。(升序排列)而基准的左侧都是比基准小的元素,也就是说当前基准的位置就是第m小的元素。

下标0123456789
元素532861014181119

如上表:

  • 5号下标的元素10为当前基准,而array[0] ~ array[4] 都小于10,也就是说array[5]第6小元素。
  • 如果我们要找第3小,那么我们只需在基准左侧再进行一趟快排即可得到。
  • 如果我们要找第8小,那么我们只需在基准右侧再进行一趟快排即可得到。

由此一来,我们就可以按照快排的特点设计算法了。

template<typename _T>		// 按首位基准划分,一趟快排
int OnePartition(_T* arr, int left, int right)
{
	int i = left + 1, j = i;
	while (j <= right)
	{
		if (arr[j] <= arr[left])
		{
			swap(arr[i++], arr[j++]);
		}
		else j++;
	}
	i = i - 1;
	swap(arr[left], arr[i]);
	return i;		// 返回基准下标
}
template<typename Type>
const Type& Select_K(Type* br, int left, int right, int k)
{
	int i = OnePartition(br, left, right);		// 找基准位置
	int pos = i - left + 1;						// 找到第pos小
	if(pos == k) return br[left];	// return br[i] // 是否找到第k小
	else if (k < pos) return Select_K(br, left, i-1, k);	// 左侧区间找
	else return Select_K(br, i + 1, right, k - pos);		// 右侧区间找
}
template<typename Type>
const Type& Select_K_Min(int* br, int n, int k)
{
	assert(NULL != br && n > 0 && k <= n && k > 0);
	return Select_K(br, 0, n - 1, k);
}
测试:
int main()
{
	int ar[] = { 89,100,67,78,23,34,56,12,45,90 };
	int n = sizeof(ar) / sizeof(ar[0]);

	Select_2Min2(ar, n);	// 问题一:第一、第二小

	for (int k = 1; k <= n; k++)	// 分别输出最小 ~ 最大元素
	{
		cout << "["<<k<<"]min ==>" <<Select_K_Min<int>(ar, n, k) << endl;
	}

	return 0;
}

输出:

min1: 12min2: 23
[1]min ==>12
[2]min ==>23
[3]min ==>34
[4]min ==>45
[5]min ==>56
[6]min ==>67
[7]min ==>78
[8]min ==>89
[9]min ==>90
[10]min ==>100
  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我叫RT

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

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

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

打赏作者

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

抵扣说明:

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

余额充值