需求如下
需要实现以下的单词拼写校验功能:
字母分状态展示
- 等待输入状态
- 已输入
- 正确
- 漏写
- 多写
- 错写
简单的描述一下需求就是,需要校验用户输入的单词,和正确的单词之间的差异。
核心算法来源
本次不讨论该题目怎么解决,直接给出ac的代码:
kotlion
class Solution {
fun minDistance(word1: String, word2: String): Int {
//dp[i][j] 表示 word1[0..i] 和 word2[0..j] 的最小编辑距离
val dp = Array(word1.length + 1) { IntArray(word2.length + 1) }
//第一列:word1[0..i] 变成空串的最小编辑距离
for (i in 1..word1.length) {
dp[i][0] = i
}
//第一行:空串变成 word2[0..j] 的最小编辑距离
for (j in 1..word2.length) {
dp[0][j] = j
}
for (i in 1..word1.length) {
for (j in 1..word2.length) {
if (word1[i - 1] == word2[j - 1]) {
//如果 word1[i] == word2[j],则不需要编辑操作
dp[i][j] = dp[i - 1][j - 1]
} else {
//如果 word1[i] != word2[j],则 dp[i][j] 可以由以下三种操作得到:
//1、如果将 word1[i] 替换成 word2[j],则 dp[i][j] = dp[i-1][j-1] + 1
//2、如果在 word1[i] 后面添加一个 word2[j],则 dp[i][j] = dp[i][j-1] + 1
//3、如果将 word1[i] 删除,则 dp[i][j] = dp[i-1][j] + 1
dp[i][j] = minOf(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
}
}
}
return dp[word1.length][word2.length]
}
}
如何将上述的算法应用到需求里面?
需求告诉我们可以对输入的单词做出下面4种动作,从而让输入的单词变为正确的单词。
- 不操作 [正确的情况]
- 替换
- 删除
- 添加
那我们先定义上述的动作:
kt
/**
* INIT:初始状态
* NOOP:不操作
* INSERT:插入
* DELETE:删除
* REPLACE:替换
* */
enum class EditAction {
INIT, NOOP, INSERT, DELETE, REPLACE
}
我们在回头看一下上述编辑距离的ac代码,发现只能知道最小的编辑距离,并不能知道是执行的那个动作。
那就记录下该动作:
kt
data class EditStep(
val action: EditAction,
val distance: Int
)
但是还有问题,即使使用了上述的EditStep,我们只能知道最后一步的动作是什么,无法知道整个流程的所以的动作。 我们这里选择链表的方式,通过添加prev的属性来记录上一步是什么,这样我们就能通过遍历链表的方式得知整个流程的动作,修改EditStep:
kt
data class EditStep(
val action: EditAction,
val distance: Int,
val prev: EditStep?
)
我们结合上面的分析和编辑距离的ac代码,得到如下的方案:
kt
fun minEditDistance(except: CharSequence, actual: CharSequence): List<EditStep>? {
if (except.isEmpty() && actual.isEmpty()) {
return null
}
val editDp = Array(except.length + 1) {
Array(actual.length + 1) {
EditStep(EditAction.INIT, 0, null)
}
}
//第一行: 表示except 与空串的编辑距离
for (row in 1..actual.length) {
editDp[0][row] = EditStep(EditAction.DELETE, row, editDp[0][row - 1])
}
//第一列: 空串 与 actual 的编辑距离
for (col in 1..except.length) {
editDp[col][0] = EditStep(EditAction.INSERT, col, editDp[col - 1][0])
}
for (row in 1..except.length) {
for (col in 1..actual.length) {
if (Character.toLowerCase(except[row - 1]) == Character.toLowerCase(actual[col - 1])) {
val noopEditStep = editDp[row - 1][col - 1]
editDp[row][col] = EditStep(EditAction.NOOP, noopEditStep.distance, noopEditStep)
} else {
val replaceEditStep = editDp[row - 1][col - 1]
val insertEditStep = editDp[row - 1][col]
val deleteEditStep = editDp[row][col - 1]
editDp[row][col] = minOfEditStep(replaceEditStep, insertEditStep, deleteEditStep)
}
}
}
val editSteps = ArrayList<EditStep>()
var lastStep: EditStep? = editDp[except.length][actual.length]
while (lastStep != null && lastStep.action != EditAction.INIT) {
editSteps.add(lastStep)
lastStep = lastStep.prev
}
//这里得到step是后往前,所以我们需要反转一下
return editSteps.asReversed()
}
kt
private fun minOfEditStep(
replaceEditStep: EditStep,
insertEditStep: EditStep,
deleteEditStep: EditStep
): EditStep {
return if (replaceEditStep.distance <= insertEditStep.distance && replaceEditStep.distance <= deleteEditStep.distance) {
EditStep(EditAction.REPLACE, replaceEditStep.distance + 1, replaceEditStep)
} else if (insertEditStep.distance <= replaceEditStep.distance && insertEditStep.distance <= deleteEditStep.distance) {
EditStep(EditAction.INSERT, insertEditStep.distance + 1, insertEditStep)
} else {
EditStep(EditAction.DELETE, deleteEditStep.distance + 1, deleteEditStep)
}
}
上述方案就是对编辑距离这道题目ac代码的简单修改,各位观众姥爷可以先自己ac一下编辑距离这道题目,这样理解会方便很多。
这里我们先一下简单的测试:
kt
val result = minEditDistance("handsome", "hoodsomee")
result?.forEach {
Log.d("MinEditDistance", "Action: ${it.action}")
}
发现符合我们想要的,最后一步就是通过Action,给文本进行染色:
kt
private fun renderMatchResult(editSteps: List<EditStep>?, actual: String): Editable {
val spannable = SpannableBuilder()
var cursor = 0
editSteps?.forEach {
when (it.action) {
EditAction.NOOP -> {
spannable.nextSpannable(actual[cursor++].toString()).setColor(Color.GREEN)
}
EditAction.REPLACE -> {
spannable.nextSpannable(actual[cursor++].toString()).setColor(Color.RED)
}
EditAction.INSERT -> {
spannable.nextSpannable("_").setColor(Color.RED)
}
EditAction.DELETE -> {
spannable.nextSpannable(actual[cursor++].toString()).setColor(Color.GRAY)
}
else -> {/**之前过滤了INIT,所以不会有*/}
}
}
return spannable.finish()
}
最后我们就实现了这个需求:
最后
做Android开发也有两年半的时间了,平时工作中很少能用到动态规划去处理问题,难得遇到一次这样的需求,故记录下来和大家分享一下。
文章中用的案例代码: github.com/Mrs-Chang/D...