背景
- 需求: 将学生做错的题,存入用户错题本;将符合一定错误率的错题,存入年级错题库;当学生进入错题重练时,随机给出15个错题。
- 过程: 最开始想的简单,每次取0-14序号的试题ID组装试题,但紧接着想到,如果每次都取0-14,那练10次,出现的题还是一样的或者说就跟小时候背英语词典一样,前面的都翻烂了,后边的崭新。 所以,每次给出的15个题,应该为当前用户选择题库中的随机15个。
- 问题: 怎样给出随机的15个试题ID?
一、解决过程:
1,通过随机函数random.nextInt()
- A: 先查出所有的试题Ids
- B: 生成15个0-Ids.size()的随机数
- C: 组装试题
- 问题: 可能会有重复(虽然概率不大)
2,利用hashset将重复生成的随机数排除
3,通过Collections.shuffle(),将list打乱取值
- 备注:最开始,我先将一个非空list打乱,然后如果大于15个,取0-14序号元素
- 后改为:如果大于15个元素,打乱list,取0-14号元素
- 目前结果: 我采用了Collections.shuffle()方法,将list打乱,然后取值。
二、后续问题:
1,shuffle方法的实现:(SHUFFLE_THRESHOLD=5)
@SuppressWarnings({"rawtypes", "unchecked"})
public static void shuffle(List<?> list, Random rnd) {
int size = list.size();
if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
for (int i=size; i>1; i--)
swap(list, i-1, rnd.nextInt(i));
} else {
Object arr[] = list.toArray();
// Shuffle array
for (int i=size; i>1; i--)
swap(arr, i-1, rnd.nextInt(i));
// Dump array back into list
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
ListIterator it = list.listIterator();
for (int i=0; i<arr.length; i++) {
it.next();
it.set(arr[i]);
}
}
}
分析这段逻辑: 当list长度小于5时,或者list 实现了RandomAccess接口时,直接交换,否则,先转换为数组,进行交换,然后采用ListIterator迭代器将数组转成list。
对于,这一段,问题可能是有4个,
- 为什么是长度小于5,而不是其他的值?
- 为什么要转成数组处理?
- 在打乱顺序的过程中,这个list被别的线程使用怎么处理?
- 为什么使用的是ListIterator迭代器?
第一个问题: 好吧,其实我也还不清楚(我觉得,这也是和各个list的效率有关的。5个,可能只是开发者测试的一个值,也可能是一个幸运数,who know呢)
第二个问题: 主要是从LinkedList和ArrayList的效率来说的。 在常用的List接口实现中,ArrayList底层通过数组,而LinkedList则是链表,大家都知道,对于数组来说,直接通过下标取值,然后赋值就OK了,但对于链表来说,则。。。。。。呵呵呵呵了。 所以,转换为数组,是为了效率。 PS:List接口的另一个实现Vector,底层也是通过数组实现。
第三个问题: 首先对于转换为数组的LinkedList来说,数组本身就是一个复制了list的局部变量,它的更改,不影响原List。 而对于ArrayList这样的来说,先看看它的swap交换方法:
@SuppressWarnings({"rawtypes", "unchecked"})
public static void swap(List<?> list, int i, int j) {
// instead of using a raw type here, it's possible to capture
// the wildcard but it will require a call to a supplementary
// private method
final List l = list;
l.set(i, l.set(j, l.get(i)));
}
在它的交换中,先将list转换为了临时变量 l,并且定义为final不允许变更。 可见,第一是真正发生变动的,是临时变量 l,并不是原list,以此确保原list 可用。 而定义为final, 是保证在此循环交换结束前,此list始终是同一个list,即,保存了每一次交换的list。
第四个问题: 不是这次总结的兴趣所在。。。。。百度百度,看看代码就知道啦。 listIterator继承了Iterator接口
2,问题:
以shuffle的实现可知,他生成了随机数,然后做了全部list的交换。这里有一个问题,就是刚开始说到的问题,如果随机数生成了两个一样的呢? 也会交换,因为我最开始的做法是直接取随机数的下标,如果生成了两个5,那我最后组装的15个元素,就会出现两个 5号元素的内容。 而如果是交换,第一次是 1 号和5号交换, 5号的位置,变成了1号的内容。 就算后边再生成 5号随机数,也实现了交换。
然而真正的问题是: 我的目的是取出15个随机的试题Id而已,并不想花费大力气去打乱整个list。假设这个list的容量足够大,如果我仍然打乱再取值,这个效率会比较低。所以,我目前觉得,以当前的情况,我通过随机函数生成15个随机数,借用hashset的不可重复性,保证15个不重复即可。 然后,通过下标从list中取值,我觉得这是更高效率的方式。
三、其他方式出题
结合到第2个问题,我真正的意图是去生成15个不重复的随机数(即随机试题Id么?)并不是,我只是希望,用户错题记录中的错题,不会出现一个题练不过,就始终没有下一题。或者15个题,不停的被重复练,而有些题根本练不到。
所以,针对于此,我有了以下备选出题方案(前提:先定知识点,再定题):
1,以时间点排序, 始终给出距离当前最久的错题。出题之后,将原纪录剔除。如果再答错,以当前时间写入记录。 ——优点: 以一个相对较快的速度,轮询完整个错题记录。 缺点:受限于时间排序
2,以全部用户的错题率出题, 即,如果用户的这个题在所有用户中的错误率都很高,优先给出。 ——优点:能够练到易错题,可以提升学生的能力。 缺点:增加额外的逻辑,还有一点就是:如果错题率都相当呢?一下子又回到解放前了。
所以,我其实还是比较喜欢,全盘洗牌!