Sample K算法
0、题目来源
最近去国内某牛叉互联网公司面试,出了一道算法题,看似简单,但是真正的答案十分巧妙。故此回忆并将原题以及解题思路记录下来,供大家学习:
随机的选取容量为N的数组中的k个元素,要求是不能重复选取,并且不能删除数组中的元素,只能够进行交换。其中 k≤n 。
1、解题思路
对于这个问题我目前有两种解法:
- 蓄水池算法 ;
- 交换元素法;
下面我就将这两种算法解决该问题的思路进行详细的解释。
1.1 蓄水池算法解题思路
蓄水池算法的详细原理的解释和证明不是本文的重点,读者可以去百度上搜索(我发誓,我绝对会)。但是本文还是会简单的介绍一下它的大致的意思:对于一个未知大小的数组,从其中最多选取K个不重复的数据,保证每个数据被选取的概率大小完全一样。 如果你搞明白了蓄水池算法的思路,你就会明白它和本题的题意其实是完全一模一样。并且很清楚的知道它的算法复杂度为 O(n) 。
1.2 交换元素法解题思路
第二种解题思路就更巧妙了,因为它不需要 O(n) 的复杂度,仅仅需要的是 O(k) 的复杂度。因为题目中我们知道 k≤n ,所以O(k) 的复杂度比O(n) 的复杂度更优。接下来我将解释交换元素法的原理以及必要性。
首先,我们设想一下,如果题目不要求不重复选取的条件,那么我们很容易的想得到 O(k) 的算法。也就是依次从数组中随机的选取k个数字,不管他重复与否。但是现在题目的要求是不重复,当n很大k很小的时候,采用上述的方法也许可行,但是当k —> n 时,那时候随机选取的重复的概率将趋向于1,此法不可行。 再进一步设想,如果我们把已经选取的数据与未被选取的数据能够分隔开的话,那么这样的话,我们是不是就可以毫无顾忌的在未被选取的数据集合中进行选取而保证不会出现过重复? 答案是肯定的。 具体的做法如下:逻辑上将元素分成前n-k个元素,和最后的k个被选的元素。这样就能把元素分隔开。每次迭代的从前n-k个元素中依次选取第k个元素,此时将被选取的元素与第n-k个元素进行交换。这样就能保证,当k递增的时候,未被选取的元素总在前n-k个中,所以随机选取的时候不会出现重复的情况。而且,我们只需要k次操作就能完成所有的元素的选取,因此算法复杂度为 O(k) 。
1.3 上述两种算法优缺点
1.3.1 蓄水池算法
- 优点:不会改变元素的顺序;
- 缺点:算法复杂度高于交换元素法。
1.3.2 交换元素法
- 优点:算法复杂度最优;
- 缺点:改变了原始数组的数据出现的顺序;
所以,针对具体的问题场景和要求我们可以在上述两种算法中选择一种。
2、贴上源码(Java)
本文题目的要求来说,算法2是最优的算法。所以给出的代码是第二种算法的实现。如果你因为对Java不熟悉看不懂源码,那么我只能说,怪我咯……
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* Created by Administrator on 2017/7/23.
*/
public class Test {
public static void main(String[] args) {
int N = 100;
int k = 50;
// 产生一个1~100的整形数组
List<Integer> src = new ArrayList<Integer>();
for (int i = 0; i < N; i++) {
src.add(i + 1);
}
// 定义结果数组并获取数据
List<Integer> ans;
ans = sample(src, k);
// 输出结果,并检验结果
for (int i = 0; i < ans.size(); i++) {
System.out.print(String.format("%-5d", ans.get(i)));
if (i % 10 == 9) {
System.out.println();
}
}
}
public static List<Integer> sample(List<Integer> src, int k) {
int rear = src.size();
int index, tmp;
Random random = new Random(System.currentTimeMillis());
List<Integer> ans = new ArrayList<Integer>();
for (int i = 0; i < k; i++) {
// 随机获取数组所以并将其添加到结果数组中
index = random.nextInt(rear);
ans.add(src.get(index));
// 交换数组数据
tmp = src.get(index);
src.set(index, src.get(rear - 1));
src.set(rear - 1, tmp);
// 将随机选取的数组索引下界范围前移
rear--;
}
return ans;
}
}
运行结果:
32 66 16 3 82 20 97 61 74 8
19 100 23 11 42 35 75 13 15 89
84 81 9 55 38 94 67 71 96 58
30 24 57 5 12 90 95 63 99 14
17 88 69 53 46 52 21 39 93 6
- 程序员你为什么这么累【续】:编码习惯之配置规范
- Spring Security (一) Architecture Overview
- Spring Security (二) Guides
- 一个 android 的框架
- Spring Cloud实战小贴士:Ribbon的饥饿加载(eager-load)模式
- android常用接口(二)
- Spring Cloud实战小贴士:Zuul的饥饿加载(eager-load)使用
- RxAndroid完全教程
- 全能型反汇编引擎 – Capstone-Engine
- Hijack攻击揭秘
- 都在说微服务,那么微服务的反模式和陷阱是什么(二)
- Spring Boot 2.0 - WebFlux framework
- Spring Cloud构建微服务架构:服务网关(路由配置)【Dalston版】
- SpringCloud实战小贴士:Zuul的路径匹配
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Android ScrollView嵌套横向滑动控件时冲突问题
- Android用户输入自动提示控件AutoCompleteTextView使用方法
- Android自定义View实现水波纹效果
- Android如何获取系统通知的开启状态详解
- Handler实现线程之间的通信下载文件动态更新进度条
- Android下Button实现图文混排效果
- android handler.post和handler.sendMessage的区别和联系
- 3小时带你开发一款商城类小程序(零基础入门小程序系列)
- Android 隐式Intent的实例详解
- Android编程使用加速度传感器实现摇一摇功能及优化的方法详解
- 微信小程序分页加载数据~上拉加载更多~小程序云数据库的分页加载
- TextView实现跑马灯效果 就这么简单!
- Android自带emoji表情的使用方法详解
- Android EditText实现输入表情
- Android编程实现使用Intent传输包含自定义类的ArrayList示例