Düz bir masayı ağaca ayrıştırmanın en etkili / zarif yolu nedir?


517

Sıralı bir ağaç hiyerarşisini saklayan düz bir tablonuz olduğunu varsayalım:

Id   Name         ParentId   Order
 1   'Node 1'            0      10
 2   'Node 1.1'          1      10
 3   'Node 2'            0      20
 4   'Node 1.1.1'        2      10
 5   'Node 2.1'          3      10
 6   'Node 1.2'          1      20

İşte bir şemamız var [id] Name. Kök düğümü 0 kurgusaldır.

                       [0] KÖK
                          / \ 
              [1] Düğüm 1 [3] Düğüm 2
              / \ \
    [2] Düğüm 1.1 [6] Düğüm 1.2 [5] Düğüm 2.1
          /          
 [4] Düğüm 1.1.1

Bunu doğru sıralı, doğru girintili bir ağaç olarak HTML'ye (veya bu konudaki metne) çıkarmak için hangi minimalist yaklaşımı kullanırsınız?

Üstelik, sadece temel veri yapılarınız (diziler ve hashmaplar) olduğunu, ebeveyn / çocuk referansları olan süslü nesnelerinizin olmadığını, ORM'nin olmadığını, çerçevenin olmadığını, sadece iki elinizi olduğunu varsayalım. Tablo, rasgele erişilebilen bir sonuç kümesi olarak temsil edilir.

Sahte kod veya sade İngilizce tamam, bu tamamen kavramsal bir soru.

Bonus soru: RDBMS'de böyle bir ağaç yapısını depolamanın temelde daha iyi bir yolu var mı?


DÜZENLEMELER VE EKLER

Bir yorumcunun ( Mark Bessey'nin ) sorusunu cevaplamak için: Bir kök düğüm gerekli değildir, çünkü asla gösterilmeyecektir. ParentId = 0, "bunlar üst düzeydir" ifade etme kuralıdır. Sıralama sütunu, aynı üst öğeye sahip düğümlerin nasıl sıralanacağını tanımlar.

Ben bahsettiğim "sonuç kümesi" bir dizi hashmaps (bu terminolojide kalmak için) olarak resmedilebilir. Çünkü benim örneğim zaten oradaydı. Bazı cevaplar ekstra mil gidip ilk inşa, ama sorun değil.

Ağaç keyfi olarak derin olabilir. Her düğümün N çocuğu olabilir. Yine de aklımda bir "milyonlarca giriş" ağacı yoktu.

Güvenecek bir şey için düğüm adlandırma seçimimi ('Düğüm 1.1.1') yanılmayın. Düğümler aynı şekilde 'Frank' veya 'Bob' olarak adlandırılabilir, hiçbir adlandırma yapısı ima edilmez, bu sadece okunabilir kılmaktı.

Kendi çözümümü yayınladım, böylece parçaları parçalara ayırabilirsiniz.


2
"Ebeveyn / çocuk referansları olan süslü nesneler yok" - neden olmasın? .AddChild (), .getParent () yöntemiyle temel bir Düğüm nesnesi oluşturmak, düğüm ilişkisini oldukça iyi bir şekilde modellemenizi sağlar.
matt b

2
Düzenli (n'nin> 2 olabileceği çocuklar) ağaç mı yoksa ikili ağaç mı (düğüm 0, 1 veya 2 çocuğu olabilir)?
BKimmel

Bir hashmap ile uygun bir düğüm veri yapısı uygulayabildiğiniz için, burada gerçek bir kısıtlama yoktur, sadece daha fazla çalışma vardır.
Svante

... ve aynen bunu yaptınız.
Svante

Yanıtlar:


451

Artık MySQL 8.0 özyinelemeli sorguları desteklediğine göre, tüm popüler SQL veritabanlarının standart sözdiziminde özyinelemeli sorguları desteklediğini söyleyebiliriz .

WITH RECURSIVE MyTree AS (
    SELECT * FROM MyTable WHERE ParentId IS NULL
    UNION ALL
    SELECT m.* FROM MyTABLE AS m JOIN MyTree AS t ON m.ParentId = t.Id
)
SELECT * FROM MyTree;

2017'de Özyinelemeli Sorgu Atma sunumumda MySQL 8.0'da özyinelemeli sorguları test ettim .

Aşağıda 2008'den aldığım orijinal yanıt var:


Ağaçla yapılandırılmış verileri ilişkisel bir veritabanında depolamanın birkaç yolu vardır. Örneğinizde gösterdikleriniz iki yöntem kullanır:

  • Bitişiklik Listesi ("üst" sütun) ve
  • Yol Numaralandırma (ad sütununuzdaki noktalı sayılar).

Başka bir çözüm İç İçe Kümeler olarak adlandırılır ve aynı tabloda da saklanabilir. Bu tasarımlar hakkında daha fazla bilgi için Joe Celko'nun " Smarties için SQL'de Ağaçlar ve Hiyerarşiler " bölümünü okuyun .

Genellikle ağaç yapılı verileri saklamak için Kapatma Tablosu ("Bitişik İlişki" olarak da bilinir) adı verilen bir tasarımı tercih ederim . Başka bir tablo gerektirir, ancak daha sonra ağaçları sorgulamak oldukça kolaydır.

Sunumumda Kapatma Tablosunu SQL ve PHP ile Hiyerarşik Veri Modelleri ve kitabımda SQL Antipatterns: Veritabanı Programlamanın Tuzaklarından Kaçınma .

CREATE TABLE ClosureTable (
  ancestor_id   INT NOT NULL REFERENCES FlatTable(id),
  descendant_id INT NOT NULL REFERENCES FlatTable(id),
  PRIMARY KEY (ancestor_id, descendant_id)
);

Tüm yolları, bir düğümden diğerine doğrudan bir soyun bulunduğu Kapatma Tablosunda saklayın. Her düğümün kendisine başvurması için bir satır ekleyin. Örneğin, sorunuzda gösterdiğiniz veri kümesini kullanarak:

INSERT INTO ClosureTable (ancestor_id, descendant_id) VALUES
  (1,1), (1,2), (1,4), (1,6),
  (2,2), (2,4),
  (3,3), (3,5),
  (4,4),
  (5,5),
  (6,6);

Şimdi düğüm 1'den başlayarak şöyle bir ağaç alabilirsiniz:

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1;

Çıktı (MySQL istemcisinde) aşağıdaki gibi görünür:

+----+
| id |
+----+
|  1 | 
|  2 | 
|  4 | 
|  6 | 
+----+

Başka bir deyişle, düğüm 3 ve 5 hariç tutulur, çünkü bunlar düğüm 1'den inen ayrı bir hiyerarşinin parçasıdır.


Re: e-satis'ten yakın çocuklar (veya yakın ebeveyn) hakkında yorum yapın. Özellikle acil bir alt veya üst öğeyi (veya başka bir mesafeyi) sorgulamayı kolaylaştırmak için path_lengthsimgesine " " sütunu ekleyebilirsiniz ClosureTable.

INSERT INTO ClosureTable (ancestor_id, descendant_id, path_length) VALUES
  (1,1,0), (1,2,1), (1,4,2), (1,6,1),
  (2,2,0), (2,4,1),
  (3,3,0), (3,5,1),
  (4,4,0),
  (5,5,0),
  (6,6,0);

Ardından, belirli bir düğümün hemen alt öğelerini sorgulamak için aramanızda bir terim ekleyebilirsiniz. Bunlar path_length1 olan torunlardır .

SELECT f.* 
FROM FlatTable f 
  JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
  AND path_length = 1;

+----+
| id |
+----+
|  2 | 
|  6 | 
+----+

@Ashraf'tan yorum yapın: "Ağacın tamamını [ada göre] sıralamaya ne dersiniz?"

Düğüm 1'in torunları olan tüm düğümleri döndürmek, bunları gibi diğer düğüm özniteliklerini içeren FlatTable'a katılmak ve ada göre namesıralamak için örnek bir sorgu .

SELECT f.name
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id)
WHERE a.ancestor_id = 1
ORDER BY f.name;

@Nate adlı kişiden yorum yap:

SELECT f.name, GROUP_CONCAT(b.ancestor_id order by b.path_length desc) AS breadcrumbs
FROM FlatTable f 
JOIN ClosureTable a ON (f.id = a.descendant_id) 
JOIN ClosureTable b ON (b.descendant_id = a.descendant_id) 
WHERE a.ancestor_id = 1 
GROUP BY a.descendant_id 
ORDER BY f.name

+------------+-------------+
| name       | breadcrumbs |
+------------+-------------+
| Node 1     | 1           |
| Node 1.1   | 1,2         |
| Node 1.1.1 | 1,2,4       |
| Node 1.2   | 1,6         |
+------------+-------------+

Bir kullanıcı bugün bir düzenleme önerdi. SO moderatörler düzenlemeyi onayladı, ancak geri alıyorum.

Düzenleme ORDER BY b.path_length, f.name, muhtemelen sıralamanın hiyerarşiyle eşleştiğinden emin olmak için yukarıdaki son sorgudaki ORDER BY öğesinin olması gerektiğini önerdi . Ancak bu işe yaramaz, çünkü "Düğüm 1.2" den sonra "Düğüm 1.1.1" komutunu verir.

Sıralamanın hiyerarşiyi mantıklı bir şekilde eşleştirmesini istiyorsanız, bu mümkündür, ancak yalnızca yol uzunluğuna göre sıralayarak değil. Örneğin, MySQL Kapatma Tablosu hiyerarşik veritabanına verdiğim cevaba bakın - Bilgileri doğru sırayla nasıl çıkarabilirim .


6
Bu çok zarif, teşekkürler. Bonus puan verilir. ;-) Yine de küçük bir dezavantaj görüyorum - çocuk ilişkisini açık ve örtülü olarak sakladığı için, ağaç yapısında küçük bir değişiklik için bile çok dikkatli bir GÜNCELLEME yapmanız gerekiyor.
Tomalak

16
Doğru, ağaç yapılarını bir veritabanında depolamanın her yöntemi, ağacı oluştururken veya güncellerken ya da ağaçları ve alt ağaçları sorgularken biraz çalışma gerektirir. Daha basit olmasını istediğiniz tasarımı seçin: yazma veya okuma.
Bill Karwin

2
@buffer, bir hiyerarşi için tüm satırları oluştururken tutarsızlıklar yaratma şansı vardır. Bitişiklik Listesi ( parent_id) her bir üst-alt ilişkisini ifade etmek için yalnızca bir satıra sahiptir, ancak Kapatma Tablosu çok sayıda vardır.
Bill Karwin

1
@BillKarwin Bir şey daha, belirli bir düğüme birden çok yolu olan bir grafik için uygun Kapatma Tablolarıdır (örneğin, herhangi bir yaprak veya yaprak olmayan düğümün birden fazla üst
kullanıcı

2
@Reza, böylece yeni bir çocuk düğümü eklerseniz (1) 'in tüm torunlarını sorgulayabilirsiniz ve bunlar yeni çocuğun atalarıdır.
Bill Karwin

58

Yuvalanmış kümeler (bazen Değiştirilmiş Ön Sipariş Ağacı Geçişi olarak da adlandırılır) kullanırsanız, tüm ağaç yapısını veya içindeki herhangi bir alt ağacı ağaç sırasına göre tek bir sorgu ile ayıklayabilirsiniz. ağaç yapısı boyunca sıralı bir yolu tanımlayan sütunları yönetir.

İçin Django-mptt şöyle bir yapı kullandım:

id parent_id ağaç_kimliği düzeyi lft sağ
- --------- ------- ----- --- ----
 1 boş 1 0 1 14
 2 1 1 1 2 7
 3 2 1 2 3 4
 4 2 1 2 5 6
 5 1 1 1 8 13
 6 5 1 2 9 10
 7 5 1 2 11 12

Bu, şuna benzer bir ağacı tanımlar ( idher bir öğeyi temsil eder):

 1
 + - 2
 | + - 3
 | + - 4
 |
 + - 5
     + - 6
     + - 7

Veya, lftve rghtdeğerlerinin nasıl çalıştığını daha belirgin hale getiren iç içe bir set diyagramı olarak :

 __________________________________________________________________________
| Kök 1 |
| ________________________________ ________________________________ |
| | Çocuk 1.1 | | Çocuk 1.2 | |
| | ___________ ___________ | | ___________ ___________ | |
| | | C 1.1.1 | | C 1.1.2 | | | | C 1.2.1 | | C 1.2.2 | | |
1 2 3___________4 5___________6 7 8 9___________10 11__________12 13 14
| | ________________________________ | | ________________________________ | |
| __________________________________________________________________________ |

Gördüğünüz gibi, ağaç sırayla, sadece var tüm satırları seçmek zorunda, belirli düğüm için tüm alt ağacı almak lftve rghtonun arasındaki değerler lftve rghtdeğerler. Belirli bir düğüm için ataların ağacını almak da basittir.

levelSütun daha fazla bir şey kolaylık denormalisation biraz ve tree_idsütun yeniden sağlar lftve rghtşekilde, insert, hareket ve silme etkilenen sütun sayısı azaltan her üst düzeyde düğüm için numaralandırma lftve rghtsütun olmak zorunda boşluklar oluşturmak veya kapatmak için bu işlemler yapıldığında buna göre ayarlanır. Başımı her işlem için gerekli olan sorguların etrafına sarmaya çalıştığımda bazı geliştirme notları yaptım .

Bir ağacı görüntülemek için aslında bu verilerle çalışmak açısından, tree_item_iteratorher bir düğüm için, istediğiniz her türlü ekranı oluşturmak için size yeterli bilgi vermesi gereken bir yardımcı program işlevi oluşturdum .

MPTT hakkında daha fazla bilgi:


9
Keşke sütun adları gibi lftve kısaltmaları kullanmayı bıraksak rght, kaç karakter yazmak zorunda kalmadık? bir?!
orustammanapov

21

Bu oldukça eski bir soru, ama birçok görüşü olduğu için bir alternatif sunmaya değer olduğunu düşünüyorum ve bence çok zarif bir çözüm.

Bir ağaç yapısını okumak için özyinelemeli Ortak Tablo İfadeleri (CTE'ler) kullanabilirsiniz. Bir kerede tüm ağaç yapısını alma, düğümün seviyesi, ana düğümü ve ana düğümün çocuklarında düzen hakkında bilgi sahibi olma imkanı verir.

Bunun PostgreSQL 9.1'de nasıl çalışacağını göstereyim.

  1. Bir yapı oluşturun

    CREATE TABLE tree (
        id int  NOT NULL,
        name varchar(32)  NOT NULL,
        parent_id int  NULL,
        node_order int  NOT NULL,
        CONSTRAINT tree_pk PRIMARY KEY (id),
        CONSTRAINT tree_tree_fk FOREIGN KEY (parent_id) 
          REFERENCES tree (id) NOT DEFERRABLE
    );
    
    
    insert into tree values
      (0, 'ROOT', NULL, 0),
      (1, 'Node 1', 0, 10),
      (2, 'Node 1.1', 1, 10),
      (3, 'Node 2', 0, 20),
      (4, 'Node 1.1.1', 2, 10),
      (5, 'Node 2.1', 3, 10),
      (6, 'Node 1.2', 1, 20);
  2. Bir sorgu yazın

    WITH RECURSIVE 
    tree_search (id, name, level, parent_id, node_order) AS (
      SELECT 
        id, 
        name,
        0,
        parent_id, 
        1 
      FROM tree
      WHERE parent_id is NULL
    
      UNION ALL 
      SELECT 
        t.id, 
        t.name,
        ts.level + 1, 
        ts.id, 
        t.node_order 
      FROM tree t, tree_search ts 
      WHERE t.parent_id = ts.id 
    ) 
    SELECT * FROM tree_search 
    WHERE level > 0 
    ORDER BY level, parent_id, node_order;

    Sonuçlar burada:

     id |    name    | level | parent_id | node_order 
    ----+------------+-------+-----------+------------
      1 | Node 1     |     1 |         0 |         10
      3 | Node 2     |     1 |         0 |         20
      2 | Node 1.1   |     2 |         1 |         10
      6 | Node 1.2   |     2 |         1 |         20
      5 | Node 2.1   |     2 |         3 |         10
      4 | Node 1.1.1 |     3 |         2 |         10
    (6 rows)

    Ağaç düğümleri bir derinlik düzeyine göre sıralanmıştır. Son çıktıda bunları sonraki satırlarda sunacağız.

    Her seviye için, ebeveyn içindeki parent_id ve node_order tarafından sıralanırlar. Bu bize bunları çıkış - bağlantı düğümünde ana öğeye bu sırayla nasıl sunacağımızı gösterir.

    Böyle bir yapıya sahip olmak HTML'de gerçekten güzel bir sunum yapmak zor olmaz.

    Yinelemeli CTE'ler PostgreSQL, IBM DB2, MS SQL Server ve Oracle'da mevcuttur .

    Özyinelemeli SQL sorguları hakkında daha fazla bilgi edinmek isterseniz, en sevdiğiniz DBMS belgelerine bakabilir veya bu konuyu kapsayan iki makalemi okuyabilirsiniz:


18

Oracle 9i'den itibaren CONNECT BY kullanabilirsiniz.

SELECT LPAD(' ', (LEVEL - 1) * 4) || "Name" AS "Name"
FROM (SELECT * FROM TMP_NODE ORDER BY "Order")
CONNECT BY PRIOR "Id" = "ParentId"
START WITH "Id" IN (SELECT "Id" FROM TMP_NODE WHERE "ParentId" = 0)

SQL Server 2005'ten itibaren, özyinelemeli ortak tablo ifadesi (CTE) kullanabilirsiniz.

WITH [NodeList] (
  [Id]
  , [ParentId]
  , [Level]
  , [Order]
) AS (
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , 0 AS [Level]
    , CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
  WHERE [Node].[ParentId] = 0
  UNION ALL
  SELECT [Node].[Id]
    , [Node].[ParentId]
    , [NodeList].[Level] + 1 AS [Level]
    , [NodeList].[Order] + '|'
      + CONVERT([varchar](MAX), [Node].[Order]) AS [Order]
  FROM [Node]
    INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[ParentId]
) SELECT REPLICATE(' ', [NodeList].[Level] * 4) + [Node].[Name] AS [Name]
FROM [Node]
  INNER JOIN [NodeList] ON [NodeList].[Id] = [Node].[Id]
ORDER BY [NodeList].[Order]

Her ikisi de aşağıdaki sonuçları verir.

ad
'Düğüm 1'
'Düğüm 1.1'
'Düğüm 1.1.1'
'Düğüm 1.2'
'Düğüm 2'
'Düğüm 2.1'

CTE hem SQLSunucusu ve oracle @Eric Weilnau kullanılabilir
Nisar

9

Bill'in cevabı oldukça gosh-darned iyi, bu cevap bana SO destekli dişli cevaplar dilememi sağlayan bazı şeyler ekliyor.

Her neyse, ağaç yapısını ve Order özelliğini desteklemek istedim. leftSiblingAynı Ordersoru orijinal soru (soldan sağa doğru korumak) için yapılan denir her düğümde tek bir özellik dahil .

mysql> desc düğümleri;
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| Alan | Türü | Boş | Anahtar | Varsayılan | Ekstra |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
| id | int (11) | HAYIR | PRI | NULL | auto_increment |
| adı | varchar (255) | EVET | | NULL | |
| leftSibling | int (11) | HAYIR | | 0 | |
+ ------------- + -------------- + ------ + ----- + ------- - + ---------------- +
Sette 3 sıra (0.00 saniye)

mysql> descacacencies;
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| Alan | Türü | Boş | Anahtar | Varsayılan | Ekstra |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
| relationId | int (11) | HAYIR | PRI | NULL | auto_increment |
| ebeveyn | int (11) | HAYIR | | NULL | |
| çocuk | int (11) | HAYIR | | NULL | |
| pathLen | int (11) | HAYIR | | NULL | |
+ ------------ + --------- + ------ + ----- + --------- + --- ------------- +
Sette 4 sıra (0.00 saniye)

Blogumda daha fazla ayrıntı ve SQL kodu .

Teşekkürler Bill cevabınız başlamanıza yardımcı oldu!


7

İyi bir seçim göz önüne alındığında, nesneleri kullanırdım. Her nesnenin bir childrenkoleksiyona sahip olduğu her kayıt için bir nesne oluşturabilir ve hepsini Id anahtarı olan bir assoc dizisinde (/ hashtable) saklayabilirim. Ve koleksiyonu bir kez karıştırın, çocukları ilgili çocuk alanlarına ekleyin. Basit.

Ancak, iyi bir OOP kullanımını kısıtlayarak eğlenceli olmadığınız için, muhtemelen aşağıdakilere dayalı olarak yinelenirim:

function PrintLine(int pID, int level)
    foreach record where ParentID == pID
        print level*tabs + record-data
        PrintLine(record.ID, level + 1)

PrintLine(0, 0)

Düzenleme: Bu diğer girişlerin bir çift benzer, ama biraz daha temiz olduğunu düşünüyorum. Ekleyeceğim bir şey: Bu son derece SQL yoğun. Çok kötü . Seçiminiz varsa OOP yoluna gidin.


"Çerçevesiz" ile kastettiğim bu - LINQ kullanıyorsunuz, değil mi? İlk paragrafınızla ilgili: Sonuç kümesi zaten var, neden önce tüm bilgileri yeni bir nesne yapısına kopyalıyorsunuz? (Bu konuda yeterince net değildim, üzgünüm)
Tomalak

Tomalak - kod yalancı kod değildir. Tabii ki işleri uygun seçimlere ve yineleyicilere ve gerçek bir sözdizimine bölmelisiniz! Neden OOP? Çünkü yapıyı tam olarak yansıtabilirsiniz. İşleri güzel tutar ve daha verimli olur (sadece bir seçim)
Oli

Aklımda da tekrarlanan seçimler yoktu. OOP ile ilgili olarak: Mark Bessey cevabında şöyle dedi: "Herhangi bir veri yapısını bir hashmap ile taklit edebilirsiniz, bu korkunç bir sınırlama değildir." Çözümünüz doğru, ancak bence OOP olmadan bile bir miktar ön iyileştirme var.
Tomalak

5

Bu hızlı bir şekilde yazılmıştır ve ne güzel ne de etkilidir (artı çok fazla otomatik kutuları, arasında dönüştürme intve Integersinir bozucu!), Ama işe yarıyor.

Kendi nesnelerimi oluşturduğum için muhtemelen kuralları ihlal ediyor ama hey bunu gerçek işten saptırma olarak yapıyorum :)

Bu ayrıca, sonuç kümesinin / tablonun, Düğümleri oluşturmaya başlamadan önce bir çeşit yapıya tamamen okunduğunu varsayar; bu, yüz binlerce satırınız varsa en iyi çözüm olmaz.

public class Node {

    private Node parent = null;

    private List<Node> children;

    private String name;

    private int id = -1;

    public Node(Node parent, int id, String name) {
        this.parent = parent;
        this.children = new ArrayList<Node>();
        this.name = name;
        this.id = id;
    }

    public int getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public void addChild(Node child) {
        children.add(child);
    }

    public List<Node> getChildren() {
        return children;
    }

    public boolean isRoot() {
        return (this.parent == null);
    }

    @Override
    public String toString() {
        return "id=" + id + ", name=" + name + ", parent=" + parent;
    }
}

public class NodeBuilder {

    public static Node build(List<Map<String, String>> input) {

        // maps id of a node to it's Node object
        Map<Integer, Node> nodeMap = new HashMap<Integer, Node>();

        // maps id of a node to the id of it's parent
        Map<Integer, Integer> childParentMap = new HashMap<Integer, Integer>();

        // create special 'root' Node with id=0
        Node root = new Node(null, 0, "root");
        nodeMap.put(root.getId(), root);

        // iterate thru the input
        for (Map<String, String> map : input) {

            // expect each Map to have keys for "id", "name", "parent" ... a
            // real implementation would read from a SQL object or resultset
            int id = Integer.parseInt(map.get("id"));
            String name = map.get("name");
            int parent = Integer.parseInt(map.get("parent"));

            Node node = new Node(null, id, name);
            nodeMap.put(id, node);

            childParentMap.put(id, parent);
        }

        // now that each Node is created, setup the child-parent relationships
        for (Map.Entry<Integer, Integer> entry : childParentMap.entrySet()) {
            int nodeId = entry.getKey();
            int parentId = entry.getValue();

            Node child = nodeMap.get(nodeId);
            Node parent = nodeMap.get(parentId);
            parent.addChild(child);
        }

        return root;
    }
}

public class NodePrinter {

    static void printRootNode(Node root) {
        printNodes(root, 0);
    }

    static void printNodes(Node node, int indentLevel) {

        printNode(node, indentLevel);
        // recurse
        for (Node child : node.getChildren()) {
            printNodes(child, indentLevel + 1);
        }
    }

    static void printNode(Node node, int indentLevel) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < indentLevel; i++) {
            sb.append("\t");
        }
        sb.append(node);

        System.out.println(sb.toString());
    }

    public static void main(String[] args) {

        // setup dummy data
        List<Map<String, String>> resultSet = new ArrayList<Map<String, String>>();
        resultSet.add(newMap("1", "Node 1", "0"));
        resultSet.add(newMap("2", "Node 1.1", "1"));
        resultSet.add(newMap("3", "Node 2", "0"));
        resultSet.add(newMap("4", "Node 1.1.1", "2"));
        resultSet.add(newMap("5", "Node 2.1", "3"));
        resultSet.add(newMap("6", "Node 1.2", "1"));

        Node root = NodeBuilder.build(resultSet);
        printRootNode(root);

    }

    //convenience method for creating our dummy data
    private static Map<String, String> newMap(String id, String name, String parentId) {
        Map<String, String> row = new HashMap<String, String>();
        row.put("id", id);
        row.put("name", name);
        row.put("parent", parentId);
        return row;
    }
}

Her zaman çok fazla kaynak kodu ile sunulduğunda algoritmaya özgü parçayı uygulamaya özgü parçadan filtrelemeyi zor buluyorum. Bu yüzden öncelikle dile özgü olmayan bir çözüm istedim. Ama bu işi yapıyor, zaman ayırdığınız için teşekkürler!
Tomalak

Şimdi ne demek istediğini görüyorum, eğer ana algoritma NodeBuilder.build () 'de açık değilse - muhtemelen bunu özetlemek için daha iyi bir iş yapmış olabilirdim.
matt b

5

SQL endekslerinin dahili btree temsilini kullanan gerçekten iyi çözümler var. Bu, 1998 civarında yapılan bazı büyük araştırmalara dayanmaktadır.

İşte bir örnek tablo (mysql olarak).

CREATE TABLE `node` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `tw` int(10) unsigned NOT NULL,
  `pa` int(10) unsigned DEFAULT NULL,
  `sz` int(10) unsigned DEFAULT NULL,
  `nc` int(11) GENERATED ALWAYS AS (tw+sz) STORED,
  PRIMARY KEY (`id`),
  KEY `node_tw_index` (`tw`),
  KEY `node_pa_index` (`pa`),
  KEY `node_nc_index` (`nc`),
  CONSTRAINT `node_pa_fk` FOREIGN KEY (`pa`) REFERENCES `node` (`tw`) ON DELETE CASCADE
)

Ağaç temsili için gerekli olan alanlar şunlardır:

  • tw: Soldan Sağa DFS Ön Sipariş dizini; burada root = 1.
  • pa: Ana düğüme yapılan başvuru (tw kullanarak), root değeri null.
  • sz: Kendisi de dahil olmak üzere düğümün dalının boyutu.
  • nc: sözdizimsel şeker olarak kullanılır. tw + nc'dir ve düğümün "sonraki alt öğesinin" ikisini temsil eder.

İşte tw tarafından sıralanan örnek bir 24 düğüm popülasyonu:

+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|   2 | A       |  2 |    1 |   14 |   16 |
|   3 | AA      |  3 |    2 |    1 |    4 |
|   4 | AB      |  4 |    2 |    7 |   11 |
|   5 | ABA     |  5 |    4 |    1 |    6 |
|   6 | ABB     |  6 |    4 |    3 |    9 |
|   7 | ABBA    |  7 |    6 |    1 |    8 |
|   8 | ABBB    |  8 |    6 |    1 |    9 |
|   9 | ABC     |  9 |    4 |    2 |   11 |
|  10 | ABCD    | 10 |    9 |    1 |   11 |
|  11 | AC      | 11 |    2 |    4 |   15 |
|  12 | ACA     | 12 |   11 |    2 |   14 |
|  13 | ACAA    | 13 |   12 |    1 |   14 |
|  14 | ACB     | 14 |   11 |    1 |   15 |
|  15 | AD      | 15 |    2 |    1 |   16 |
|  16 | B       | 16 |    1 |    1 |   17 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
|  18 | D       | 23 |    1 |    1 |   24 |
|  19 | E       | 24 |    1 |    1 |   25 |
+-----+---------+----+------+------+------+

Her ağaç sonucu yinelemesiz olarak yapılabilir. Örneğin, düğümün atalarının bir listesini tw = '22 'olarak almak için

Atalar

select anc.* from node me,node anc 
where me.tw=22 and anc.nc >= me.tw and anc.tw <= me.tw 
order by anc.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|   1 | Root    |  1 | NULL |   24 |   25 |
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

Kardeşler ve çocuklar önemsiz - sadece pa alan düzenini tw ile kullanın.

Torunları

Örneğin, tw = 17 köklü düğüm kümesi (dalı).

select des.* from node me,node des 
where me.tw=17 and des.tw < me.nc and des.tw >= me.tw 
order by des.tw;
+-----+---------+----+------+------+------+
| id  | name    | tw | pa   | sz   | nc   |
+-----+---------+----+------+------+------+
|  17 | C       | 17 |    1 |    6 |   23 |
| 359 | C0      | 18 |   17 |    5 |   23 |
| 360 | C1      | 19 |   18 |    4 |   23 |
| 361 | C2(res) | 20 |   19 |    3 |   23 |
| 362 | C3      | 21 |   20 |    2 |   23 |
| 363 | C4      | 22 |   21 |    1 |   23 |
+-----+---------+----+------+------+------+

ek Notlar

Bu metodoloji, ekler veya güncellemelerden çok daha fazla sayıda okuma olduğunda son derece kullanışlıdır.

Ağaca bir düğümün eklenmesi, hareketi veya güncellenmesi ağacın ayarlanmasını gerektirdiğinden, eyleme başlamadan önce tabloyu kilitlemek gerekir.

Ekleme / silme maliyeti yüksektir, çünkü tw indeksi ve sz (dal boyutu) değerlerinin, ekleme noktasından sonraki tüm düğümlerde ve tüm atalar için güncellenmesi gerekecektir.

Şube taşıma, dalın tw değerini aralık dışına taşımayı içerir, bu nedenle bir dalı taşırken yabancı anahtar kısıtlamalarını devre dışı bırakmak da gereklidir. Bir dalı taşımak için esasen dört sorgu gereklidir:

  • Şubeyi aralık dışına taşıyın.
  • Kaldığı boşluğu kapatın. (kalan ağaç normalleştirilmiştir).
  • Gideceği boşluğu açın.
  • Şubeyi yeni konumuna taşıyın.

Ağaç Sorgularını Ayarlama

Ağaçtaki boşlukların açılması / kapatılması, oluşturma / güncelleme / silme yöntemleri tarafından kullanılan önemli bir alt işlevdir, bu yüzden buraya dahil ediyorum.

İki parametreye ihtiyacımız var - küçülüp küçülmememizi veya büyütmemizi temsil eden bir bayrak ve düğümün tw dizini. Yani, örneğin tw = 18 (dal büyüklüğü 5 olan). Diyelim ki küçülüyoruz (tw'yi kaldırıyoruz) - bu, aşağıdaki örneğin güncellemelerinde '+' yerine '-' kullandığımız anlamına gelir.

Önce sz değerini güncellemek için (biraz değiştirilmiş) bir ata fonksiyonu kullanırız.

update node me, node anc set anc.sz = anc.sz - me.sz from 
node me, node anc where me.tw=18 
and ((anc.nc >= me.tw and anc.tw < me.pa) or (anc.tw=me.pa));

Daha sonra, tw kaldırılacak daldan daha yüksek olanlar için tw'i ayarlamamız gerekir.

update node me, node anc set anc.tw = anc.tw - me.sz from 
node me, node anc where me.tw=18 and anc.tw >= me.tw;

Daha sonra, pa sayısı tw kaldırılacak daldan daha yüksek olanlar için ebeveyn ayarlamalıyız.

update node me, node anc set anc.pa = anc.pa - me.sz from 
node me, node anc where me.tw=18 and anc.pa >= me.tw;

3

Kök öğelerinin sıfır olduğunu bildiğinizi varsayarsak, metne çıktı almak için sözde kod:

function PrintLevel (int curr, int level)
    //print the indents
    for (i=1; i<=level; i++)
        print a tab
    print curr \n;
    for each child in the table with a parent of curr
        PrintLevel (child, level+1)


for each elementID where the parentid is zero
    PrintLevel(elementID, 0)

3

Herhangi bir veri yapısını bir hashmap ile taklit edebilirsiniz, bu yüzden bu korkunç bir sınırlama değildir. Yukarıdan aşağıya doğru tarama yaparak, her sütun için bir giriş ile veritabanının her satırı için bir hashmap oluşturursunuz. Bu hashmapslerin her birini id ile anahtarlanmış bir "master" hashmap'a ekleyin. Herhangi bir düğümde henüz görmediğiniz bir "üst" varsa, ana hashmap'de bunun için bir yer tutucu girişi oluşturun ve gerçek düğümü gördüğünüzde doldurun.

Yazdırmak için, girinti seviyesini yol boyunca takip ederek basit bir derinlik-ilk önce verilerden geçin. Bunu, her satır için bir "alt" giriş tutarak ve verileri tararken doldurarak kolaylaştırabilirsiniz.

Verileri nasıl kullanacağınıza bağlı olarak, bir ağacı bir veritabanında depolamanın "daha iyi" bir yolu olup olmadığı konusunda. Hiyerarşideki her seviye için farklı bir tablo kullanan bilinen bir maksimum derinliğe sahip sistemleri gördüm. Sonuçta, ağaçtaki seviyeler sonuçta oldukça eşdeğer değilse çok mantıklıdır (üst düzey kategoriler yapraklardan farklıdır).


1

İç içe karma haritalar veya diziler oluşturulabilirse, o zaman en baştan tabloya gidebilir ve her öğeyi iç içe diziye ekleyebilirim. İç içe dizide hangi seviyeye ekleneceğini bilmek için kök düğüme her satırı izlemeliyim. Aynı ebeveyni tekrar tekrar aramam gerekmediği için not kullanabilirim.

Düzenleme: Ben tüm tabloyu bir dizi içine ilk kez okumak istiyorsunuz, bu yüzden DB tekrar tekrar sorgu olmaz. Tabii ki masanız çok büyükse bu pratik olmayacaktır.

Yapı oluşturulduktan sonra, önce derinlikten geçmek ve HTML'yi yazdırmak zorundayım.

Bu bilgileri tek bir tablo kullanarak depolamanın daha iyi bir temel yolu yoktur (yine de yanlış olabilirim;) ve daha iyi bir çözüm görmek isterim). Ancak, dinamik olarak oluşturulmuş db tablolarını kullanmak için bir düzen oluşturursanız, basitlik ve SQL cehennemi riskinden ödün vermeden yepyeni bir dünya açtınız;).


1
Yerine sadece alt düzey yeni bir seviye gerekli DB düzeni değiştirmek istemem. :-)
Tomalak

1

Öğeler örneğinizde gösterildiği gibi ağaç düzenindeyse, aşağıdaki Python örneği gibi bir şey kullanabilirsiniz:

delimiter = '.'
stack = []
for item in items:
  while stack and not item.startswith(stack[-1]+delimiter):
    print "</div>"
    stack.pop()
  print "<div>"
  print item
  stack.append(item)

Bunun yaptığı, ağaçtaki geçerli konumu temsil eden bir yığını korumaktır. Tablodaki her öğe için, geçerli öğenin üst öğesini bulana kadar yığın öğelerini (eşleşen div'leri kapatarak) açar. Sonra o düğümün başlangıcını çıkarır ve yığına doğru iter.

İç içe geçmiş öğeler yerine girinti kullanarak ağaç çıktısı almak istiyorsanız, div'ları yazdırmak için print deyimlerini atlayabilir ve her öğeden önce yığının boyutunun katlarına eşit sayıda boşluk yazdırabilirsiniz. Örneğin, Python'da:

print "  " * len(stack)

Bu yöntemi, bir dizi iç içe liste veya sözlük oluşturmak için de kolayca kullanabilirsiniz.

Düzenleme: Açıklamanızdan, isimlerin düğüm yolları olarak tasarlanmadığını görüyorum. Bu alternatif bir yaklaşım önerir:

idx = {}
idx[0] = []
for node in results:
  child_list = []
  idx[node.Id] = child_list
  idx[node.ParentId].append((node, child_list))

Bu, tuples dizilerinden oluşan bir ağaç oluşturur (!). idx [0] ağacın kök (ler) ini temsil eder. Bir dizideki her öğe, düğümün kendisinden ve tüm alt öğelerinin bir listesinden oluşan 2 demettir. Oluşturulduktan sonra, düğümlere kimlikleriyle erişmek istemiyorsanız idx [0] üzerinde bekleyebilir ve idx'i atabilirsiniz.


1

Bill'in SQL çözümünü genişletmek için temel olarak aynısını düz bir dizi kullanarak yapabilirsiniz. Ayrıca, dizelerinizin tümü aynı uzunluğa sahipse ve maksimum çocuk sayınız biliniyorsa (örneğin, ikili ağaçta), tek bir dize (karakter dizisi) kullanarak yapabilirsiniz. Eğer rasgele sayıda çocuğunuz varsa, bu işleri biraz karmaşıklaştırır ... Ne yapılabileceğini görmek için eski notlarımı kontrol etmem gerekir.

Daha sonra, biraz ağaçtan fedakarlık yapmak, özellikle de ağacınız seyrek ve / veya dengesizse, biraz endeks matematiğiyle, ağacınızı saklayarak, genişlik dizide ilk önce böyle bir dizine (bir ikili için) rastgele erişebilirsiniz. ağacı):

String[] nodeArray = [L0root, L1child1, L1child2, L2Child1, L2Child2, L2Child3, L2Child4] ...

dize uzunluğunu biliyorsun, biliyorsun

Ben şimdi işte çok fazla zaman harcayamıyorum ama ilgi ile bunu yapmak için kod biraz getirebilirsiniz.

Bunu, DNA kodonlarından yapılan ikili ağaçlarda, ağacı inşa eden bir işlemde aramak için kullanırız, daha sonra metin desenlerini aramak için düzleştirdik ve bulunduğunda, endeks matematiği (yukarıdan tersine) olsa da düğümü geri alırız ... çok hızlı ve verimli, sert ağacımızın nadiren boş düğümleri vardı, ancak gigabaytlarca veriyi bir anda toplayabiliriz.


0

Hiyerarşik yapılar için neo4j gibi nosql araçlarını kullanmayı düşünün. Örneğin, Linkedin gibi ağa bağlı bir uygulama couchbase kullanıyor (başka bir nosql çözümü)

Ancak nosql'i yalnızca veri-mart düzeyinde sorgular için kullanın ve işlemleri saklamak / korumak için kullanmayın


SQL ve "tablo olmayan" yapıların karmaşıklıklarını ve mükemmelliğini okuduktan sonra, bu benim de ilk düşüncem, nosql. Tabii ki, ihracat, vb için pek çok sorun vardır. Artı, OP sadece tablolar bahsetti. Oh iyi. Açıkça görüldüğü gibi, DB uzmanı değilim.
Josef.B
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.