problem
元素选择问题的一般提法是:给定线性序集中n个元素和一个整数k,1<=k<=n,要求找到这n个元素中第k小的元素。
solution
随机时间线性选择
- 教材《算法设计与分析(第三版)》(王晓东)中这部分的算法解释怎么都看不懂,只能自己再琢磨一次。自己带入具体的例子,一步一步按照代码流程走下来,算法明白是明白了,但是我还是不太明白背后的原理。
- 首先是用的随机划分算法,这个题目类似一个LeetCode的题目,印象中做过,找到了就来补上是什么题目。
- 流程:
- 终止条件是递归到当前数组只有一个元素的时候,当l(数组左边界下标)==r(右边界下标),返回当前数字。依据k的范围(1<=k<=n),一定能找到。
- 如果不是,就随机划分该数组,假设该位置为i,(l<=i<=r)
- 计算出l-i元素个数j
- 如果k<=j,继续递归,传入的参数是l,i,k
- 如果就k>j,继续递归,传入的参数是i+1,r,k-j
代码
import java.util.*;
public class Main {
public static void main(String[] args) {
int [] nums=new int []{8,31,60,33,17,4,51,57,49,35,11,43,37,3,13,52,6,19,25,32,54,16,5,41,7,23,22,46,29};
// 排序之后:
//[3, 4, 5, 6, 7, 8, 11, 13, 16, 17, 19, 22, 23, 25, 29, 31, 32, 33, 35, 37, 41, 43, 46, 49, 51, 52, 54, 57, 60]
int k=18;
System.out.println("第k小值为"+randomizedSelect(nums,0,nums.length-1,k));
}
/**
* 线性时间选择——随机选取
* @param nums
* @param l
* @param r
* @param k
* @return int
*/
public static int randomizedSelect(int [] nums,int l,int r,int k){
if(l==r){
return nums[l];
}
// 得随机取值的标准值i
int i=randomizedPartition(l,r,nums);
// 计算l到i的个数j
int j=i-l+1;
if(k<=j){
return randomizedSelect(nums,l,i,k);
}else{
return randomizedSelect(nums,i+1,r,k-j);
}
}
/**
* 随机取值划分并进行一次快排
* @param l
* @param r
* @param nums
* @return int
*/
public static int randomizedPartition(int l,int r,int [] nums){
// 随机再[l,r]中选取一个数
Random random=new Random();
// nextInt 取的是[0,r)
int i=random.nextInt(r)%(r-l+1)+l;
System.out.println("得到的随机数为: "+i);
System.out.println("比较的标准为:"+nums[i]);
// 这里有点类似于快排了,但是不往下递归
// 先要交换一下位置,把基准换第一个位置上,进行快排
int tnum=nums[i];
nums[i]=nums[l];
nums[l]=tnum;
// 开始快排
int tl=l;
int tr=r;
while(tl<tr){
while(tl<tr && nums[tr]>tnum){
tr--;
}
if(tl<tr){
nums[tl]=nums[tr];
tl++;
}
while(tl<tr && nums[tl]<tnum){
tl++;
}
if(tl<tr){
nums[tr]=nums[tl];
tr--;
}
}
nums[tl]=tnum;
System.out.println("最终标准所在位置为: "+tl);
return tl;
}
}
中位数时间线性选择
- 流程:在上面的随机选择的情况变成找中位数
- 按照教材的解释如下:
- 书本中内容看不懂,可以参考这个画图的思路理解怎么做的 线性时间选择
- 我的总结就是:
这种方法是基于快排思想的,而快排性能最好的时候,是输入的序列是均匀分布,不是正序或是逆序排列好的,(因为那样的切分是最慢的)所以要做到让每次递归时候切分的位置都尽可能左端的数字等于右端的数字数目。而利用中位数的位置的特殊性就几乎能做到。而对中位数的位置的选择所使用的时间复杂度也可以在O(N)中做到。这就完成了把复杂降低到了O(n)
代码
public class Main {
public static void main(String[] args) {
int [] nums=new int[150];
for(int i=0;i<150;++i){
nums[i]=i*3-i*i;
// nums[i]=i;
}
int k=60;
System.out.println("第k小值为"+select(nums,0,nums.length-1,k));
}
public static int select(int [] nums,int l,int r,int k){
// 小于75个后直接快速排序,然后给出答案
// 这是一个递归终止条件,小于75个就不再划分,直接返回中位数
if(r-l<75){
quickSort(nums,l,r);
return nums[l+k-1];
}
// 从第0组计算,(r-l+1)有多少个元素,(r-l+1)/5有多少组,最后减掉1从0开始
for(int i=0;i<=(r-l-4)/5;++i){
// 每5个一组都要排序
quickSort(nums,l+5*i,l+i*5+4);
// 中位数交换到数组前面去
int temp=nums[l+i*5+2];
nums[l+i*5+2]=nums[l+i];
nums[l+i]=temp;
}
// 找中位数的中位数
int x=select(nums,l,l+(r-l-4)/5,(r-l+6)/10);
int p=partition(nums,l,r,x);
int j=p-l+1;
if(k<=j){
return select(nums,l,p,k);
}else{
return select(nums,p+1,r,k-j);
}
}
public static void quickSort(int [] nums,int l,int r){
if(l<r){
int q=partition(nums,l,r,nums[l]);
quickSort(nums,l,q-1);
quickSort(nums,q+1,r);
}
}
public static int partition(int [] nums,int l,int r,int value){
// 找到该值得位置,放到第一个去
int pos=l;
for(int i=l;i<=r;++i){
if(nums[i]==value){
pos=i;
break;
}
}
int temp=nums[pos];
nums[pos]=nums[l];
nums[l]=temp;
int i=l;
int j=r+1;
int x=nums[l];
while(true){
// 最坏在等于x的位置肯定会停下来的
while(nums[++i] <x && i<r);
while(nums[--j] > x );
if (i>=j){
break;
}
temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
nums[l]=nums[j];
nums[j]=x;
return j;
}
}
总结
理解了好久,终于算是大概弄懂了吧,代码也是调试了n次才搞定。害,脑子不够用~