模糊匹配之——BK树与拼写纠正

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Kurozaki_Kun/article/details/82555772

介绍

拼写纠错功能常常出现在比较高级的文本编辑应用中,例如大家熟知的word,高级一点的IDE例如Jet Brains系列,在一些在线翻译上,也有自动校正拼写的功能,例如谷歌翻译。

原理

拼写纠正的实现方式有多种,这里使用的是一种名为BK树的数据结构,也叫作Burkhard-Keller树,是由Burkhard,Keller这两人提出来的,不过网上能找到的相关资料并不多,参见ACM文档https://dl.acm.org/citation.cfm?id=362003.362025

最短编辑距离

在介绍BK树思想之前,要介绍一个概念——莱文斯坦距离,又称Levenshtein距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括

  1. 将一个字符替换成另一个字符。
  2. 插入一个字符。
  3. 删除一个字符。

每经过一种操作编辑距离+1,当然也能够根据实际需求设定不同的权值。例如从 cute 到 cat,需要将u替换为a,删除e,因此它的的最短编辑距离为2。

定义dist(a, b)为字符串a到b的编辑距离,有如下数学性质

  • dist(a, b) = 0  <==>  a = b   // 相同的字符串编辑距离当然为0
  • dist(a, b) = dist(b, a)        // a到b的编辑距离与b到a的编辑距离是一样的,因为添加-删除,替换-替换是互逆的操作
  • dist(a, temp) + dist(temp, b) >= dist(a, b)   // 这点很重要,这是一条三角不等式,对于三角形从a顶点到b顶点的距离必然小于a到c再从c到b的距离。

推论

在模糊匹配值,我们需要设定一个容错值,而这个值用最短编辑距离来衡量,例如我们设定容错值是1,字典中有 [cat, cute, candy],给出字符串cate,能够匹配到[cat, cute],因为这两个字符串与cate的编辑距离都小于等于1 ,而dist("cate", "candy") = 3>1,不能加入匹配列表。设容错值为 r, Q为待校正的字符串,拿它与任意字符串A比较最短距离为d,如果Q能够在容错范围内转化为B,A到B的编辑距离最小是d-r,最大是d+r,原因如下

dist(Q, A) + dist(Q, B) = dist(A, Q) + dist(Q, B) >= dist(A, B)

==>  dist(A, B) <= dist(A, Q) + dist(Q, B) = d + r

dist(Q, A) + dist(A, B) >= dist(Q, B)

类似的也能推出 dist(Q, A) >= d - r,dist(Q, A) >= 1

因此max(1, d-r) <= dist(A ,B) <= d + r

BK树

BK树的构建

BK树的每个节点有任意个子节点,每个子节点均有一个编号,插入一个单词时,与当前节点比较最短编辑距离为k,如果当前节点还没有编号为k的子节点,将这个单词的节点插入,编号设置为k,否则,就递归向编号为k的节点插入单词。

BK树的查找

从根节点开始查找,对于任意一个节点,比较节点记录的单词与查找单词的最短距离d,根据上面推出的性质,只需要在 (max(1, d-r) , d+r) 编号的节点递归查找(前提是对应编号的节点存在)就可以了

代码实现

/**
 * Created by YotWei on 2018/9/8.
 * 定义计算编辑距离的接口
 */
public interface Metric<T> {

    int getMetric(T t1, T t2);
}
/**
 * Created by YotWei on 2018/9/8.
 * 这是典型的动态规划计算最短编辑距离算法
 */
public class StringEditMetric implements Metric<String> {
    @Override
    public int getMetric(String s1, String s2) {

        int[][] dp = new int[s1.length() + 1][s2.length() + 1];
        for (int i = 1; i <= s1.length(); i++) {
            dp[i][0] = i;
        }
        for (int i = 1; i <= s2.length(); i++) {
            dp[0][i] = i;
        }
        for (int i = 1; i <= s1.length(); i++) {
            for (int j = 1; j <= s2.length(); j++) {
                dp[i][j] = Math.min(
                        dp[i - 1][j - 1] + (s1.charAt(i - 1) == s2.charAt(j - 1) ? 0 : 1),
                        Math.min(dp[i][j - 1], dp[i - 1][j]) + 1
                );
            }
        }

        return dp[s1.length()][s2.length()];
    }
}
import java.util.*;

/**
 * Created by YotWei on 2018/9/8.
 *
 * BK树的实现
 */
public class BKTree<T> {

    private final int radius;    // 模糊匹配的范围,如果值为0,就变成了精确匹配

    private Node root;
    private Metric<T> metric;

    public BKTree(int radius, Metric<T> metric) {
        this.radius = radius;
        this.metric = metric;
    }

    public void add(T value) {
        if (root == null)
            root = new Node(value);
        else {
            root.add(value);
        }
    }

    public void addAll(Collection<? extends T> collection) {
        for (T val : collection) {
            add(val);
        }
    }

    public Set<T> search(T value) {
        Set<T> result = new HashSet<>();
        if (root != null)
            root.search(value, result);
        return result;
    }

    class Node {
        private T value;
        private Map<Integer, Node> childs;

        Node(T v) {
            this.value = v;
            this.childs = new HashMap<>();
        }

        void add(T value) {
            int distance = metric.getMetric(this.value, value);
            if (this.childs.containsKey(distance)) {
                this.childs.get(distance).add(value);
            } else {
                this.childs.put(distance, new Node(value));
            }
        }

        void search(T value, Set<T> resultSet) {
            int distance = BKTree.this.metric.getMetric(this.value, value);

            if (distance <= radius) {    // 编辑距离在匹配范围,接入结果集
                resultSet.add(this.value);
            }

            for (int i = Math.max(distance - radius, 1); i <= distance + radius; i++) {
                Node ch = this.childs.get(i);
                if (ch != null)
                    ch.search(value, resultSet);
            }
        }
    }
}

客户端

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

/**
 * Created by YotWei on 2018/9/8.
 */
public class Client {

    public static void main(String[] args) throws IOException {
        Scanner sc = new Scanner(System.in);

        BKTree<String> bkTree = new BKTree<>(1, new StringEditMetric());
        bkTree.addAll(getDictionary());    // 构建bk树

        while (sc.hasNext()) {
            String word = sc.next().trim();
            System.out.printf("matching set: %s\n", bkTree.search(word));
        }
    }

    /**
     * 从文件中读入字典集,可自己定义
     */
    private static Set<String> getDictionary() throws IOException {
        Set<String> dictionary = new HashSet<>();
        BufferedReader br = new BufferedReader(new FileReader(new File("bktree/src/words")));
        String temp;
        while ((temp = br.readLine()) != null) {
            dictionary.add(temp);
        }
        br.close();

        return dictionary;
    }
}

运行结果

>> applo
matching set: [apple, apply]
>> absolutel
matching set: [absolute, absolutely]
>> humann
matching set: [human]
>> cat
matching set: [cut, car, cat]
>> animte
matching set: [animate]
>> edito
matching set: [editor, edit]

猜你喜欢

转载自blog.csdn.net/Kurozaki_Kun/article/details/82555772
bk
今日推荐