由“交卷”功能引发的思考——对比两个字符串数组的差异
最近在做一个答题系统,在交卷的时候需要判断客观题的答题情况
客观题的题型有单选题、多选题、判断题
其中判断题可以当做单选题处理,而单选题也可以当做标准答案长度为一的多选题
所以最终只需要实现多选题的判定即可
一、需求分析
将标准答案和考生回答分别记为字符串数组 standard 和 answer
经过分析,多选题可能出现的情况有:
1. 完全正确
answer 中的所有元素都能在 standard 中找到,且 answer 和 standard 长度相等
2. 部分正确
answer 中的所有元素都能在 standard 中找到,但 answer 的长度小于 standard
3. 回答错误
answer 中至少有一个元素不在 standard 中
为了处理以上情况,可以设计一个排除 standard 和 answer 中重复项的函数,并基于各自剩下的元素来判断结果
/**
* 去除两个数组的重复项, 并返回处理后的两个数组
* ```ts
* 输入: [ ['A', 'B'], ['A', 'C'] ]
* 输出: [ ['B'], ['C'] ]
* ```
* @param data [string[], string[]]
* @returns [string[], string[]]
*/
function compareArray(data: [string[], string[]]): [string[], string[]] {
const [standard, answer] = data || [];
// ...
}
二、函数实现
这个 compareArray 函数内部逻辑和数组去重的过程很类似
// 关于数组去重的方案可以参考《JavaScript 高性能数组去重》
需要遍历数组 A, 拿每个元素去和数组 B 对比, 如果重复则在两个数组中都删除该元素
1. 基础版:
使用 indexOf 在 B 数组中查找重复项
若存在重复项,则将元素置空,最后使用 filter 过滤
不建议使用 splice 删除元素,因为它会修改原数组,不易维护遍历的过程
function compareArray(data: [string[], string[]]): [string[], string[]] {
const [standard, answer] = data || [];
// 拷贝原数组
const _standard = [...standard];
const _answer = [...answer];
for (let i = 0; i < _standard.length; i++) {
const k = _standard[i];
const answerIndex = _answer.indexOf(k);
if (answerIndex < 0) continue;
// 如果存在重复元素, 则将对应的元素标记为空
_standard[i] = '';
_answer[answerIndex] = '';
}
// 过滤空元素
return [
_standard.filter((x) => !!x),
_answer.filter((x) => !!x),
];
}
2. 进阶版
上面的基础版使用了 indexOf 来查找重复项,会存在重复遍历的情况
可采用空间换时间的思路,改用对象来判断重复项
另外还可以使用增量存储的方式,记录 standard 中未重复的元素,这样就可以省略最后一步的 filter
function compareArray(data: [string[], string[]]): [string[], string[]] {
const [standard, answer] = data || [];
// 用于记录 standard 数组的处理结果(增量存储)
const _standard: string[] = [];
// 将 answer 转为对象便于查询, 需要手动删除重复值
const _answerMap: Record<string, true> = answer.reduce(
(res, x) => ({ ...res, [x]: true }),
{},
);
for (let i = 0; i < standard.length; i++) {
const k = standard[i];
// 如果存在重复值, 则删除 answerMap 中的键
if (_answerMap[k]) {
Reflect.deleteProperty(_answerMap, k);
} else {
// 如果存在不重复, 则记录到结果数组中
_standard.push(k);
}
}
return [_standard, Object.keys(_answerMap)];
}
三、结果判断
文章的开头已经提到,多选题的结果存在这三种情况
/** 答题结果 */
export enum ANSWER_RESULT_ENUM {
/** 完全正确 */
CORRECT = '1',
/** 部分正确 */
NOTBAD = '2',
/** 回答错误 */
WRONG = '3',
}
结合上面的 compareArray 函数,可以这么来判断答题结果:
type AnswerItem = string[];
/** 检查单个问题的答题情况 */
function markingPapers(
answer: AnswerItem,
standard: AnswerItem,
): ANSWER_RESULT_ENUM {
// 未作答
if (!answer?.length) {
return ANSWER_RESULT_ENUM.WRONG;
}
const [_answer, _standard] = compareArray([answer, standard]);
// 完全正确
if (!_standard.length && !_answer.length) {
return ANSWER_RESULT_ENUM.CORRECT;
}
// 部分正确
if (!_answer.length && _standard.length > 0) {
return ANSWER_RESULT_ENUM.NOTBAD;
}
// 回答错误
return ANSWER_RESULT_ENUM.WRONG;
}