中文分词引擎 java 实现 — 正向最大、逆向最大、双向最大匹配法

<article class="baidu&#95;pl">

正向最大匹配法

</article>

分词目标:

在词典中进行扫描,尽可能地选择与词典中最长单词匹配的词作为目标分词,然后进行下一次匹配。

算法流程

假设词典中最长的单词为 5 个(MAX_LENGTH),那么最大匹配的起始子串字数也为 5 个

(1)扫描字典,测试读入的子串是否在字典中

(2)如果存在,则从输入中删除掉该子串,重新按照规则取子串,重复(1)

(3)如果不存在于字典中,则从右向左减少子串长度,重复(1)

分词实例:

比如说输入 “北京大学生前来应聘”,

  1. 第一轮:取子串 “北京大学生”,正向取词,如果匹配失败,每次去掉匹配字段最后面的一个字
    • “北京大学生”,扫描 5 字词典,没有匹配,子串长度减 1 变为“北京大学”
    • “北京大学”,扫描 4 字词典,有匹配,输出“北京大学”,输入变为“生前来应聘”
  2. 第二轮:取子串“生前来应聘”
    • “生前来应聘”,扫描 5 字词典,没有匹配,子串长度减 1 变为“生前来应”
    • “生前来应”,扫描 4 字词典,没有匹配,子串长度减 1 变为“生前来”
    • “生前来”,扫描 3 字词典,没有匹配,子串长度减 1 变为“生前”
    • “生前”,扫描 2 字词典,有匹配,输出“生前”,输入变为“来应聘””
  3. 第三轮:取子串“来应聘”
    • “来应聘”,扫描 3 字词典,没有匹配,子串长度减 1 变为“来应”
    • “来应”,扫描 2 字词典,没有匹配,子串长度减 1 变为“来”
    • 颗粒度最小为 1,直接输出“来”,输入变为“应聘”
  4. 第四轮:取子串“应聘”
    • “应聘”,扫描 2 字词典,有匹配,输出“应聘”,输入变为“”
  5. 输入长度为0,扫描终止


正向匹配法最终的切分结果为:”北京大学 / 生前 / 来 / 应聘”

正向匹配法实现代码如下:

public List<String> leftMax(String str) {

        List<String> results = new ArrayList<String>();
        String input = str;

        while( input.length() > 0 ) {

            String subSeq;
            // 每次取小于或者等于最大字典长度的子串进行匹配
            if( input.length() < MAX_LENGTH) 
                subSeq = input;
            else
                subSeq = input.substring(0, MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                // 如果字典中含有该子串或者子串颗粒度为1,子串匹配成功
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    results.add(subSeq);
                    // 输入中从前向后去掉已经匹配的子串
                    input = input.substring(subSeq.length());
                    break;      // 退出循环,进行下一次匹配
                } else {
                    // 去掉匹配字段最后面的一个字
                    subSeq = subSeq.substring(0, subSeq.length() - 1);
                }   
            }

        }
        return results;
    }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30


逆向最大匹配法

分词目标:

在词典中进行扫描,尽可能地选择与词典中最长单词匹配的词作为目标分词,然后进行下一次匹配。

在实践中,逆向最大匹配算法性能优于正向最大匹配算法。

算法流程

假设词典中最长的单词为 5 个(MAX_LENGTH),那么最大匹配的起始子串字数也为 5 个

(1)扫描字典,测试读入的子串是否在字典中

(2)如果存在,则从输入中删除掉该子串,重新按照规则取子串,重复(1)

(3)如果不存在于字典中,则从左向右减少子串长度,重复(1)

分词实例:

比如说输入 “北京大学生前来应聘”,

  1. 第一轮:取子串 “生前来应聘”,逆向取词,如果匹配失败,每次去掉匹配字段最前面的一个字
    • “生前来应聘”,扫描 5 字词典,没有匹配,字串长度减 1 变为“前来应聘”
    • “前来应聘”,扫描 4 字词典,没有匹配,字串长度减 1 变为“来应聘”
    • “来应聘”,扫描 3 字词典,没有匹配,字串长度减 1 变为“应聘”
    • “应聘”,扫描 2 字词典,有匹配,输出“应聘”,输入变为“大学生前来”
  2. 第二轮:取子串“大学生前来”
    • “大学生前来”,扫描 5 字词典,没有匹配,字串长度减 1 变为“学生前来”
    • “学生前来”,扫描 4 字词典,没有匹配,字串长度减 1 变为“生前来”
    • “生前来”,扫描 3 字词典,没有匹配,字串长度减 1 变为“前来”
    • “前来”,扫描 2 字词典,有匹配,输出“前来”,输入变为“北京大学生”
  3. 第三轮:取子串“北京大学生”
    • “北京大学生”,扫描 5 字词典,没有匹配,字串长度减 1 变为“京大学生”
    • “京大学生”,扫描 4 字词典,没有匹配,字串长度减 1 变为“大学生”
    • “大学生”,扫描 3 字词典,有匹配,输出“大学生”,输入变为“北京”
  4. 第四轮:取子串“北京”
    • “北京”,扫描 2 字词典,有匹配,输出“北京”,输入变为“”
  5. 输入长度为0,扫描终止


逆向匹配法最终的切分结果为:”北京/ 大学生/ 前来 / 应聘”

逆向匹配法实现如下:

public List<String> rightMax(String str) {
        // 采用堆栈处理结果,后进先出
        Stack<String> store=new Stack<String>();
        List<String> results = new ArrayList<String>();
        String input = str;

        while( input.length() > 0 ) {

            String subSeq;
            // 每次取小于或者等于最大字典长度的子串进行匹配
            if( input.length() < MAX_LENGTH)
                subSeq = input;
            else 
                subSeq = input.substring(input.length() - MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                // 如果字典中含有该子串或者子串颗粒度为1,子串匹配成功
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    store.add(subSeq);
                    // 输入中从后向前去掉已经匹配的子串
                    input = input.substring(0, input.length() - subSeq.length());
                    break;
                } else {
                    // 去掉匹配字段最前面的一个字
                    subSeq = subSeq.substring(1);
                }
            }
        }
        // 输出结果
        int size = store.size();
        for( int i = 0; i < size; i ++) {
            results.add(store.pop());
        }

        return results;
    }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36


双向最大匹配法

分词目标:

将正向最大匹配算法和逆向最大匹配算法进行比较,从而确定正确的分词方法。

算法流程:

  1. 比较正向最大匹配和逆向最大匹配结果
  2. 如果分词数量结果不同,那么取分词数量较少的那个
  3. 如果分词数量结果相同
    • 分词结果相同,可以返回任何一个
    • 分词结果不同,返回单字数比较少的那个


分词实例:

就上例来看,

正向匹配最终切分结果为:北京大学 / 生前 / 来 / 应聘,分词数量为 4,单字数为 1

逆向匹配最终切分结果为:”北京/ 大学生/ 前来 / 应聘,分词数量为 4,单字数为 0

逆向匹配单字数少,因此返回逆向匹配的结果。

双向最大匹配法实现如下:

public List<String> segment() {
        List<String> fmm = this.leftMax();
        List<String> bmm = this.rightMax();

        // 如果分词的结果不同,返回长度较小的
        if( fmm.size() != bmm.size()) {
            if ( fmm.size() > bmm.size())
                return bmm;
            else 
                return bmm;
        }
        // 如果分词的词数相同
        else {
            int fmmSingle = 0, bmmSingle = 0;
            boolean isEqual = true;
            for( int i = 0; i < bmm.size(); i ++) {
                if( !fmm.get(i).equals(bmm.get(i))) {
                    isEqual = false;
                }
                if( fmm.get(i).length() == 1)
                    fmmSingle ++;
                if( bmm.get(i).length() == 1)
                    bmmSingle ++;
            }
            // 如果正向、逆向匹配结果完全相等,返回任意结果
            if ( isEqual ) {
                return fmm;
            // 否则,返回单字数少的匹配方式
            } else if ( fmmSingle > bmmSingle)      
                return bmm;
            else 
                return fmm;     
        }

    }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35


载入字典和自定义添加词

这里的字典文件采用的是

http://download.csdn.net/download/yuanlulu/2380141

载入字典和自定义添加词实现如下:

private static Set<String> dictionary;  
    // 初始化字典,采用 hashset 存储
    public void getDictionary() {
        dictionary = new HashSet<String>();  
        String dicpath = "data/worddict2.txt";  
        String line = null;  

            BufferedReader br;
            try {
                // 按照 gbk 编码读入文件
                br = new BufferedReader(new InputStreamReader(new FileInputStream(dicpath),"gbk"));
                try {
                    while(((line = br.readLine())!=null)) {
                        // 按照空格切分,只读取第二部分
                        String[] str = line.split("\\s+");
                        line = str[1];
                        dictionary.add(line);   
                    }
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } catch (UnsupportedEncodingException | FileNotFoundException e) {
                e.printStackTrace();
            }       
    }
    // 自定义添加词汇
    public void addWord(String str) {
        dictionary.add(str);    
    }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30


歧义句测试

可以看到效果还不错,最大匹配法的效果还是取决于字典的质量。

整体代码如下:

package mm;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException; 
public class MMSegment {

    private String request;
    private int MAX_LENGTH = 5;
    private static Set<String> dictionary;  

    public void getDictionary() {
        dictionary = new HashSet<String>();  
        String dicpath = "data/worddict2.txt";  
        String line = null;  

            BufferedReader br;
            try {
                br = new BufferedReader(new InputStreamReader(new FileInputStream(dicpath),"gbk"));
                try {
                    while(((line = br.readLine())!=null)) {
                        String[] str = line.split("\\s+");
                        line = str[1];
                        dictionary.add(line);   
                    }
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } catch (UnsupportedEncodingException | FileNotFoundException e) {
                e.printStackTrace();
            }       
    }

    public void addWord(String str) {
        dictionary.add(str);    
    }

    public List<String> leftMax() {

        List<String> results = new ArrayList<String>();
        String input = request;

        while( input.length() > 0 ) {

            String subSeq;
            if( input.length() < MAX_LENGTH) 
                subSeq = input;
            else
                subSeq = input.substring(0, MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    results.add(subSeq);
                    input = input.substring(subSeq.length());
                    break;  
                } else {
                    subSeq = subSeq.substring(0, subSeq.length() - 1);
                }   
            }

        }
        return results;
    }
    public List<String> rightMax() {

        Stack<String> store=new Stack<String>();
        List<String> results = new ArrayList<String>();
        String input = request;

        while( input.length() > 0 ) {

            String subSeq;
            if( input.length() < MAX_LENGTH)
                subSeq = input;
            else 
                subSeq = input.substring(input.length() - MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    store.add(subSeq);
                    input = input.substring(0, input.length() - subSeq.length());
                    break;
                } else {
                    subSeq = subSeq.substring(1);
                }
            }
        }
        int size = store.size();
        for( int i = 0; i < size; i ++) {
            results.add(store.pop());
        }

        return results;
    }

    public List<String> segment() {
        List<String> fmm = this.leftMax();
        List<String> bmm = this.rightMax();

        if( fmm.size() != bmm.size()) {
            if ( fmm.size() > bmm.size())
                return bmm;
            else 
                return fmm;
        }

        else {
            int fmmSingle = 0, bmmSingle = 0;
            boolean isEqual = true;
            for( int i = 0; i < bmm.size(); i ++) {
                if( !fmm.get(i).equals(bmm.get(i))) {
                    isEqual = false;
                }
                if( fmm.get(i).length() == 1)
                    fmmSingle ++;
                if( bmm.get(i).length() == 1)
                    bmmSingle ++;
            }

            if ( isEqual ) {
                return fmm;
            } else if ( fmmSingle > bmmSingle)      
                return bmm;
            else 
                return fmm; 
        }
    }

    public void test(String str) {
        request = str;
        System.out.println(this.segment());
    }

    public static void main(String[] args) {
        MMSegment f = new MMSegment();
        f.getDictionary();
        f.test("研究生命科学");
        f.test("研究生命令本科生");
        f.test("我从马上下来");
        f.test("北京大学生喝进口红酒");
        f.test("美军中将竟公然说");
        f.test("阿美首脑会议将讨论巴以和平等问题");
        f.addWord("巴以和平");
        System.out.println("---------------------------");
        System.out.println("向字典中添加'巴以和平'后");
        f.test("阿美首脑会议将讨论巴以和平等问题");
        f.test("我不想吃东西");
    }

}

 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160

参考资料

[1] http://blog.csdn.net/worldwindjp/article/details/18085725

[2] http://blog.csdn.net/hu948162999/article/details/43608107

[3] http://blog.csdn.net/xiaoyeyopulei/article/details/25194021

[4] http://blog.csdn.net/chenlei0630/article/details/40710441

全部评论

相关推荐

粗心的雪碧不放弃:纯学历问题,我这几个月也是一直优化自己的简历,后来发现优化到我自己都觉得牛逼的时候,发现面试数量也没有提升,真就纯学历问题
点赞 评论 收藏
分享
拒绝无效加班的小师弟很中意你:求职意向没有,年龄、课程冗余信息可以删掉,需要提升项目经历。排版需要修改。
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务