来源:.Net.NewLife。需求:假设在某系统存储了许多地址,例如:“北京市海淀区中关村大街1号海龙大厦”。用户输入“北京 海龙大厦”即可查询到这条结果。另外还需要有容错设计,例如输入“广西 京岛风景区”能够搜索到"广西壮族自治区京岛风景名胜区"。最终的需求是:可以根据用户输入,匹配若干条近似结果共用户选择目的:避免用户输入类似地址导致数据出现重复项。例如,已经存在“北京市中关村”,就不应该再允许存在“北京中关村”。

举例

此类技术在搜索引擎中早已广泛使用,例如“查询预测”功能。

要实现此算法,首先需要明确“字符串近似”的概念。

计算字符串相似度通常使用的是动态规划(DP)算法。

常用的算法是 Levenshtein Distance。用这个算法可以直接计算出两个字符串的“编辑距离”。所谓编辑距离,是指一个字符串,每次只能通过插入一个字符、删除一个字符或者修改一个字符的方法,变成另外一个字符串的最少操作次数。这就引出了第一种方法:计算两个字符串之间的编辑距离。稍加思考之后发现,不能用输入的关键字直接与句子做匹配。你必须从句子中选取合适的长度后再做匹配。把结果按照距离升序排序。

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace BestString
{
    public static class SearchHelper
    {
        public static string[] Search(string param, string[] datas)
        {
            if (string.IsNullOrWhiteSpace(param))
                return new string[0];
            string[] words = param.Split(new char[] { ' ', ' ' }, StringSplitOptions.RemoveEmptyEntries);
            foreach (string word in words)
            {
                int maxDist = (word.Length - 1) / 2;
                var q = from str in datas
                        where word.Length <= str.Length
                            && Enumerable.Range(0, maxDist + 1)
                            .Any(dist =>
                            {
                                return Enumerable.Range(0, Math.Max(str.Length - word.Length - dist + 1, 0))
                                    .Any(f =>
                                    {
                                        return Distance(word, str.Substring(f, word.Length + dist)) <= maxDist;
                                    });
                            })
                        orderby str
                        select str;
                datas = q.ToArray();
            }
            return datas;
        }
        static int Distance(string str1, string str2)
        {
            int n = str1.Length;
            int m = str2.Length;
            int[,] C = new int[n + 1, m + 1];
            int i, j, x, y, z;
            for (i = 0; i <= n; i++)
                C[i, 0] = i;
            for (i = 1; i <= m; i++)
                C[0, i] = i;
            for (i = 0; i < n; i++)
                for (j = 0; j < m; j++)
                {
                    x = C[i, j + 1] + 1;
                    y = C[i + 1, j] + 1;
                    if (str1[i] == str2[j])
                        z = C[i, j];
                    else
                        z = C[i, j] + 1;
                    C[i + 1, j + 1] = Math.Min(Math.Min(x, y), z);
                }
            return C[n, m];
        }
    }
}

分析这个方法后发现,每次对一个句子进行相关度比较的时候,都要把把句子从头到尾扫描一次,每次扫描还需要以最大误差作长度控制。这样一来,对每个句子的计算次数大大增加。达到了二次方的规模(忽略距离计算时间)。

所以我们需要更高效的计算策略。在纸上写出一个句子,再写出几个关键字。一个一个涂画之后,偶然发现另一种字符串相关的算法完全可以适用。那就是 Longest common subsequence(LCS,最长公共字串)。为什么这个算法可以用来计算两个字符串的相关度?先看一个例子:

关键字:     少年时代 的 神话             播下了浪漫注意

句子:   就是少年时代大量神话传说在其心田里播下了浪漫主义这颗难以磨灭的种子

这里用了两个关键字进行搜索。可以看出来两个关键字都有部分匹配了句子中的若干部分。这样可以单独为两个关键字计算 LCS,LCS之和就是简单的相关度。看到这里,你若是已经理解了核心思想,已经可以实现出基本框架了。但是,请看下面这个例子:

关键字:      东土大唐       唐三藏

句子:  我本是东土大唐钦差御弟唐三藏大徒弟孙悟空行者

看出来问题了吗?下面还是使用同样的关键字和句子。

关键字:     东土大         (唐唐)三藏

句子: 我本是东土大唐钦差御弟唐   三藏大徒弟孙悟空行者

举这个例子为了说明,在进行 LCS 计算的过程中,得到的结果并不能保证就是我们期望的结果。为了①保证所匹配的结果中不存在交集,并且②在句子中的匹配结果尽可能的短,需要采取两个补救措施。(为什么需要满足这样的条件,读者自行思考)

第一:可以在单次计算 LCS 之后,用贪心策略向前(向后)找到最先能够完成匹配的位置,再用相同的策略向后(向前)扫描。这样可以满足第二个条件找到句子中最短的匹配。如果你对 LCS 算法有深入了解,完全可以在计算 LCS 的过程中找到最短匹配的结束位置,然后只需要进行一次向前扫描就可以完成。这样节约了一次扫描过程。

第二:增加一个标记数组,记录句子中的字符是否被匹配过。

最后标记数组中标记过的位置就是匹配结果。

相信你看到这里一定非常头晕,下面用一个例子解释:(句子)

关键字:   ABCD

句子:     XAYABZCBXCDDYZ

句子分解: X Y  Z  X   YZ

A   B C   D

A   B C D

你可能会匹配成 AYABZCBXCDD,AYABZCBXCD,ABZCBXCDD,ABZCBXCD。我们实际需要的只是ABZCBXCD。

使用LCS匹配之后,得到的很可能是 XAYABZCBXCDDYZ;

用贪心策略向前处理后,得到结果为 XAYABZCBXCDDYZ;

用贪心策略向后处理后,得到结果为 XAYABZCBXCDDYZ。

这样处理的目的是为了避免得到较长的匹配结果(类似正则表达式的贪婪、懒惰模式)。

以上只是描述了怎么计算两个字符串的相似程度。除此之外还需要:①剔除相似度较低的结果;②对结果进行排序。

剔除相似度较低的结果,这里设定了一个阈值:差错比例不能超过匹配结果长度的一半。

对结果进行排序,不能够直接使用相似度进行排序。因为相似度并没有考虑到句子的长度。按照使用习惯,通常会把匹配度高,并且句子长度短的放在前面。这就得到了排序因子:(不匹配度+0.5)/句子长度。

最后得到我们最终的搜索方法

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
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
namespace BestString
{
    public static class SearchHelper
    {
        public static string[] Search(string param, string[] items)
        {
            if (string.IsNullOrWhiteSpace(param) || items == null || items.Length == 0)
                return new string[0];
            string[] words = param
                                .Split(new char[] { ' ', '\u3000' }, StringSplitOptions.RemoveEmptyEntries)
                                .OrderBy(s => s.Length)
                                .ToArray();
            var q = from sentence in items.AsParallel()
                    let MLL = Mul_LnCS_Length(sentence, words)
                    where MLL >= 0
                    orderby (MLL + 0.5) / sentence.Length, sentence
                    select sentence;
            return q.ToArray();
        }
        //static int[,] C = new int[100, 100];
        /// <summary>
        ///
        /// </summary>
        /// <param name="sentence"></param>
        /// <param name="words">多个关键字。长度必须大于0,必须按照字符串长度升序排列。</param>
        /// <returns></returns>
        static int Mul_LnCS_Length(string sentence, string[] words)
        {
            int sLength = sentence.Length;
            int result = sLength;
            bool[] flags = new bool[sLength];
            int[,] C = new int[sLength + 1, words[words.Length - 1].Length + 1];
            //int[,] C = new int[sLength + 1, words.Select(s => s.Length).Max() + 1];
            foreach (string word in words)
            {
                int wLength = word.Length;
                int first = 0, last = 0;
                int i = 0, j = 0, LCS_L;
                //foreach 速度会有所提升,还可以加剪枝
                for (i = 0; i < sLength; i++)
                    for (j = 0; j < wLength; j++)
                        if (sentence[i] == word[j])
                        {
                            C[i + 1, j + 1] = C[i, j] + 1;
                            if (first < C[i, j])
                            {
                                last = i;
                                first = C[i, j];
                            }
                        }
                        else
                            C[i + 1, j + 1] = Math.Max(C[i, j + 1], C[i + 1, j]);
                LCS_L = C[i, j];
                if (LCS_L <= wLength >> 1)
                    return -1;
                while (i > 0 && j > 0)
                {
                    if (C[i - 1, j - 1] + 1 == C[i, j])
                    {
                        i--;
                        j--;
                        if (!flags[i])
                        {
                            flags[i] = true;
                            result--;
                        }
                        first = i;
                    }
                    else if (C[i - 1, j] == C[i, j])
                        i--;
                    else// if (C[i, j - 1] == C[i, j])
                        j--;
                }
                if (LCS_L <= (last - first + 1) >> 1)
                    return -1;
            }
            return result;
        }
    }
}

对于此类问题,要想得到更快速的实现,必须要用到分词+索引的方案。在此不做探讨。

代码打包下载:http://files.cnblogs.com/Aimeast/BestString.zip

http://www.cnblogs.com/Aimeast/archive/2011/09/05/2167844.html

【算法】字符串近似搜索(转)相关推荐

  1. mysql替换开头_如何在MySQL的字符串开头搜索和替换特定字符?

    为此,您可以使用INSERT().让我们首先创建一个表-mysql> create table DemoTable -> ( -> ZipCode varchar(200) -> ...

  2. Algorithm:C++语言实现之字符串相关算法(字符串的循环左移、字符串的全排列、带有同个字符的全排列、串匹配问题的BF算法和KMP算法)

    Algorithm:C++语言实现之字符串相关算法(字符串的循环左移.字符串的全排列.带有同个字符的全排列.串匹配问题的BF算法和KMP算法) 目录 一.字符串的算法 1.字符串的循环左移 2.字符串 ...

  3. C++Rabin Karp算法字符串快速查找(附完整源码)

    C++Rabin Karp算法字符串快速查找 C++Rabin Karp算法字符串快速查找完整源码(定义,实现,main函数测试) C++Rabin Karp算法字符串快速查找完整源码(定义,实现,m ...

  4. C语言durand kerner算法求近似根roots(附完整源码)

    实现durand kerner算法求近似根roots 实现以下几个相关的个接口 durand kerner算法求近似根roots的完整源码(定义,实现,main函数测试) 实现以下几个相关的个接口 l ...

  5. 外挂学习之路(12)--- 用CE搜索字符串和搜索字符数组的区别

    用VS2008写个小工程做测试,得到如下一些结论 代码如下: TCHAR test[50] = TEXT("你好啊"); void CasciiDlg::OnBnClickedBu ...

  6. 算法导论--广度优先搜索和深度优先搜索

    广度优先搜索 在给定图G=(V,E)和一个特定的源顶点s的情况下,广度优先搜索系统地探索G中的边,以期"发现"可从s 到达的所有顶点,并计算s 到所有这些可达顶点之间的距离(即最少 ...

  7. 算法---字符串顺序平移

    算法-字符串顺序平移 原理:矩阵的转置思想 代码: #include <stdio.h> #include <stdlib.h> void swap(char *a,int i ...

  8. 深度搜索和广度搜索领接表实现_数据结构与算法--图的搜索(深度优先和广度优先)...

    数据结构与算法--图的搜索(深度优先和广度优先) 有时候我们需要系统地检查每一个顶点或者每一条边来获取图的各种性质,为此需要从图的某个顶点出发,访遍图中其余顶点,且使得每一个顶点只被访问一次,这个过程 ...

  9. Python中的字符串(搜索和替换、对齐、统计、分离和连接)

    1.字符串的搜索和替换 s = 'hello world hello'#find找到子串,并返回最小的索引print(s.find('hello'))print(s.find('world'))#rf ...

最新文章

  1. 用c++实现LR语法分析器 通过LR分析表及三个栈形成对输入表达式的判断!
  2. 分布式架构的对比-IBM XIV
  3. 7能进安全模式但正常启动黑屏_电脑进入系统后黑屏怎么解决
  4. 409 Longest Palindrome
  5. pip 安装依赖库版本低问题
  6. JAVA 条件语句 跟PHP没有区别!!!!!
  7. 平台电商类的增长策略:从用户激励到养成类游戏
  8. Tomcat的安装和运行
  9. 哪种修复redis未授权访问漏洞的方法是相对不安全的_关于Linux挖矿、DDOS等应急事件处置方法...
  10. mysql8.0.15远程登陆权限,MySQL8.0给root用户赋予远程连接权限
  11. java.io.IOException: No FileSystem for scheme: hdfs转载加自己笔记
  12. 微软发布 Visual Studio 2019年第二季度路线图
  13. 图算法--深度优先/广度优先/最小生成树/最短路径
  14. python怎么使用-如何使用python进行第一个机器学习项目(详细教程篇)
  15. c#使用类库nthereum在.net上开发以太坊的实战示例
  16. html中居中方法,HTML中的居中方法
  17. Python——轮盘抽奖游戏
  18. 徒步50公里,35岁的联想可以,你也可以
  19. 中山マミ - 彼女×彼女×彼女 ~今夜はぎゅっと抱きしめてね~
  20. 【Java】Java中的常用工具类(排名前 16)

热门文章

  1. OpenGL多线程实例
  2. C++递增运算符重载
  3. 经典C语言程序100例之七
  4. stata中计算公式命令_#stata中哪个命令和stats命令等价#stata中计算命令
  5. java equals比较_Java中equals()和==的比较
  6. 02/03_Pytorch安装、Conda安装Pythorch,换源、pytorch官网、验证、安装jupyter、卸载、安装、启动jupyter、配置Jupyter notebook、使用
  7. 数据采集之解析Mysql的binlog日志发送至Kafka实时消费(转:https://blog.csdn.net/liguohuabigdata/article/details/79472777)
  8. kylin备份元数据(用于清除不用的数据方式)
  9. 5.中文问题(自身,操作系统级别,应用软件的本身),mysql数据库备份
  10. iask(http://ishare.iask.sina.com.cn/download/explain.php?fileid=12207500)