【问题标题】:Data structure for indexed searches of subsets子集索引搜索的数据结构
【发布时间】:2011-07-11 19:33:23
【问题描述】:

我正在研究c# jquery implementation,并试图找出一种有效的算法来定位整个 DOM 的子集(例如子选择器)中的元素。目前我正在创建一个常用选择器的索引:构建 DOM 时的类、id 和标记。

基本数据结构正如人们所期望的那样,一棵Elements 的树,其中包含IEnumerable<Element> Children 和一个Parent。当使用Dictonary<string,HashSet<Element>> 来存储索引搜索整个域时,这很简单。

我一直无法理解使用索引搜索元素子集的最有效方法。我使用术语“子集”来指代链中后续选择器将从其运行的起始集。以下是我想到的方法:

  1. 从整个 DOM 中检索子查询的匹配项,并消除不属于子集的匹配项。这需要遍历每个匹配项的父项,直到找到根(并将其消除)或找到子集的成员(并且它是子项,因此包括在内)
  2. 为每个元素单独维护索引。
  3. 为每个元素维护一组父元素(通过消除遍历使 #1 更快)
  4. 为每个子查询重建整个索引。
  5. 只需手动搜索,主要选择器除外。

每种可能技术的成本很大程度上取决于正在执行的确切操作。 #1 在大多数情况下可能相当不错,因为大多数情况下,当您进行子选择时,您的目标是特定元素。所需的迭代次数为结果数 * 每个元素的平均深度。

第二种方法是迄今为止选择速度最快的方法,但代价是存储需求随深度呈指数增长,并且索引维护困难。我已经基本消除了这个。

第 3 种方法的内存占用相当差(尽管比第 2 种要好得多) - 这可能是合理的,但除了存储要求之外,添加和删除元素变得更加昂贵和复杂。

第 4 种方法无论如何都需要遍历整个选择,所以这似乎毫无意义,因为大多数子查询只会运行一次。仅当期望重复子查询时才有益。 (或者,我可以在遍历子集时执行此操作 - 除了某些选择器不需要搜索整个子域,例如 ID 和位置选择器)。

第 5 种方法适用于有限的子集,但比第 1 种方法适用于大部分 DOM 的子集。

关于如何最好地完成此任务有任何想法或其他想法吗?考虑到正在搜索的子集的大小与 DOM 的大小,我可以通过猜测哪个更有效来混合#1 和#4,但这很模糊,我宁愿找到一些通用的解决方案。现在我只使用#4(只有全 DOM 查询使用索引),这很好,但如果你决定做类似$('body').Find('#id') 的事情,那就太糟糕了

免责声明:这是早期优化。我没有需要解决的瓶颈,但作为一个学术问题我不能停止思考它......

解决方案

这是答案提出的数据结构的实现。可以完美地替代字典。

interface IRangeSortedDictionary<TValue>: IDictionary<string, TValue>
{
    IEnumerable<string> GetRangeKeys(string subKey);
    IEnumerable<TValue> GetRange(string subKey);

}
public class RangeSortedDictionary<TValue> : IRangeSortedDictionary<TValue>
{
    protected SortedSet<string> Keys = new SortedSet<string>();
    protected Dictionary<string,TValue> Index = 
        new Dictionary<string,TValue>();
    public IEnumerable<string> GetRangeKeys(string subkey)
    {
        if (string.IsNullOrEmpty(subkey)) {
            yield break;
        }
        // create the next possible string match
        string lastKey = subkey.Substring(0,subkey.Length - 1) +
            Convert.ToChar(Convert.ToInt32(subkey[subkey.Length - 1]) + 1);

        foreach (var key in Keys.GetViewBetween(subkey, lastKey))
        {
            // GetViewBetween is inclusive, exclude the last key just in case
            // there's one with the next value
            if (key != lastKey)
            {
                yield return key;
            }
        }
    }

    public IEnumerable<TValue> GetRange(string subKey)
    {
        foreach (var key in GetRangeKeys(subKey))
        {
            yield return Index[key];
        }
    }
    // implement dictionary interface against internal collections
}

代码在这里:http://ideone.com/UIp9R

【问题讨论】:

    标签: c# data-structures


    【解决方案1】:

    如果您怀疑名称冲突并不常见,那么它可能足够快,只需爬上树即可。

    如果冲突很常见,那么使用擅长有序前缀搜索的数据结构(例如树)可能会更快。您的各种子集构成了前缀。然后,您的索引键将包括选择器和总路径。

    对于 DOM:

    <path>
      <to>
        <element id="someid" class="someclass" someattribute="1"/>
      </to>
    </path>
    

    您将拥有以下索引键:

    <element>/path/to/element
    #someid>/path/to/element
    .someclass>/path/to/element
    @someattribute>/path/to/element
    

    现在,如果您根据前缀搜索这些键,您可以将查询限制为您想要的任何子集:

    <element>           ; finds all <element>, regardless of path
    .someclass>         ; finds all .someclass, regardless of path
    .someclass>/path    ; finds all .someclass that exist in the subset /path
    .someclass>/path/to ; finds all .someclass that exist in the subset /path/to
    #id>/body           ; finds all #id that exist in the subset /body
    

    一棵树可以在 O(log n) 中找到下界(第一个元素 >= 到您的搜索值),因为它是从那里排序的简单地迭代,直到你找到一个不再匹配前缀的键。会很快的!

    .NET 没有合适的树结构(它有 SortedDictionary,但遗憾的是没有公开所需的 LowerBound 方法),因此您需要自己编写或使用现有的第三方。优秀的C5 Generic Collection Library 具有适合Range 方法的树。

    【讨论】:

    • 不确定我将如何实现这一点,或者我可能对您的提议感到困惑。元素可以通过许多键(或无键)来识别,例如“body”、“#someid”、“.someclass”(它们都是选择器,也是我用作索引键的实际字符串)。任何元素都有许多可能的路径。
    • 我已经更新了我的答案以更好地解释这个想法。我认为它对这种琐碎的选择器非常有用,尽管我认为 jQuery 可能支持更复杂的选择器,这些选择器不适用于任何索引方案。
    • 感谢您的详细解释。我明白了,我认为这是理想的。我相信给定最常见的选择器模式,这将非常有效。无论如何,任何重要的选择器都必须通过迭代来完成——这不是我关心的。我希望解决的问题是更常见的情况,即通过链接两个琐碎的选择器(例如$("div[title]") 或我之前给出的最典型的body 示例)意外取消优化查询。如果无法对琐碎的选择器进行子选择,引擎必须遍历每个 div 才能找到具有标题的那些。
    • 我想我可以使用 SortedSet 实际上维护键列表。它有一个方法GetViewBetween,我可以将其与任意上限(例如,没有上限)一起使用,并在下一个键不再与子字符串匹配时停止查找。假设此方法在内部使用二进制搜索来定位起点,这应该提供我正在寻找的性能,那么我可以交叉引用匹配键的字典,这看起来对吗?
    • 是的,这很好。你也可以这样做。 GetViewBetween("aaa", "aab") 它将返回一个包含前缀为“aaa”的所有值的视图。
    猜你喜欢
    • 1970-01-01
    • 2013-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-06
    • 2012-02-02
    相关资源
    最近更新 更多