练习题可进行混合题型练习和各题型(单选、多选、判断)分项练习,可指定题目数量。
题库中包含各类型题目,题库中题量未必满足指定题目总数,在混合练习的前提下,如果题库中题目数足够,三种类型题目数量均分,否则,不满足均分后题目数量的题型取全部题目,剩下的均分,依次类推。如果题目总数不满足,则取题库中全部题目。
例:要取10道题进行混合练习,题库中共有2道单选题,5道多选题,8道判断题,那么应取2道单选,(10-2)/2=4道多选,10-2-4=4道判断。假如题库中有8到单选,1道多选,2道判断,那么应取1道多选,2道判断,和7道单选。
public class QuestionType {
// 混合题
public static final int ALL = 0;
// 单选题
public static final int RADIO = 1;
// 多选题
public static final int MULTI = 2;
// 判断题
public static final int JUDGE = 3;
}
@Data
@ApiModel(value = "各类题目数量", description = "各类题目数量")
public class QuestionNumber {
@ApiModelProperty("单选题数目")
private int radioCount;
@ApiModelProperty("多选题数目")
private int multiCount;
@ApiModelProperty("判断题数目")
private int judgeCount;
}
传参 int quType 题目类型 0-混合题 1-单选题 2-多选题 3-判断题,int totalNum 题目数量, Long repoId 题库id
List<QuQuestion> list = new ArrayList<>();
if (quType == QuestionType.ALL) {
// 混合题型联系,获取题库中各类题目数量
QuestionNumber qt = questionService.getQuestionNumberByType(repoId);
if (qt.getRadioCount() + qt.getMultiCount() + qt.getJudgeCount() <= totalNum) {
// 三种题目总数少于等于练习卷题目数量,取全部题目
list = questionService.listByRandom(repoId, null, null, totalNum);
} else {
if (qt.getRadioCount() < totalNum / 3 || qt.getMultiCount() < totalNum / 3 || qt.getJudgeCount() < totalNum / 3) {
// 至少有一种题型题目数量不够
if (qt.getRadioCount() < totalNum / 3) {
// 假如单选题目数量不够,检查多选题目数量是否足够
if (qt.getMultiCount() < (totalNum - qt.getRadioCount()) / 2) {
// 假如多选题目数量也不够,单选题多选题取全部,判断题重新取剩余数目
qt.setJudgeCount(totalNum - qt.getRadioCount() - qt.getMultiCount());
} else {
// 假如多选题数目足够,检查判断题数目是否足够
if (qt.getJudgeCount() < (totalNum - qt.getRadioCount()) / 2) {
// 假如判断题数目不够,单选题判断题取全部,多选题重新取剩余数目
qt.setMultiCount(totalNum - qt.getRadioCount() - qt.getJudgeCount());
} else {
// 多选题判断题都足够,多选题判断题数量均分
qt.setMultiCount((totalNum - qt.getRadioCount()) / 2);
qt.setJudgeCount(totalNum - qt.getRadioCount() - qt.getMultiCount());
}
}
} else {
// 单选题数目足够,检查多选题数目是否足够
if (qt.getMultiCount() < totalNum / 3) {
// 假如多选题目数量不够,检查判断题数目是否足够
if (qt.getJudgeCount() < (totalNum - qt.getMultiCount()) / 2) {
// 假如判断题数目不够,多选题判断题取全部,单选题重新取剩余数目
qt.setRadioCount(totalNum - qt.getMultiCount() - qt.getJudgeCount());
} else {
// 单选题判断题都足够,单选题判断题数量均分
qt.setRadioCount((totalNum - qt.getMultiCount()) / 2);
qt.setJudgeCount(totalNum - qt.getMultiCount() - qt.getRadioCount());
}
} else {
// 假如多选题数目足够,判断题数目不够,判断题取全部,单选题多选题数量均分
qt.setRadioCount((totalNum - qt.getJudgeCount()) / 2);
qt.setMultiCount(totalNum - qt.getJudgeCount() - qt.getRadioCount());
}
}
} else {
// 三种题型题目数量都足够
qt.setRadioCount(totalNum / 3);
qt.setMultiCount(totalNum / 3);
qt.setJudgeCount(totalNum - qt.getRadioCount() - qt.getMultiCount());
}
list = questionService.listByRandom(repoId, QuestionType.RADIO, null, qt.getRadioCount());
list.addAll(questionService.listByRandom(repoId, QuestionType.MULTI, null, qt.getMultiCount()));
list.addAll(questionService.listByRandom(repoId, QuestionType.JUDGE, null, qt.getJudgeCount()));
}
} else {
// 单题型练习,按照题库id、题目类型和总数筛选题目
list = questionService.listByRandom(repoId, quType, null, totalNum);
}
代码倒是生效了,注释也解释清楚了思路逻辑,但是这样写,将来万一要扩展,光逻辑不得写死在当场。另外测试时发现使用以下这个示例:
要取20道题进行混合练习,题库中共有15道单选题,8道多选题,1道判断题,代码出现了错误,细究之下发现上面冗长的代码依然少考虑了分支,比如说,8道多选,虽然满足20/3=6的第一次均分,但是在(10-1)/2=9的第二次均分时,代码缺少了比较8<9的情况而误判多选满足均分条件,因此单选和多选题的选择数量并不正确。
考虑到代码的可复用性和未来扩展,这里考虑引入多重循环,思路其实和之前几乎一致:每次找出均分后数目不足的题型,取其全部,然后再次均分剩余题型,再次取均分后数目不足的题型全部,如此往复,直到所有剩余题型均满足均分,那么取均分量;或所有题型均不满足均分量,都取全部。
int[] num = {qt.getRadioCount(), qt.getMultiCount(), qt.getJudgeCount()};
// 剩余总量
int remain = totalNum;
// 均分分母
int avg = num.length;
for (int i = 0; i < num.length; i++) {
if (num[i] < totalNum / num.length) {
// 题目数量不满足总数均分,取全部题目数量,num[i]没有变化
// 剩余数量更新
remain = remain - num[i];
// 更新均分分母
avg--;
}
}
for (int i = 0; i < num.length; i++) {
if (num[i] >= totalNum / num.length) {
if (avg > 1) {
// 剩余超过一个题型,还需要继续均分
num[i] = remain / avg;
// 剩余数量更新
remain = remain - num[i];
// 更新均分分母
avg--;
} else {
// 只剩最后一个题型,不再均分,假如题目数量大于剩余数量,取剩余数量
num[i] = Math.min(remain, num[i]);
}
}
}
以上代码使用两个for循环,如果题库中某些类型的题目不够,那么我们需要其他数量足够的类型的题目来补足总数,但是,仔细套入思路,会发现两个for循环并不能满足需求,因为我们无法保证只循环两次就可以使得所有题型满足均分条件。
为改变这个问题,我们需要及时更新remain/avg,而不是一直使用totalNum/num.length,这里就必须采用while的循环方式。
为使得代码便于理解,这里使用了第三种写法(p.s.前两种写法也依然可以继续修改满足需求,但是这里不再展开),思路与之前完全相反:每次均分剩余题目(最开始的剩余题目为题目总数)。针对每种题型,当其满足均分时取均分值;不满足时取全部,并不再计入下次均分。更新每种题型均分后的剩余量。然后用总剩余量再次均分还有剩余量的题型,满足均分时取均分值;不满足时取全部,如此往复,直到总剩余量清零。
这里定义了三个数组,分别用于标识原始题库中的各题型数量num,最终所取的各题型数量result,以及每种题型均分后的剩余量remain
以要取20道题进行混合练习,题库中共有15道单选题,8道多选题,1道判断题为例,初始阶段
num={15,8,1}; remain{15,8,1}; result={0,0,0}; total=20; avg=3; total/avg=6;
第一轮while循环之后
num={9,2,0}; remain{9,2,0}; result={6,6,1}; total=7; avg=2; total/avg=3;
第二轮while循环之后
num={6,0,0}; remain{6,0,0}; result={9,8,1}; total=2; avg=1;
最后进入avg==1的分支,得到最终结果result={11,8,1};
List<QuQuestion> list = new ArrayList<>();
// 各题型题目数量
int[] num = {qt.getRadioCount(), qt.getMultiCount(), qt.getJudgeCount()};
// 各题型剩余题目数量
int[] remain = num;
// 各题型应取题目数量
int[] result = new int[num.length];
// 剩余数量
int total = totalNum;
// 均分分母
int avg = num.length;
while (total > 0) {
int temp = avg;
// 每一轮循环,各类型题目取均分数量,若不满足均分数量,取全部,记录该题型剩余量
for (int i = 0; i < num.length; i++) {
if (remain[i] > 0) {
if (avg == 1 || total / avg == 0) {
// 只剩一个题型,取全部数值
result[i] = result[i] + Math.min(remain[i], total);
total = 0;
break;
}
if (remain[i] < total / avg) {
// 某题型题目数量不满足均分,取该题型全部数量,剩余量归零,分母减一
result[i] = num[i] + result[i];
remain[i] = 0;
temp--;
} else {
// 某题型题目数量满足均分,取均分数量,剩余量减去均分值
result[i] = total / avg + result[i];
remain[i] = remain[i] - total / avg;
}
}
}
// 更新剩余总量和均分分母
avg = temp;
total = totalNum - IntStream.of(result).sum();
}
list = questionService.listByRandom(repoId, QuestionType.RADIO, null, result[0]);
list.addAll(questionService.listByRandom(repoId, QuestionType.MULTI, null, result[1]));
list.addAll(questionService.listByRandom(repoId, QuestionType.JUDGE, null, result[2]));
以上逻辑已经满足了基本需求,但是仍然忽略了一种情况,即,剩余总量最后一轮有余数的情况。示例:应取题目20,题库中单选7,多选7,判断6。在当前逻辑的情况下,可能会出现{8,6,6}的误判。这也就说明total/avg=0这个分支的逻辑并不完整。好在这最后一轮的逻辑简单,只要稍加变化即可。
while (total > 0) {
int temp = avg;
// 每一轮循环,各类型题目取均分数量,若不满足均分数量,取全部,记录该题型剩余量
for (int i = 0; i < num.length; i++) {
if (remain[i] > 0) {
if (avg == 1) {
// 只剩一个题型,取全部数值
result[i] = result[i] + Math.min(remain[i], total);
total = 0;
break;
}
if (total / avg == 0) {
// 最后一轮,但是有余数total
if (remain[i] > 0) {
result[i]++;
remain[i]--;
total--;
}
} else {
if (remain[i] < total / avg) {
// 某题型题目数量不满足均分,取该题型全部数量,剩余量归零,分母减一
result[i] = num[i] + result[i];
remain[i] = 0;
temp--;
} else {
// 某题型题目数量满足均分,取均分数量,剩余量减去均分值
result[i] = total / avg + result[i];
remain[i] = remain[i] - total / avg;
}
}
if (total == 0) break;
// 避免死循环,强制跳出while
cnt++;
if (cnt > 10) {
break;
}
}
// 更新剩余总量和均分分母
avg = temp;
total = totalNum - IntStream.of(result).sum();
}
}
list = questionService.listByRandom(repoId, QuestionType.RADIO, null, result[0]);
list.addAll(questionService.listByRandom(repoId, QuestionType.MULTI, null, result[1]));
list.addAll(questionService.listByRandom(repoId, QuestionType.JUDGE, null, result[2]));