如何快速搜索基于字符串的键/值集合

时间:2008-10-14 17:48:31

标签: algorithm search tree ternary-search-tree

你好伙伴stackoverflowers!

我有一个200.000字符串条目的单词列表,平均字符串长度大约是30个字符。这个单词列表是关键,每个键都有一个域对象。我想通过只知道密钥的一部分来找到此集合中的域对象。 I.E.搜索字符串“kov”将例如匹配键“stackoverflow”。

目前我正在使用三元搜索树(TST),它通常会在100毫秒内找到这些项目。然而,这对我的要求来说太慢了。可以通过一些小的优化来改进TST实现,我可以尝试平衡树。但我认为这些东西不会给我5x - 10x的速度提升我的目标。我假设这么慢的原因是我基本上必须访问树中的大多数节点。

有关如何提高算法速度的任何想法?我还应该关注其他算法吗?

提前致谢, 奥斯卡

7 个答案:

答案 0 :(得分:13)

后缀数组和 q -gram索引

如果您的字符串在大小上有严格的上限,您可以考虑使用suffix array:只需使用特殊字符(例如null char)将所有字符串填充到相同的最大长度。然后连接所有字符串并在它们上构建后缀数组索引。

这为您提供 m * log n 的查找运行时,其中 m 是查询字符串的长度, n < / em>是组合字符串的总长度。如果这仍然不够好并且 m 具有固定的小长度,并且您的字母Σ的大小受限(例如,Σ<128个不同的字符),您还可以构建一个 q -gram index 。这将允许在恒定时间中进行检索。但是, q -gram表需要Σ m 条目(= 3个字符时为8 MiB,4个字符为1 GiB) !)。

使索引更小

可以通过调整哈希函数来减小 q -gram表的大小(在最好的情况下以指数方式)。您可以使用有损哈希函数,而不是为每个可能的 q -gram分配唯一编号。然后,该表必须存储可能的后缀数组索引的列表,而不是仅存储对应于完全匹配的一个后缀数组条目。但这将导致查找不再是常量,因为必须考虑列表中的所有条目。

顺便说一句,我不确定您是否熟悉 q -gram索引如何工作,因为互联网对此主题没有帮助。我之前在另一个话题中提到了这一点。因此,我在bachelor thesis中包含了构造的描述和算法。

概念证明

我写了一个非常小的C#概念证明(因为你另有说明你使用过C#)。它有效,但由于两个原因它非常慢。首先,后缀数组创建只是对后缀进行排序。仅此一项就有运行时 n 2 log n 。有很多优越的方法。然而,更糟糕的是,我使用SubString来获取后缀。不幸的是,.NET为此创建了整个后缀的副本。要在实践中使用此代码,请确保使用不会不必要地复制任何数据的就地方法。从字符串中检索 q -grams也是如此。

最好不要构造我的示例中使用的m_Data字符串。相反,您可以保存对原始数组的引用,并通过处理此数组来模拟我的所有SubString访问。

但是,很容易看出这个实现基本上预期了不断的时间检索(如果字典表现良好)!这是一项相当成就,不可能被搜索树/特里打败!

class QGramIndex {
    private readonly int m_Maxlen;
    private readonly string m_Data;
    private readonly int m_Q;
    private int[] m_SA;
    private Dictionary<string, int> m_Dir = new Dictionary<string, int>();

    private struct StrCmp : IComparer<int> {
        public readonly String Data;
        public StrCmp(string data) { Data = data; }
        public int Compare(int x, int y) {
            return string.CompareOrdinal(Data.Substring(x), Data.Substring(y));
        }
    }

    private readonly StrCmp cmp;

    public QGramIndex(IList<string> strings, int maxlen, int q) {
        m_Maxlen = maxlen;
        m_Q = q;

        var sb = new StringBuilder(strings.Count * maxlen);
        foreach (string str in strings)
            sb.AppendFormat(str.PadRight(maxlen, '\u0000'));
        m_Data = sb.ToString();
        cmp = new StrCmp(m_Data);
        MakeSuffixArray();
        MakeIndex();
    }

    public int this[string s] { get { return FindInIndex(s); } }

    private void MakeSuffixArray() {
        // Approx. runtime: n^3 * log n!!!
        // But I claim the shortest ever implementation of a suffix array!
        m_SA = Enumerable.Range(0, m_Data.Length).ToArray();
        Array.Sort(m_SA, cmp);
    }

    private int FindInArray(int ith) {
        return Array.BinarySearch(m_SA, ith, cmp);
    }

    private int FindInIndex(string s) {
        int idx;
        if (!m_Dir.TryGetValue(s, out idx))
            return -1;
        return m_SA[idx] / m_Maxlen;
    }

    private string QGram(int i) {
        return i > m_Data.Length - m_Q ?
            m_Data.Substring(i) :
            m_Data.Substring(i, m_Q);
    }

    private void MakeIndex() {
        for (int i = 0; i < m_Data.Length; ++i) {
            int pos = FindInArray(i);
            if (pos < 0) continue;
            m_Dir[QGram(i)] = pos;
        }
    }
}

使用示例:

static void Main(string[] args) {
    var strings = new [] { "hello", "world", "this", "is", "a",
                           "funny", "test", "which", "i", "have",
                           "taken", "much", "too", "far", "already" };

    var index = new QGramIndex(strings, 10, 3);

    var tests = new [] { "xyz", "aki", "ake", "muc", "uch", "too", "fun", "est",
                         "hic", "ell", "llo", "his" };

    foreach (var str in tests) {
        int pos = index[str];
        if (pos > -1)
            Console.WriteLine("\"{0}\" found in \"{1}\".", str, strings[pos]);
        else
            Console.WriteLine("\"{0}\" not found.", str);
    }
}

答案 1 :(得分:2)

这是给你的WAG。 我在算法精明

中没有使用Knuthian

好的,所以naiive Trie通过从树的根开始并向下移动与键中每个字母匹配的分支来编码字符串键,从键的第一个字母开始。因此,键“foo”将映射到(root)->f->fo->foo,并且值将存储在'foo'节点指向的位置。

您正在搜索键中的任何子字符串,而不仅仅是从键的开头开始的子字符串。

因此,您需要做的是将节点与包含该特定子字符串的任何键相关联。在我之前给出的foo示例中,您不会在节点'f'和'fo'下找到foo值的引用。在支持您要查找的搜索类型的TST中,您不仅可以在所有三个节点('f','fo'和'foo')下找到foo对象,您还可以找到它在'o'和'oo'之下。

扩展搜索树以支持这种类型的索引会有一些明显的后果。首先,你刚刚爆炸了树的大小。令人吃惊的。如果您可以存储并以有效的方式使用它,您的搜索将花费O(1)时间。如果您的密钥保持静态,并且您可以找到一种方法来对索引进行分区,这样您在使用它时就不会受到巨大的IO损失,这可能会分摊价值。

其次,您会发现搜索小字符串会导致大量点击,这可能会使您的搜索无用,除非您在搜索字词上设置最小长度。

从好的方面来看,您可能还会发现可以通过标记化(如压缩压缩)或压缩不分支的节点压缩树(例如,如果你有'w' - &gt;'o ' - &gt;'o' - &gt;并且第一个'o'不分支,您可以安全地将其折叠为'w' - &gt;'oo')。也许甚至一个邪恶的屁股都可以让事情变得更容易......

无论如何,正如我所说,WAG。

答案 2 :(得分:0)

你的trie键与机器寄存器的大小相当,你会获得任何好处吗?所以,如果你在一个32位的盒子上,你可以一次比较4个字符,而不是单独比较每个字符?我不知道会增加你的应用程序的大小有多糟糕。

答案 3 :(得分:0)

是否有可能“散列”键值?基本上有第二棵树将搜索所有可能的值,指向第一棵树的键列表。

你需要2棵树;第一个是域对象的哈希值。第二个树是哈希值的搜索字符串。第二个树有多个键到相同的哈希值。

实施例 树1: STCKVRFLW - &gt;域对象

树2: 堆栈 - &gt; STCKVRFLW,STCK 结束 - &gt; STCKVRFLW,VRBRD,VR

因此,使用第二棵树上的搜索会为您提供在第一棵树上搜索的键列表。

答案 4 :(得分:0)

选择最小搜索字符串大小(例如,四个字符)。浏览字符串条目列表并构建每四个字符子字符串的字典,映射到子字符串出现的条目列表。当您进行搜索时,根据搜索字符串的前四个字符查找初始设置,然后将初始设置缩小到仅匹配完整搜索字符串的那些。

最糟糕的情况是O(n),但如果您的字符串条目几乎全部相同,那么您只会得到它。查找字典可能非常大,因此将它存储在磁盘上或使用关系数据库可能是个好主意: - )

答案 5 :(得分:0)

/编辑:我的一位朋友刚刚在构建q-gram表时指出了一个愚蠢的假设。结构可以更简单 - 因此,更快。我编辑了源代码和解释来反映这一点。我认为这可能是最终解决方案

受RafałFowgird对我之前回答的评论的启发,我已经更新了我的代码。我认为这应该是一个自己的答案,因为它也很长。此代码不是填充现有字符串,而是在原始字符串数组上构建索引。后缀数组不是存储单个位置,而是存储一对:目标字符串的索引和该字符串中后缀的位置。在结果中,只需要第一个数字。但是,第二个数字是构造 q -gram表所必需的。

该算法的新版本通过遍历后缀数组而不是原始字符串来构建 q -gram表。这样可以节省后缀数组的二进制搜索。因此,构造的运行时从 O n * log n )下降到 O n )(其中 n 是后缀数组的大小)。

请注意,与我的第一个解决方案一样,使用SubString会导致大量不必要的副本。显而易见的解决方案是编写一个扩展方法,创建一个轻量级的包装器,而不是复制字符串。然后必须略微调整比较。这留给读者练习。 ; - )

using Position = System.Collections.Generic.KeyValuePair<int, int>;

class QGramIndex {
    private readonly int m_Q;
    private readonly IList<string> m_Data;
    private Position[] m_SA;
    private Dictionary<string, int> m_Dir;

    public QGramIndex(IList<string> strings, int q) {
        m_Q = q;
        m_Data = strings;
        MakeSuffixArray();
        MakeIndex();
    }

    public int this[string s] { get { return FindInIndex(s); } }

    private int FindInIndex(string s) {
        int idx;
        if (!m_Dir.TryGetValue(s, out idx))
            return -1;
        return m_SA[idx].Key;
    }

    private void MakeSuffixArray() {
        int size = m_Data.Sum(str => str.Length < m_Q ? 0 : str.Length - m_Q + 1);
        m_SA = new Position[size];
        int pos = 0;
        for (int i = 0; i < m_Data.Count; ++i)
            for (int j = 0; j <= m_Data[i].Length - m_Q; ++j)
                m_SA[pos++] = new Position(i, j);

        Array.Sort(
            m_SA,
            (x, y) => string.CompareOrdinal(
                m_Data[x.Key].Substring(x.Value),
                m_Data[y.Key].Substring(y.Value)
            )
        );
    }

    private void MakeIndex() {
        m_Dir = new Dictionary<string, int>(m_SA.Length);

        // Every q-gram is a prefix in the suffix table.
        for (int i = 0; i < m_SA.Length; ++i) {
            var pos = m_SA[i];
            m_Dir[m_Data[pos.Key].Substring(pos.Value, 5)] = i;
        }
    }
}

用法与其他示例相同,减去构造函数所需的maxlen参数。

答案 6 :(得分:0)

要以有效的方式查询大量文本,您可以使用“编辑距离/前缀编辑距离”的概念。

  

编辑距离ED(x,y):从x到y的最小透明数

但是,在每个术语和查询文本之间计算ED是资源和时间消耗。因此,我们不是首先计算每个术语的ED,而是使用称为 Qgram Index 的技术提取可能的匹配术语。然后对这些选定的术语应用ED计算。

Qgram索引技术的一个优点是它支持模糊搜索

调整QGram索引的一种可能方法是使用Qgrams构建一个倒置索引。在那里,我们存储了包含特定Qgram的所有单词(而不是存储完整的字符串,您可以为每个字符串使用唯一的ID)。

  

col: col mbia, col ombo,gan col a,ta col ama

然后在查询时,我们计算查询文本和可用术语之间的常见Qgrams数。

using MvvmCross.Binding.BindingContext;
using MvvmCross.iOS.Views;

namespace MyApp.iOS.Views
{
    public partial class CreateAccount : MvxViewController
    {
        public CreateAccount() : base("CreateAccount", null)
        {
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            // Perform any additional setup after loading the view, typically from a nib.

            Title = "Register";

            var set = this.CreateBindingSet<CreateAccount, Core.ViewModels.CreateAccountModel>();
            set.Bind(regNameEdit).To(vm => vm.NameStr);
        }

        public override void DidReceiveMemoryWarning()
        {
            base.DidReceiveMemoryWarning();
            // Release any cached data, images, etc that aren't in use.
        }
    }
}

对于具有大量常见Qgrams的条款,我们根据查询字词计算ED / PED,然后向最终用户建议该术语。

您可以在以下项目中找到该理论的实现。随意问任何问题。 https://github.com/Bhashitha-Gamage/City_Search

要了解有关编辑距离,前缀编辑距离Qgram索引的更多信息,请观看Hannah Bast教授的以下视频 https://www.youtube.com/embed/6pUg2wmGJRo(课程从20:06开始)