超星按题目的差异度排序

差异度越小的,排在前面。建议(1)当前页显示条目数设置为最大 (2)打开“显示题目详情”选项。

// ==UserScript==
// @name         超星按题目的差异度排序
// @namespace    http://tampermonkey.net/
// @version      2025-04-18-002
// @description  差异度越小的,排在前面。建议(1)当前页显示条目数设置为最大 (2)打开“显示题目详情”选项。
// @author       周利斌
// @match        https://mooc2-ans.chaoxing.com/mooc2-ans/qbank/questionlist?courseid=*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=chaoxing.com
// @grant        none
// @license      MIT
// ==/UserScript==

// 立即执行函数表达式,创建一个独立的作用域,防止全局变量污染
(function () {
    'use strict';

    /**
     * 计算两个字符串之间的 Levenshtein 距离
     * Levenshtein 距离指的是将一个字符串转换为另一个字符串所需的最少编辑操作(插入、删除、替换)次数
     * @param {string} s1 - 第一个字符串
     * @param {string} s2 - 第二个字符串
     * @returns {number} - 两个字符串之间的 Levenshtein 距离
     */
    function levenshteinDistance(s1, s2) {
        // 获取第一个字符串的长度
        const m = s1.length;
        // 获取第二个字符串的长度
        const n = s2.length;
        // 创建一个二维数组 dp 来存储子问题的解
        const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));

        // 双重循环遍历所有可能的子字符串组合
        for (let i = 0; i <= m; i++) {
            for (let j = 0; j <= n; j++) {
                // 如果第一个字符串为空,那么距离等于第二个字符串的长度
                if (i === 0) {
                    dp[i][j] = j;
                }
                // 如果第二个字符串为空,那么距离等于第一个字符串的长度
                else if (j === 0) {
                    dp[i][j] = i;
                }
                // 如果当前字符相同,那么距离等于前一个子问题的距离
                else if (s1[i - 1] === s2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1];
                }
                // 如果当前字符不同,那么距离等于插入、删除、替换操作中的最小距离加 1
                else {
                    dp[i][j] = 1 + Math.min(
                        dp[i - 1][j],      // 删除操作
                        dp[i][j - 1],      // 插入操作
                        dp[i - 1][j - 1]   // 替换操作
                    );
                }
            }
        }

        // 返回最终的 Levenshtein 距离
        return dp[m][n];
    }

    /**
     * 根据 Levenshtein 距离对 ul 列表中的 li 元素进行排序
     */
    function sortListByLevenshtein() {
        // 获取页面中 id 为 questionUl 的 ul 元素
        const ul = document.querySelector('ul#questionUl');
        // 如果未找到该 ul 元素,等待 1 秒后重新调用该函数
        if (!ul) {
            setTimeout(sortListByLevenshtein, 1000);
            return;
        }
        // 获取 ul 元素下的所有 li 元素,并将其转换为数组
        const lis = Array.from(ul.children);
        // 打印 ul 元素和 li 元素的数量,用于调试
        // console.log(ul, lis.length);

        // 提取每个 li 元素中类名为 choose-name 的元素的文本内容及其索引
        const items = lis.map((li, index) => {
            const text = li.querySelector('.choose-name')?.textContent.trim() || "";
            const optionLis = li.querySelectorAll(".questions-details .option li")
            const options = Array.from(optionLis).map(a => a.textContent.substring(2))
            options.sort()
            return { text: text + options.join("."), index };
        });
        console.log(items[0], items[1])
        // 创建一个二维数组 distances 用于存储所有元素之间的 Levenshtein 距离
        const distances = [];
        // 获取元素的数量
        const n = items.length;
        for (let i = 0; i < n; i++) {
            distances[i] = [];
            for (let j = 0; j < n; j++) {
                // 同一元素之间的距离设为无穷大
                if (i === j) {
                    distances[i][j] = Infinity;
                }
                // 如果已经计算过 j 到 i 的距离,直接使用
                else if (j < i) {
                    distances[i][j] = distances[j][i];
                }
                // 计算 i 到 j 的 Levenshtein 距离
                else {
                    distances[i][j] = levenshteinDistance(items[i].text, items[j].text);
                }
            }
        }

        // 用于存储排序后的元素
        const newItems = [];
        // 用于标记元素是否已经被处理过
        const used = new Array(n).fill(false);

        // 循环直到所有元素都被处理
        while (newItems.length < n) {
            // 初始化最小距离为无穷大
            let minDistance = Infinity;
            // 用于存储最小距离的元素对的索引
            let minPair = [];

            // 遍历所有未处理的元素对,找到最小距离的元素对
            for (let i = 0; i < n; i++) {
                if (used[i]) continue;
                for (let j = i + 1; j < n; j++) {
                    if (used[j]) continue;
                    if (distances[i][j] < minDistance) {
                        minDistance = distances[i][j];
                        minPair = [i, j];
                    }
                }
            }

            // 如果找到了最小距离的元素对
            if (minPair.length > 0) {
                // 将最小距离的元素对添加到新数组中
                newItems.push(items[minPair[0]], items[minPair[1]]);
                // 标记这两个元素已经被处理过
                used[minPair[0]] = true;
                used[minPair[1]] = true;

                // 遍历所有未处理的元素,找到与最小距离元素对中第一个元素距离相同的元素
                for (let i = 0; i < n; i++) {
                    if (!used[i] && distances[i][minPair[0]] === minDistance) {
                        // 将这些元素添加到新数组中
                        newItems.push(items[i]);
                        // 标记这些元素已经被处理过
                        used[i] = true;
                    }
                }
            }
            // 如果没有找到最小距离的元素对,将剩余未处理的元素添加到新数组中
            else {
                newItems.push(...items.filter((f, i) => !used[i]));
            }
        }

        // 获取排序后的元素的索引数组
        const newOrder = newItems.map(item => item.index);
        // 获取原始元素的索引数组
        const originalOrder = items.map(item => item.index);

        // 检查排序后的顺序是否与原始顺序不同
        const isOrderChanged = newOrder.some((value, index) => value !== originalOrder[index]);

        // 如果顺序发生了改变
        if (isOrderChanged) {
            // 清空 ul 元素
            while (ul.firstChild) {
                ul.removeChild(ul.firstChild);
            }
            // 打印原始顺序和新顺序,用于调试
            // console.log(originalOrder, newOrder)
            // 根据新顺序重新添加 li 元素到 ul 中
            newOrder.forEach(index => {
                ul.appendChild(lis[index]);
            });
        }
        // 等待 1 秒后再次调用该函数,实现定时排序
        setTimeout(sortListByLevenshtein, 1000);
    }

    // 调用排序函数
    sortListByLevenshtein();
})();