错题重练—应用Collections.shuffle()及其思考

背景

  1. 需求: 将学生做错的题,存入用户错题本;将符合一定错误率的错题,存入年级错题库;当学生进入错题重练时,随机给出15个错题。
  2. 过程: 最开始想的简单,每次取0-14序号的试题ID组装试题,但紧接着想到,如果每次都取0-14,那练10次,出现的题还是一样的或者说就跟小时候背英语词典一样,前面的都翻烂了,后边的崭新。 所以,每次给出的15个题,应该为当前用户选择题库中的随机15个。
  3. 问题: 怎样给出随机的15个试题ID?

一、解决过程:

1,通过随机函数random.nextInt()

  1. A: 先查出所有的试题Ids
  2. B: 生成15个0-Ids.size()的随机数
  3. C: 组装试题
  4. 问题: 可能会有重复(虽然概率不大)

2,利用hashset将重复生成的随机数排除

3,通过Collections.shuffle(),将list打乱取值

  1. 备注:最开始,我先将一个非空list打乱,然后如果大于15个,取0-14序号元素
  2. 后改为:如果大于15个元素,打乱list,取0-14号元素
  3. 目前结果: 我采用了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,以全部用户的错题率出题, 即,如果用户的这个题在所有用户中的错误率都很高,优先给出。 ——优点:能够练到易错题,可以提升学生的能力。 缺点:增加额外的逻辑,还有一点就是:如果错题率都相当呢?一下子又回到解放前了。

所以,我其实还是比较喜欢,全盘洗牌!

猜你喜欢

转载自blog.csdn.net/u013034889/article/details/88739981