web-dev-qa-db-ja.com

SortedSet <T> .GetViewBetweenがO(log N)ではないのはなぜですか?

.NET 4.0以降では、クラス_SortedSet<T>_にGetViewBetween(l, r)というメソッドがあり、指定された2つの間のすべての値を含むツリーパーツのインターフェイスビューを返します。 _SortedSet<T>_が赤黒木として実装されていることを考えると、当然、O(log N)時間で実行されると思います。 C++での同様のメソッドは_std::set::lower_bound/upper_bound_で、Javaそれは_TreeSet.headSet/tailSet_であり、対数です。

しかし、それは真実ではありません。次のコードは32秒で実行されますが、同等のO(log N)バージョンのGetViewBetweenでは、このコードは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にC++やJavaなどのキーに基づいてツリーを分割するためのO(log N)メソッドが含まれていないのはなぜですか?それは本当に多くの状況で役立ちます。
65
Skiminok

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<T>_のMoveNext()メソッドでは:

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

UPDATE2:

しかし、O(N)ループは、versionだけでなく、Countプロパティに対しても発生する可能性があります。

GetViewBetweenのMSDN が言ったので:

このメソッドは、比較子によって定義された、lowerValueとupperValueの間にある要素の範囲のビューを返します....ビューと基になるSortedSet(Of)の両方で変更を加えることができます。 T)

したがって、更新のたびに、countフィールドを同期する必要があります(キーと値はすでに同じです)。 Countが正しいことを確認するには

目標を達成するための2つのポリシーがありました。

  1. マイクロソフトの
  2. モノの

First.MSは、コード内でGetViewBetween()のパフォーマンスを犠牲にし、Countプロパティのパフォーマンスを獲得します。

VersionCheckImpl()は、Countプロパティを同期する1つの方法です。

第二に、モノ。 monoのコードでは、GetViewBetween()の方が高速ですが、GetCount() methodでは次のようになります。

_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)操作です!

19
llj098