前言
今天菊厂笔试碰到了一道最短编辑距离的算法题,其实用动态规划秒杀的解法大家可能都做了不下五次了。但是这个题目新颖在提出了一个最短编辑路径的实际应用并添加了很多的个性化限制。
背景
题目的背景是为一款语音识别系统设置一个评分系统,其中输入为两条字符串,一条为原句的字符串 target
,另一条为系统输出的字符串 output
,并且字符串有如下的一些前提条件。
- 字符串只能包含
数字
字母
!
,
.
?
- 字符串以单词为单位进行比较,单词与单词之间用空格隔开,但标点符号除外,每一个标点符号单独组成一个单词。例如,
ab?
为两个单词,而ab2
为一个单词。 - 增添或删除一个单词记为一个偏差,但替换因为需要先增加后删除所以记为两个偏差。例如,
a
和ab
有一个偏差, 但a
和bb
有两个偏差 !
.
,
这三个标点符号视为相等,但?
和它们不同。例如,a!
和a.
没有偏差,但a!
和a?
有两个偏差。- 字母间不区分大小写
最后输出的结果为偏差数以及原字符串的单词数
思路
很容易联想到最短编辑路径,但是这里不是要我们以字符为单位进行比,而是以“单词”为单位。因此首先要做的第一步应该就是先将原字符串转化成一个“单词”数组,然后再以单词数组进行比较。
转化为“单词”数组
如果有正则表达式玩的特别溜的大佬欢迎在下方留言,小弟不才只能介绍一种笨方法?。
我们首先声明一个数组 word
,当遇到字母或数字则直接 push()
进去,如果遇到空格或标点符号则将 word
转化为字符串并压入结果数组 res
中,并将那三种相同的标点符号都转化为 .
以方便后面的处理。代码如下
/**
* 将字符串转化为 word 数组,并将',' '!' '.' 全部替换成 '.'
* @param {string} str
* @return {string[]}
*/
function strSplit(str){
let res = [], word = [];
const normalSignals = ['!',',','.'];
const charRegex = /^[a-zA-Z0-9]+$/;
const addWordIntoRes = () => {
if(word.length){
res.push(word.join(''));
word = [];
}
};
for (let char of str){
if (charRegex.test(char)){
word.push(char);
} else{
switch (char){
case '?': // 问号
addWordIntoRes();
res.push('?');
break;
case ' ': // 空格
addWordIntoRes();
break;
default: // 其他三种符号统一加入'.'
addWordIntoRes();
res.push('.');
}
}
}
addWordIntoRes(); //以单词结尾并后面没有符号的时候,将最后一个 word 加入
return res;
}
最短编辑距离
这里就不赘述了,如果对该算法还有疑问可以看labuladong 的这篇文章。但还是有两点值得一提,一个是怎么不区分大小写地比较两个字符串?如果在这里愣了一下,没关系,你不是一个人? 我给出的答案是先用 toLowerCase()
都转化成小写的再比较嘛。第二个是最短编辑距离中其实不止三种操作——增,删,改。还有第四种,那就是当两个单位相同时啥也不做,直接取 dp[i-1][j-1]
。
String.prototype.equalsIgnoreCase = function(str){
if(typeof str !== 'string'){
throw new Error('Input must be string');
}
return this.length === str.length && this.toLowerCase() === str.toLowerCase();
}
/**
* 计算目标字符数组与输出目标数组之间的最小距离
* @param {string[]} output —输出字符数组
* @param {target[]} target -目标字符数组
* @return {number}
*/
function wordDistance(output,target){
const outputLen = output.length + 1, targetLen = target.length + 1;
let dp = new Array(outputLen).fill(0).map(_ => new Array(targetLen).fill(0));
for(let i = 1; i < outputLen; i++){
for(let j = 1; j < targetLen; j++){
if (output[i-1].equalsIgnoreCase(target[j-1])){
dp[i][j] = dp[i-1][j-1]; // output[i-1] 和 target[j-1]相同,不用做任何操作
} else{
dp[i][j] = Math.min(...[dp[i][j-1]+1,dp[i-1][j]+1,dp[i-1][j-1]+2]);
}
}
}
console.log(dp);
return dp[outputLen-1][targetLen-1];
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。