【算法】字符串近似搜索(转)

来源:.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

时间: 2024-10-10 07:31:31

【算法】字符串近似搜索(转)的相关文章

十大基础实用算法之深度优先搜索和广度优先搜索

深度优先搜索算法(Depth-First-Search),是搜索算法的一种.它沿着树的深度遍历树的节点,尽可能深的搜索树的分支.当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点.这一过程一直进行到已发现从源节点可达的所有节点为止.如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止.DFS属于盲目搜索. 深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相

字符串算法—字符串排序(下篇)

本文将介绍3区基数快速排序.后缀排序法. 1.  前文回顾 在字符串算法-字符串排序(上篇)中,我们介绍了键索引计数法.LSD基数排序.MSD基数排序. 但LSD基数排序要求需排序字符串的长度一致:MSD基数排序虽然对字符串的长度没要求,但其递归循环里的每次循环都需要进行很多操作,且需要额外的空间. 本文将介绍一种更高效的字符串排序方法:结合MSD基数排序和3区快速排序.如果对这两种算法不熟悉的,建议先去了解一下. 2. 3区基数快速排序(3-way radix quicksort) 从例子入手

[算法]字符串左移k位

如,abcde左移3位为deabc 要求时间复杂度O(n),空间复杂度O(1),每一个字符只能遍历一次 摘自http://blog.csdn.net/geniusluzh/article/details/8460031 利用数学解决该问题 其实对于这道题,最初一看的想法就是将当前位依次替换左移m位对应的那个位,然后依次替换.后来发现有的情况一次循环替换就能全部完成整个串的左移,而有的情况下会出现多个循环链,一时只得到规律,不能想到很好的证明办法,只怪以前初等数论没有好好学啊! 我们发现对于长度为

编程算法 - 字符串的排列 代码(C)

字符串的排列 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 输入一个字符串, 打印出该字符串中字符的所有排列. 方法: 使用递归依次交换位置, 打印输出. 代码: /* * main.cpp * * Created on: 2014.6.12 * Author: Spike */ /*eclipse cdt, gcc 4.8.1*/ #include <stdio.h> void Permutation(char* pStr, char

数据结构与算法---字符串(下)

前面两篇文章,分别介绍了字符串的概念.抽象数据类型.KMP模式匹配算法.这篇文章,我们来学习字符串的一些常用算法. 字符串的相关操作算法 StrAssign: /* 功能:生成一个其值等于Chars的串T */ Status StrAssign(String T, char *chars) { int i; if (chars[0] > MAXSIZE) return ERROR; T[0] = chars[0]; //chars[0]存放的是字符chars的长度 T[0]存放着的是串T的长度

[算法]字符串编辑距离

来自编程之美的一题 许多程序会大量使用字符串.对于不同的字符串,我们希望能够有办法判断其相似程序.我们定义一套操作方法来把两个不相同的字符串变得相同,具体的操作方法为: 1.修改一个字符(如把“a”替换为“b”); 2.增加一个字符(如把“abdd”变为“aebdd”); 3.删除一个字符(如把“travelling”变为“traveling”); 比如,对于“abcdefg”和“abcdef”两个字符串来说,我们认为可以通过增加/减少一个“g”的方式来达到目的.上面的两种方案,都仅需要一 次

ACM中常用算法----字符串

ACM中常用算法--字符串 ACM中常用的字符串算法不多,主要有以下几种: Hash 字典树 KMP AC自动机 manacher 后缀数组 EX_KMP SAM(后缀自动机) 回文串自动机 下面来分别介绍一下: 0. Hash 字符串的hash是最简单也最常用的算法,通过某种hash函数将不同的字符串分别对应到不同的数字.进而配合其他数据结构或STL可以做到判重,统计,查询等操作. #### 字符串的hash函数: 一个很简单的hash函数代码如下: ull xp[maxn],hash[max

编程算法 - 字符串相同 代码(Java)

字符串相同 代码(Java) 本文地址: http://blog.csdn.net/caroline_wendy 题目: 实现一个算法, 确定一个字符串的所有字符是否全都不同. 假使不允许使用额外的数据结构, 又该如何处理. 解法1: 使用数据结构, 设置boolean数组, 把值(value)作为数组的索引(index), 判断数组是否重复. 解法2: 不使用数据结构, 可以通过位(bit)进行判断, 把每个字母映射2进制数的一位, 或运算("|")更新数字的位, 与运算("

JS使用replace()方法和正则表达式进行字符串的搜索与替换实例

1.JS字符串的替换及replace()方法的使用 replace(regexp,replacement)方法有两个参数,第一参数可以是一个纯文本字符串或是一个RegExp对象,具体请看RegExp对象的使用:第二个参数可是一个字符串也可以是一个函数. 以下是JS字符串替换的举例: 例1: var str="Hello world!"; document.write(str.replace(/world/, "phper")); 例2: var reg=new Re