Dans .NET 4.0+, une classe SortedSet<T>
a une méthode appelée GetViewBetween(l, r)
qui renvoie une vue d'interface sur une partie d'arbre contenant toutes les valeurs entre les deux spécifiées. Étant donné que SortedSet<T>
est implémenté comme un arbre rouge-noir, je m'attends naturellement à ce qu'il s'exécute en O(log N)
temps. La méthode similaire en C++ est std::set::lower_bound/upper_bound
en Java, c'est TreeSet.headSet/tailSet
et ils sont logarithmiques.
Or, ce n'est pas vrai. Le code suivant s'exécute en 32 sec, alors que l'équivalent O(log N)
version de GetViewBetween
permettrait à ce code de s'exécuter en 1 ou 2 secondes.
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);
J'ai décompilé System.dll en utilisant dotPeek et voilà ce que j'ai obtenu :
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;
}));
}
Donc, FindRange
est évidemment O(log N)
mais après cela, nous appelons VersionCheckImpl
... qui effectue une traversée en temps linéaire du sous-arbre trouvé uniquement pour recompter ses nœuds !
- Pourquoi auriez-vous besoin de faire cette traversée tout le temps ?
- Pourquoi .NET ne contient pas de
O(log N)
méthode pour diviser un arbre en fonction de la clé, comme C++ ou Java ? C'est vraiment utile dans de nombreuses situations.
16 votes
Vous avez raison, c'est VersionCheckImpl() qui le gâche. La vérification est tout à fait sacrée dans les classes de collection .NET, je ne peux pas penser à un meilleur moyen. Vous obtiendrez O(log n) aussi longtemps que vous utiliser le sous-ensemble, avec vérification en place, sa création est cependant O(n). Vous pouvez envoyer un message à connect.microsoft.com pour signaler ce problème et obtenir l'avis des initiés. Il y a cependant de fortes chances qu'ils classent l'affaire comme "By design".
3 votes
Quelle gaffe scandaleuse de la part de la BCL. La méthode GetRange est censée être plus efficace qu'un filtre linéaire !
2 votes
@Hans Par curiosité, quel est l'intérêt de
VersionCheckImpl
?24 votes
C'est une conséquence tragique de l'exigence selon laquelle
SortedSet<T>.Count
être O(1) .0 votes
@RaymondChen Quel est le problème avec l'approche Java ? Quelle que soit leur implémentation,
GetViewBetween
est dérivable en temps logarithmique à partir de 2 appels deheadSet
ytailSet
. Toutes les autres méthodes conservent leur cohérence, etCount
La récupération est toujours en temps constant.1 votes
Il n'y a rien de mal dans l'approche Java. (Bien que je ne vois pas d'exigence que Collection.size soit O(1) [dans la documentation](http://docs.oracle.com/javase/6/docs/api/java/util/Collection.html#size()) .)
3 votes
Je veux juste souligner que ce n'est pas aussi mauvais que le filtre linéaire. Si je comprends bien le code, seule la plage est parcourue linéairement, pas l'ensemble.
3 votes
Ce
TreeSubSet
Le code est juste horrible. Le constructeur appelleFindRange
deux fois sans raison valable. Comme @hwiechers l'a dit, la traversée O(n) est seulement basée sur le sous-ensemble. Toujours 3 fois plus de traversées que nécessaire...7 votes
Vérifier de 2017 ici, la mise en œuvre du noyau dotnet de SortedSet<T>.GetViewBetween(...) est une mise en œuvre O(log(n)). Ofc. plus les éléments que vous devez extraire.