【问题标题】:Why SortedSet<T>.GetViewBetween isn't O(log N)?为什么 SortedSet<T>.GetViewBetween 不是 O(log N)?
【发布时间】:2012-04-08 16:59:40
【问题描述】:

在 .NET 4.0+ 中,类SortedSet&lt;T&gt; 有一个名为GetViewBetween(l, r) 的方法,该方法返回树部分的接口视图,其中包含指定的两者之间的所有值。鉴于SortedSet&lt;T&gt; 被实现为红黑树,我自然希望它在O(log N) 时间内运行。 C++中类似的方法是std::set::lower_bound/upper_bound,Java中是TreeSet.headSet/tailSet,它们是对数的。

然而,事实并非如此。以下代码在 32 秒内运行,而 GetViewBetween 的等效 O(log N) 版本将使此代码在 1-2 秒内运行。

var s = new SortedSet<int>();
int n = 100000;
var rand = new Random(1000000007);
int sum = 0;
for (int i = 0; i < n; ++i) {
    s.Add(rand.Next());
    if (rand.Next() % 2 == 0) {
        int l = rand.Next(int.MaxValue / 2 - 10);
        int r = l + rand.Next(int.MaxValue / 2 - 10);
        var t = s.GetViewBetween(l, r);
        sum += t.Min;
    }
}
Console.WriteLine(sum);

我使用dotPeek 反编译了 System.dll,这就是我得到的:

public TreeSubSet(SortedSet<T> Underlying, T Min, T Max, bool lowerBoundActive, bool upperBoundActive)
    : base(Underlying.Comparer)
{
    this.underlying = Underlying;
    this.min = Min;
    this.max = Max;
    this.lBoundActive = lowerBoundActive;
    this.uBoundActive = upperBoundActive;
    this.root = this.underlying.FindRange(this.min, this.max, this.lBoundActive, this.uBoundActive);
    this.count = 0;
    this.version = -1;
    this.VersionCheckImpl();
}

internal SortedSet<T>.Node FindRange(T from, T to, bool lowerBoundActive, bool upperBoundActive)
{
  SortedSet<T>.Node node = this.root;
  while (node != null)
  {
    if (lowerBoundActive && this.comparer.Compare(from, node.Item) > 0)
    {
      node = node.Right;
    }
    else
    {
      if (!upperBoundActive || this.comparer.Compare(to, node.Item) >= 0)
        return node;
      node = node.Left;
    }
  }
  return (SortedSet<T>.Node) null;
}

private void VersionCheckImpl()
{
    if (this.version == this.underlying.version)
      return;
    this.root = this.underlying.FindRange(this.min, this.max, this.lBoundActive, this.uBoundActive);
    this.version = this.underlying.version;
    this.count = 0;
    base.InOrderTreeWalk((TreeWalkPredicate<T>) (n =>
    {
      SortedSet<T>.TreeSubSet temp_31 = this;
      int temp_34 = temp_31.count + 1;
      temp_31.count = temp_34;
      return true;
    }));
}

所以,FindRange 显然是 O(log N),但之后我们调用 VersionCheckImpl... 它对找到的子树进行线性时间遍历,仅用于重新计算其节点!

  1. 为什么需要一直进行这种遍历?
  2. 为什么 .NET 不包含 O(log N) 方法来根据键拆分树,如 C++ 或 Java?它在很多情况下真的很有帮助。

【问题讨论】:

  • 嗯,你是对的,破坏它的是 VersionCheckImpl()。检查在 .NET 集合类中是相当神圣的,我想不出更好的方法。只要您使用子集并检查到位,您就会得到O(log n),但是创建它是O(n)。您可以发布到 connect.microsoft.com 以指出这一点,并从内部人员那里获得看法。然而,他们将其关闭为“按设计”的可能性很高。
  • BCL 中的一个令人发指的错误。 GetRange 方法应该比线性过滤器更有效!
  • 要求SortedSet&lt;T&gt;.Count be O(1)的悲剧后果。
  • 我只想指出它并不像线性过滤器那么糟糕。如果我正确理解代码,则只有范围是线性遍历的,而不是整个集合。
  • 从 2017 年开始,SortedSet.GetViewBetween(...) 的 dotnet 核心实现是一个 O(log(n)) 实现。办公室。加上你需要提取的元素。

标签: c# .net complexity-theory sortedset


【解决方案1】:

关于version字段

UPDATE1:

在我的记忆中,BCL 中的很多(也许是全部?)集合都有 version 字段。

首先,关于foreach

据此msdn link

foreach 语句为数组或对象集合中的每个元素重复一组嵌入语句。 foreach 语句用于遍历集合以获取所需信息,但不应用于更改集合的内容以避免不可预知的副作用。

在许多其他集合中,version 受到保护,foreach 期间数据不会被修改

例如HashTableMoveNext()

public virtual bool MoveNext()
{
    if (this.version != this.hashtable.version)
    {
        throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
    }
    //..........
}

但是在SortedSet&lt;T&gt;MoveNext()方法中:

public bool MoveNext()
{
    this.tree.VersionCheck();
    if (this.version != this.tree.version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }       
    //....
}

UPDATE2:

但 O(N) 循环可能不仅适用于 version,还适用于 Count 属性。

因为MSDN of GetViewBetween说:

此方法返回由比较器定义的处于lowerValue 和upperValue 之间的元素范围的视图...... 您可以在视图和底层SortedSet(Of T) 中进行更改。

因此,对于每次更新,都应该同步 count 字段(键和值已经相同)。确保Count 正确

实现目标有两种策略:

  1. 微软的
  2. 莫诺的

首先,MS,在他们的代码中,他们牺牲了GetViewBetween() 的性能并赢得了Count 属性的性能。

VersionCheckImpl() 是同步Count 属性的一种方式。

第二,单声道。在 mono 的代码中,GetViewBetween() 更快,但在他们的 GetCount() 方法中:

internal override int GetCount ()
{
    int count = 0;
    using (var e = set.tree.GetSuffixEnumerator (lower)) {
        while (e.MoveNext () && set.helper.Compare (upper, e.Current) >= 0)
            ++count;
    }
    return count;
}

它总是一个 O(N) 操作!

【讨论】:

  • 用“子树计数”增加每个树节点不是更有意义吗?我以这种方式构建了一棵红黑树,几乎没有额外的努力。计数值在插入和删除时更新(包括任何受影响的祖先节点)。这种方法提供了对树的 O(log n) 排序索引,例如 tree[0] 返回最小节点,tree[tree.Count-1] 返回最大节点。但是,我没有提供“视图”概念,所以这可能会使它复杂化?
猜你喜欢
  • 2014-09-07
  • 1970-01-01
  • 2011-01-24
  • 2015-08-28
  • 2019-11-19
  • 1970-01-01
  • 2012-05-09
  • 2011-12-11
  • 1970-01-01
相关资源
最近更新 更多