实现对反转字符串组合的Levenshtein距离?

5

我的应用程序中有一个员工列表,每个员工都有名字和姓氏,所以我有一个像下面这样的元素列表:

["Jim Carry", "Uma Turman", "Bill Gates", "John Skeet"]

我希望我的客户可以通过模糊搜索算法按姓名搜索员工。例如,如果用户输入"Yuma Turmon",最接近的元素"Uma Turman"将返回。我使用了Levenshtein距离算法,我在这里找到了这个链接

static class LevenshteinDistance
{
    /// <summary>
    /// Compute the distance between two strings.
    /// </summary>
    public static int Compute(string s, string t)
    {
        int n = s.Length;
        int m = t.Length;
        int[,] d = new int[n + 1, m + 1];

        // Step 1
        if (n == 0)
        {
            return m;
        }

        if (m == 0)
        {
            return n;
        }

        // Step 2
        for (int i = 0; i <= n; d[i, 0] = i++)
        {
        }

        for (int j = 0; j <= m; d[0, j] = j++)
        {
        }

        // Step 3
        for (int i = 1; i <= n; i++)
        {
            //Step 4
            for (int j = 1; j <= m; j++)
            {
                // Step 5
                int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;

                // Step 6
                d[i, j] = Math.Min(
                    Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
                    d[i - 1, j - 1] + cost);
            }
        }
        // Step 7
        return d[n, m];
    }
}

我会迭代用户输入的全名,与员工姓名列表进行比较。如果距离小于3,例如,我就会返回找到的员工。
现在我想允许用户通过反转姓名进行搜索 - 例如,如果用户输入“Turmon Uma”,它将返回“Uma Turman”,因为实际上的距离是1,因为名字和姓氏与姓氏和名字相同。我的算法现在将其视为不同的字符串,很远。如何修改它以便无论顺序如何都可以找到姓名?

2
有趣的事实:您拼错了 Jon Skeet 的名字 ;) - DavidG
5
你可以计算每个元素的多个距离(按所需转换),并返回最小值,但不要改变原先的意思。 - Sinatr
3
另一个有趣的事实:比尔·盖茨是唯一一个拼写正确的名字 ;) - pcdev
3
抱歉OP,我应该说“好问题!”。我同意@Sinatr的看法,将员工列表中的名称拆分为单独的单词,并将查询术语拆分为单词,然后使用每个搜索术语的最小值之和来计算最小可能距离。Jane的答案有效,直到每个人有两个以上的名称或用户输入少于或大于两个搜索术语。 - pcdev
1
吉姆·凯瑞,著名的加法师... - 500 - Internal Server Error
显示剩余5条评论
2个回答

2

您可以使用LINQ创建员工姓名的反向版本。例如,如果您有一个员工列表:

x = ["Jim Carry", "Uma Turman", "Bill Gates", "John Skeet"]

你可以写以下代码:

最初的回答


var reversedNames = x.Select(p=> $"{p.Split(' ')[1] p.Split(' ')[0]}");

它将返回反转的版本,例如: "最初的回答"。
xReversed = ["Carry Jim", "Turman Uma", "Gates Bill", "Skeet John"]

Then repeat you algorithm with this data too.


1
这个方法在处理像 "Robert Van de Graaf" 或 "Nicolás De La Cruz" 这样的名字时会出现问题。它也无法解决原始问题,即 "如果用户在搜索框中只输入一个姓氏,怎么办?"。 - pcdev

1
作为一个潜在的复杂问题,以下是一些想法需要考虑到:
  1. 将每个员工的姓名拆分成字符串列表。个人而言,我可能会丢弃任何由2个或更少字母组成的内容,除非整个名字都是由这些字母组成的。这应该有助于像“De La Cruz”这样的姓氏,这些姓氏可能会被搜索为“dela cruz”。将每个员工的名称列表存储在指向该员工的字典中。
  2. 以与拆分列表中的名称相同的方式拆分搜索术语。对于每个搜索术语,找到具有最低Levenshtein距离的名称,然后对于每个名称,从最低开始,使用其余的搜索术语重复对该员工的其他名称进行搜索。使用查询中的每个单词重复此步骤。例如,如果查询为John Smith,则查找John的最佳单词名称匹配项,然后在那些“最佳匹配”员工中为剩余名称匹配Smith,并获取距离之和。然后查找Smith的最佳匹配项,并在John上匹配剩余名称,并将距离求和。最佳匹配是总距离最小的匹配。您可以通过返回前10个按总距离排序的最佳匹配项列表来提供最佳匹配。而且,数据库或搜索术语中的名称方式并不重要。实际上,它们可以完全无序,也没有关系。
  3. 考虑如何处理连字符名称。我可能会像没有连字符一样拆分它们。
  4. 考虑大/小写字符,如果您还没有考虑的话。您应该在一个大小写中存储查找,并在比较之前将搜索术语转换为相同的大小写。
  5. 小心带重音符号的字母,许多人在他们的名字中使用它们,例如á。您的算法将无法正确处理它们。如果您希望有非字母双字节字符(例如中文、日文、阿拉伯文等),则应更加小心。

将每个员工的名字拆分的另外两个好处:

  • “未使用”的名字不会计入总数,因此如果我仅使用姓氏进行搜索,则不会对我在查找最短距离时造成影响。
  • 沿着同样的思路,您可以应用一些额外的规则来帮助查找非标准名称。例如,连字符名称可以存储为连字符(例如Wells-Harvey)、复合名称(WellsHarvey)和单独的名称(WellsHarvey分开),所有这些都与同一员工相关联。任何一个名称的低距离匹配都是对该员工的低距离匹配,再次提醒,额外的名称不会计入总数。

这里有一些基本代码似乎能够工作,但它只真正考虑了1、2和4点:

using System;
using System.Collections.Generic;
using System.Linq;

namespace EmployeeSearch
{
    static class Program
    {
        static List<string> EmployeesList = new List<string>() { "Jim Carrey", "Uma Thurman", "Bill Gates", "Jon Skeet" };
        static Dictionary<int, List<string>> employeesById = new Dictionary<int, List<string>>();
        static Dictionary<string, List<int>> employeeIdsByName = new Dictionary<string, List<int>>();

        static void Main()
        {
            Init();
            var results = FindEmployeeByNameFuzzy("Umaa Thurrmin");
            // Returns:
            // (1) Uma Thurman  Distance: 3
            // (0) Jim Carrey  Distance: 10
            // (3) Jon Skeet  Distance: 11
            // (2) Bill Gates  Distance: 12
            Console.WriteLine(string.Join("\r\n", results.Select(r => $"({r.Id}) {r.Name}  Distance: {r.Distance}")));

            var results = FindEmployeeByNameFuzzy("Tormin Oma");
            // Returns:
            // (1) Uma Thurman  Distance: 4
            // (3) Jon Skeet  Distance: 7
            // (0) Jim Carrey  Distance: 8
            // (2) Bill Gates  Distance: 9
            Console.WriteLine(string.Join("\r\n", results.Select(r => $"({r.Id}) {r.Name}  Distance: {r.Distance}")));


            Console.Read();
        }

        private static void Init() // prepare our lists
        {
            for (int i = 0; i < EmployeesList.Count; i++)
            {
                // Preparing the list of names for each employee - add special cases such as hyphenation here as well
                var names = EmployeesList[i].ToLower().Split(new char[] { ' ' }).ToList();
                employeesById.Add(i, names);

                // This is not used here, but could come in handy if you want a unique index of names pointing to employee ids for optimisation:
                foreach (var name in names)
                {
                    if (employeeIdsByName.ContainsKey(name))
                    {
                        employeeIdsByName[name].Add(i);
                    }
                    else
                    {
                        employeeIdsByName.Add(name, new List<int>() { i });
                    }
                }
            }

        }

        private static List<SearchResult> FindEmployeeByNameFuzzy(string query)
        {
            var results = new List<SearchResult>();

            // Notice we're splitting the search terms the same way as we split the employee names above (could be refactored out into a helper method)
            var searchterms = query.ToLower().Split(new char[] { ' ' });

            // Comparison with each employee
            for (int i = 0; i < employeesById.Count; i++)
            {
                var r = new SearchResult() { Id = i, Name = EmployeesList[i] };
                var employeenames = employeesById[i];
                foreach (var searchterm in searchterms)
                {
                    int min = searchterm.Length;

                    // for each search term get the min distance for all names for this employee
                    foreach (var name in employeenames)
                    {
                        var distance = LevenshteinDistance.Compute(searchterm, name);
                        min = Math.Min(min, distance);
                    }

                    // Sum the minimums for all search terms
                    r.Distance += min;
                }
                results.Add(r);
            }

            // Order by lowest distance first
            return results.OrderBy(e => e.Distance).ToList();
        }
    }

    public class SearchResult
    {
        public int Distance { get; set; }
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public static class LevenshteinDistance
    {
        /// <summary>
        /// Compute the distance between two strings.
        /// </summary>
        public static int Compute(string s, string t)
        {
            int n = s.Length;
            int m = t.Length;
            int[,] d = new int[n + 1, m + 1];

            // Step 1
            if (n == 0)
            {
                return m;
            }

            if (m == 0)
            {
                return n;
            }

            // Step 2
            for (int i = 0; i <= n; d[i, 0] = i++)
            {
            }

            for (int j = 0; j <= m; d[0, j] = j++)
            {
            }

            // Step 3
            for (int i = 1; i <= n; i++)
            {
                //Step 4
                for (int j = 1; j <= m; j++)
                {
                    // Step 5
                    int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;

                    // Step 6
                    d[i, j] = Math.Min(
                        Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
                        d[i - 1, j - 1] + cost);
                }
            }
            // Step 7
            return d[n, m];
        }
    }
}

当你开始时,只需调用Init(),然后调用

var results = FindEmployeeByNameFuzzy(userquery);

返回一个按最佳匹配排序的有序列表。

免责声明: 这段代码不是最优的,只经过简短测试,没有检查null值,可能会出错导致程序崩溃,等等。如果你有大量员工,则可能非常缓慢。有几个改进可以进行,例如在循环Levenshtein算法时,如果距离超过当前最小距离,则可以退出循环。


我还可以建议,如果这个答案确实是你问题的最合适的答案,那么你可以将标题改为“如何实现任意顺序单词的Levenshtein搜索”,因为我希望已经表明了仅依赖于名称只有两个单词可能会导致问题。 - pcdev
OP:这个回答解决了你的问题吗?如果没有,请告诉我我理解错了什么。如果是,请投票并将其标记为您的答案,以帮助其他人解决类似的问题 - 谢谢! - pcdev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接