顺序统计的问题是:
给定一组元素,找到其中第k大(小)的元素
仅从功能实现上考虑,完全可以先对数组排序,然后直接取nums[k-1]
作为结果输出。但这个方法是需要先对数组排序的,所以效率为$\Theta(nlogn)$,我们的目标是找到一种$\Theta(n)$的方法
基于随机数的分治法
为了尽量压缩时间,可以考虑能不能在排序进行时就顺便找到所需元素,为此,可以使用快速排序算法中的分治思想
在快速排序算法中,每选定一个基准数pivot,就需要将所有小于pivot的元素移动至其左侧、所有大于pivot的元素移动至其右侧,当全部移动完成时,左右指针会正好相遇在pivot元素上
不难想到,当获得了pivot的索引值index
,就可以直接检查index
是否正好等于k-1
(假设k
指第k小的元素值),如果相等,则说明这个pivot就是要找的第k小的元素;如果不等,则进一步检查index
与k-1
之间的大小关系并确定新pivot:
- 如果
index > k-1
,说明要寻找的元素相较于pivot来说更小,那么去小于pivot的区域再找 - 如果
index < k-1
,说明要寻找的元素相较于pivot来说更大,那么去大于pivot的区域再找
以上过程完全可以用递归来实现(事实上这也是快速排序所进行的过程),而在区域中选取pivot实际上是随机取得的。另外,当传入的左右限制left == right
时,说明此区域仅有一个元素,需要寻找的只能是该元素,直接返回nums[left]
即可
代码实现(Java)
类的总体结构应为:
import java.util.Random;
class Find {
private int[] nums;
private int k;
public Find(int[] nums) {...}
public int getKElement(int k) {...}
private int randomSelect(int left, int right) {...} // 递归部分,选取随机数、划分后判断
private int partition(int random, int p, int q) {...} // 划分部分
private void swap(int i, int j) {...} // 用于交换数组元素的工具方法
}
先编写划分部分的方法,输入数组、随机索引值、左右限制,输出划分后pivot的索引值:
public int partition(int random, int p, int q) {
while (p != q) {
int pivot = nums[random];
while (pivot > nums[p]) p++;
swap(nums, p, random);
random = p;
while (pivot < nums[q]) q--;
swap(nums, q, random);
random = q;
}
return random;
}
编写递归部分:
private int randomSelect(int left, int right) {
if (left == right) return nums[left]; // 仅余一元素,必定是目标
Random rand = new Random();
int rInd = rand.nextInt(right - left + 1) + left;
int index = partition(nums, rInd, left, right);
if (index == k-1) return nums[index]; // 划分后,判断该随机元素是否命中
else if (index > k-1) return randomSelect(left, index-1, k); // index > k-1,去index左侧划分
else return randomSelect(index+1, right, k); // index < k-1,去index右侧划分
}
核心部分已经完成,只需要调用即可:
public int getKElement(int k) {
return randomSelect(0, nums.length-1);
}
通过测试输出,可以直观地看到这个算法的执行过程,通俗来讲就是一边进行快速排序,一边找到目标元素:
