Archiv für die Kategorie ‘Datenstrukturen’

Vorsicht im Umgang mit SortedSet

Mittwoch, 24. Juni 2009

Sortierte Mengen werden häufig als Implementierungen des Interfaces SortedSet umgesetzt. Dieses Interface sieht vor, dass die Elemente einer solchen Menge entweder gemäß ihrer natürlichen Ordnung (durch die compareTo-Methode von Comparable definiert) oder durch einen Comparator sortiert werden. Manch einer übersieht dabei aber eine wichtige Voraussetzung an die compareTo- bzw. compare-Methode: Sie muß mit der equals-Methode konsistent sein, d.h. für zwei Objekte a und b muß a.equals(b) genau dann true zurückliefern, wenn a.compareTo(b) (bzw. die compare-Methode des Comparators) den Wert 0 liefert. Wenn dies nicht der Fall ist, verhält sich die Collection möglicherweise nicht mehr wie eine Menge, d.h. es ist nicht mehr sichergestellt, dass

  1. es keine zwei (im Sinne von equals) gleichen Elemente in der Menge gibt.
  2. man ein Element, das sich (im Sinne von equals) von allen Elemten innerhalb der Collection unterscheidet, einfügen kann.

In diesem Fall wäre man mit einem gewöhnlichen Set, das man selbst mit Hilfe von Collections.sort() sortiert, besser beraten. Ein kleines Beispiel soll das Problem verdeutlichen:
Die Klasse Person überschreibt equals derart, dass Personen mit gleicher id als gleich betrachtet werden:

public class Person {
  private Integer id;
  private Integer age;
  public Integer getId() {
      return id;
  }
  public void setId(final Integer id) {
      this.id = id;
  }
  public Integer getAge() {
      return age;
  }
  public void setAge(final Integer age) {
    this.age = age;
  }
 
  @Override
  public boolean equals(final Object obj) {
    if (obj instanceof Person && id==(((Person)obj).id))
      return true;
    else
      return false;
  }
 
  public Person(final Integer id, final Integer age) {
    this.id = id;
    this.age = age;
  }
}

Wir definieren nun einen Comparator, der Personen nach ihrem Alter vergleicht und legen mit dessen Hilfe ein SortedSet an, in das wir zwei verschiedene Personen einfügen:

import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
 
public class Example {
 
  public static void main(final String[] args) {
    Comparator comp = new Comparator() {
      @Override
      public int compare( Person o1, Person o2) {
        return o1.getAge().compareTo(o2.getAge());
      }
    };
    SortedSet exampleSet =
      new TreeSet(comp);
    exampleSet.add(new Person(1,25));
    exampleSet.add(new Person(2,25));
    System.out.println("Size: "+exampleSet.size());
  }
}

Die Ausgabe bestätigt: es wurde lediglich eine Person tatsächlich in die Menge aufgenommen, obwohl die Personen anhand der equals-Methode eindeutig unterschieden werden. Bei komplexeren Comparator-Klassen kann es durchaus sinnvoll sein, die Konsistenz von equals-Methode und Comparator mittels eines eigenen Tests sicherzustellen. Aber bereits ein sorgfältiger Blick zur rechten Zeit kann einen hier vor schwer auffindbaren Fehlern bewahren.


Malte Wulf


Nested Sets – Teil II (Operationen)

Freitag, 29. Mai 2009

Hier im zweiten Teil der kleinen Serie über Nested Sets soll es um die Einfüge- und Löschoperation gehen. Vorher werden Operationen zur Ermittlung aller Kinder und aller Elternknoten vorgestellt.

Kindsknoten ermitteln
Die Kinder eines Knoten sind leicht erkennbar, denn ihre Grenzen müssen innerhalb der Grenzen ihres Elternknoten liegen.
Das heißt, die linke Grenze eines Knotens muß größer als die linke Grenze des Elternknotens sein, und gleichzeitig muß die linke Grenze kleiner als die rechte Grenze des Elternknotens sein. Eine Betrachtung der rechten Grenze ist nicht zwingend erforderlich, da ein Kindsknoten nicht “überlappend” sein kann (den Wertebereich des Elternknotens überschreitend).

Elternknoten ermitteln
Für die Ermittlung der Elternknoten gilt es, alle Knoten mit einer linken Grenze kleiner als die linke Grenze des Ausgangsknotens und einer rechten Grenze größer als die linke/rechte Grenze des Ausgangsknotens zu finden.

Löschoperation
Zum Löschen werden zuerst alle Kindsknoten des zu löschenden Knotens ermittelt und diese (mitsamt ihres Elternknotens) aus der DB entfernt. Anschließend muß die entstandene Lücke geschlossen werden, indem die rechte Grenze aller umschließenden Elternknoten angeapsst wird und alle “rechtsliegenden” Knoten nach links verschoben werden (Anpassung der linken und rechten Grenze). Das Offset (um das die Grenzen korregiert werden müssen) ist die Größe der Teilmenge, die der zu löschende Knoten repräsentiert (also rechte Grenze – linke Grenze + 1).

Ursprung – Katzen sollen entfernt werden:


simpletree3


Katzen und Kindsknoten wurden entfernt (Offset: 8-3+1 = 6):


simpletreekatzegeloescht


Anpassung aller Grenzen der Elternknoten des gelöschten Knotens und der rechtsliegenden Knoten:


simpletreekatzegeloeschtundgrenzenangepasst1

Einfügeoperation
Für das Einfügen eines Knotens muß zuerst der Raum für den Knoten geschaffen werden. Die Knoten die links vom einzufügenden Knoten liegen, bleiben unberührt. Alle Knoten die rechts vom einzufügenden Knoten liegen werden verschoben, ebenso der Knoten an dessen Position der Knoten eingefügt werden soll.

Es soll zwischen Katzen und Hunde, Kaninchen eingefügt werden.


simpletreesmall3

Es wurde für den neuen Knoten Raum geschaffen.


simpletreesmallmitlucke

Der neue “Kaninchen”-Knoten wurde eingefügt.


simpletreesmallmitkaninchen1

Zum Ende..
Es wird einen dritten Teil zu den Nested Sets geben, evtl. auch noch einen vierten.
In den kommenden Einträgen soll es dann um weitere Operationen, sowie Vor- und Nachteile von Nested Sets und die Persistierung in relationalen Datenbanken gehen.

Sven Seiller


Sven Seiler


Nested Sets – Teil I

Dienstag, 26. Mai 2009

In dieser kurzen Serie von Artikeln soll es um eine Einführung in Nested Sets gehen und deren Abbildung in einer relationalen Datenbank. In diesem ersten Teil soll erläutert werden, worum es sich bei Nested Sets handelt. Die verschiedene Operationen auf Nested Sets werden in den folgenden Artikeln behandelt werden.

Nested Sets stellen ein Modell dar, um Bäume mit Hilfe von (Teil-) Mengen abzubilden. Das Modell der Nested Sets wird meist in Datenbankanwendungen eingesetzt. Am einfachsten wird das Modell mit Hilfe eines Beispiels verständlich.



simpletree1

In der oben liegenden Zeichnung ist ein einfacher Baum dargestellt. Wie sieht dieser Baum jetzt als Nested Set aus?
Es gibt für jeden Knoten zwei Informationen, die linke und die rechte Grenze der Teilmenge. Folgend ein paar (redundante) Informationen/Regeln zu den Grenzen:

  • Die linke Grenze ist immer kleiner als die rechte Grenze (im folgenden wird oft nur noch von links und rechts gesprochen).
  • Die beiden Grenzen umfassen alle ihre Kindelemente.
  • Für Blätter gilt links plus eins ergibt rechts.
  • Beide Grenzen sind größer als die linke Grenze des Elternelements und kleiner als die rechte Grenze des Elternelements.

Die nächste Darstellung des Baums enthält jetzt zusätzlich die Nested Set Informationen (linke und rechte Grenze).


nestedset


Im kommenden Teil II werden dann, wie oben schon angekündigt, verschiedene Operationen erläutert werden. Motiviert wurde dieser Artikel durch die Verwendung von Nested Sets, zur Abbildung von Verzeichnis/Ordner Strukturen in einer Webanwendung.

Also bis demnächst..
Sven Seiller


Sven Seiler