文件路径匹配 - 最长公共子序列

338 阅读1分钟

写在前面

你一定在编辑器中用过搜索文件功能【command + P】,不需要输入完整的地址,也能匹配到你想要的文件。有点像模糊搜索,但又更智能一点,因为它不需要你输入的字符在目标字符里是连着的,就像下面这样【可以看到很多结果里面的字符是分散的】: image.png

如何匹配

想了一圈字符串相关的匹配,又想到最后出现这种散列的结果,应该就是最长公共子序列了。

最长公共子序列

子序列的定义是: image.png 如"a","abd", "ace"都是"abcde"的子序列。

所以最长公共子序列也很好理解了,就像下面这样:

let a = "callmedk";
let b = "faldkc";
let res = "aldk"; // res是a和b的最长公共子序列

实现

核心内容是动态规划,即

设n1,n2分别为str1和str2的下标,状态方程S(n1, n2)有:

当str1[n1] != str2[n2]时,S(n1, n2) = max(S(n1 - 1, n2), S(n1, n2 - 1));

当str1[n1] == str2[n2]时,S(n1, n2) = S(n1 - 1, n2 - 1) + 1;

function longestChild(str1, str2) { 
    // 记录数组
    let record = Array.from({ length: str1.length }, () => { 
        return Array.from({ length: str2.length });
    });
    
    function find(index1, index2) { 
        if (index1 < 0 || index2 < 0) return { len: 0, match: [] };
        if (record[index1][index2] !== undefined) return record[index1][index2];
        
        let ans = record[index1][index2] = {
            len: 0, // 长度
            match: [], // 匹配的位置(两者的下标)
        }
        if (str1[index1] === str2[index2]) {
            ans.len = 1;
            ans.match.push([index1, index2]);

            let chAns = find(index1 - 1, index2 - 1);
            ans.len += chAns.len;
            ans.match.push(...chAns.match);
        } else { 
            let chAns1 = find(index1 - 1, index2);
            let chAns2 = find(index1, index2 - 1);
            ans = record[index1][index2] = chAns1.len > chAns2.len ? chAns1 : chAns2;
        }

        return ans;
    }

    // 稍微处理一下最终结果
    let ans = find(str1.length - 1, str2.length - 1);
    ans.match.reverse();
    let len = ans.len;
    let str = "";
    for (let i = 0, l = ans.match.length; i < l; i++) { 
        str += str1[ans.match[i][0]]
    };

    return {
        length: len,
        str: str,
        detail: ans
    }
} 

let ans = longestChild("callmedk", "faldkc");
console.log(ans);

结果:

image.png

完整流程

了解完最长公共子序列之后,来过一下整个路径匹配的流程

  • 获取所有文件的路径
  • 将搜索的字符串和所有路径做最长公共子序列的匹配【设字符串为s1】
  • 筛选出长度与s1相同的结果,即为最终答案

编辑器好像还根据最近使用文件做了排序,这个方面没有研究细节

写个demo

function search(target, source) {
    let res = [];
    for (let i = 0, l = source.length; i < l; i++) {
        let item = source[i];
        let ans = longestChild(item, target);
        if (ans.length === target.length) {
            // 匹配的单个文字换成特殊文字
            let lastIndex = -1;
            let hItem = "";
            for (let j = 0, k = ans.detail.match.length; j < k; j++) {
                let charIndex = ans.detail.match[j][0];
                let specialC = `<span style="color: red;">${item[charIndex]}</span>`;
                hItem += (item.slice(lastIndex + 1, charIndex) + specialC);
                lastIndex = charIndex;
            }
            if (lastIndex !== item.length - 1) {
                hItem += item.slice(lastIndex + 1, item.length);
            }
            res.push(hItem);
        }
    }

    return res;
}

let source = [
    "abcdef",
    "acbdefghij",
    "abcdefghijklm",
    "abcdefghijklmnopq",
    "abcdefghijklmnopqrst",
    "abcdefghijklmnopqrstuvw",
    "abcdefghijklmnopqrstuvwxyz"
]

let ipt = document.createElement("input");
let container = document.createElement("div");
document.body.innerHTML = ``;
document.body.appendChild(ipt);
document.body.appendChild(container)

ipt.addEventListener("input", (e) => {
    let content = e.target.value;
    if (!content) {
        container.innerHTML = ``;
        return;
    };
    let res = search(content, source);
    container.innerHTML = `
    <div>
        <h3>Search value: ${content}</h3>
        <h3>Source: </h3>
        <div>
            ${source.map(item => {
                return `<div>${item}</div>`
            }).join("")}
        </div>

        <h3>Search result: </h3>
        <div>
            ${res.map(item => {
                return `<div>${item}</div>`
            }).join("")}
        </div>
    </div`
});

最终效果

image.png