线性时间选择算法
最近算法课的知识点之一,自己以前没遇见过(是我菜了没错),这次写一篇。
那么什么是线性时间选择呢???一句话,在线性时间内完成选择。
一般情况下是这样的,我们想要找出一个数组中的最大值或最小值,那就只需要一次排列,然后输出第一个或最后一个元素就行了,但如果是要找出一个数组中的第k小的元素呢?
-
在某些特殊情况下,很容易设计出解选择问题的线性时间算法。如:当要选择最大元素或最小元素时,显然可以在O(n)时间完成。(遍历一次)
-
一般的选择问题,特别是中位数的选择问题似乎比最小(大)元素要难。但实际上,从渐近阶的意义上,它们是一样的。也可以在O(n)时间完成。
之前我记得我更过一篇,里面说到就是先排个序,然后通过下标去得到,因此这个问题的解决效率取决于排序算法的效率。
但是现在不一样了,我们有了更好的办法。
随机划分线性选择
分析
随机划分线性选择。模仿快速排序算法,对数组在left——right范围内进行一次划分(partition方法,我们这里默认采用随机元素为基准),得到基准下标i,不大于基准的在左边,比基准大的在右边。计算出left——i(包含i)中有多少个元素(j),与k进行比较。
- 如果k<j,说明当前left——right中第k小的在左边序列中,那么从left——i-1继续上述步骤寻找第k小。
- 如果k=j,说明基准i就是left——right中第k小,返回a[j]。(因为前面的都不大于它,后面的都大于它,不管左边是序列怎么排的,整体第k(j)小就是他)
- 如果k>j,说明第k小在右边序列,大于基准a[i],那么从i+1——right继续划分,并将k改为k-j。(整体序列提出了前j个元素,那么第k小在剩下的序列中就是第k-j小了)
一直到left=right的时候就可以返回a[left]了(其实我觉得如果有上述2的情况的话就不需要这个了)
这是课堂上PPT给的思路
这是算法导论的(就是我刚刚说的)
代码
/*
* @Title randomizedSelect
* @Description 基于随机元素为基准的线性时间选择
* @author 滑技工厂
* @Date 2020/3/26
* @param [a, L, R, k -> L---R中的第k小]
* @return int
* @throws
*/
public static int randomizedSelect(int[] a, int L, int R, int k) {
if (L == R)
return a[L];
//获取为基准的随机元素的下标
int i = randomizedPartition(a, L, R);
//j为划分后左序列到基准(包含基准)的元素个数
int j = i - L + 1;
if (k < j)//如果k小于j,说明在基准i的左边
return randomizedSelect(a, L, i - 1, k);
else if (k == j)//
return a[i];
else//k大于j 说明在i的右边序列
return randomizedSelect(a, i + 1, R, k - j);
}
利用中位数线性时间选择
分析
算法的思路:如果能在线性时间内找到一个划分基准使得按这个基准所划分出的2个子数组的长度都至少为原数组长度的ε倍(0<ε<1),那么就可以在最坏情况下用O(n)时间完成选择任务。例如,当ε=9/10,算法递归调用所产生的子数组的长度至少缩短1/10。所以,在最坏情况下,算法所需的计算时间T(n)满足递推式T(n)<=T(9n/10)+O(n)。由此可得T(n)=O(n)。
先描述下过程。
将n个输入元素划分成n/5个组,每组5个元素,最只可能有一个组不是5个元素。用任意一种排序算法,将每组中元素排好序,并取出中位数,共n/5个。
递归调用Select来找出这n/5个元素中的中位数。如果n/5是个偶数,就找它两个中位数中较大的一个。以该元素作为划分基准。
这种情况下,找出的基准x至少比3(n-5)/10个元素大,同理也比3(n-5)/10个元素小。(下图中箭头指向是从大到小,红+蓝的数量为3(n-5)/10)而当n>=75时,3(n-5)/10>=n/4所以按此基准划分所得的两个子数组的长度都至少缩短1/4。
得到划分基准后,后面就和第一个一样,计算左序列个数,和k进行比较。判断在左序列还是右序列,在递归调用该方法(k在右序列仍要减j),直到k=j为止,返回那个基准。
例题
代码
/*
* @Title select
* @Description 利用中位数线性时间选择
* @author 滑技工厂
* @Date 2020/3/27
* @param [a, l, r, k]
* @return int
* @throws
*/
public static int select(int[] a, int l, int r, int k) {
if (r - l < 75) {
insertSort(a, l, r); //用插入排序进行排序
return a[l + k - 1];
}
int group = (r - l + 5) / 5;
for (int i = 0; i < group; i++) {
int left = l + 5 * i;
int right = (l + i * 5 + 4) > r ? r : l + i * 5 + 4; //如果超出右边界就用右边界赋值
int mid = (left + right) / 2;
insertSort(a, left, right);
swap(a, l + i, mid); // 将各组中位数与前i个
}
int pivot = select(a, l, l + group - 1, (group + 1) / 2); //找出中位数的中位数
int p = partition(a, l, r, pivot); //用中位数的中位数作为基准的位置
int j = p - l + 1; //leftNum用来记录基准位置的前边的元素个数
if (k == j)
return a[p];
else if (k < j)
return select(a, l, p - 1, k);
else //若k在基准位子的后边,则要从基准位置的后边数起,即第(k - leftNum - 1)个
return select(a, p + 1, r, k - j - 1);
}
详细代码去我的G站
作业又完成了一个 (^-^)V