PrettyPrintTree - rysowanie drzew w konsoli

Wprowadzenie

W tej lekcji dowiesz się jak wykorzystać bibliotekę PrettyPrintTree do rysowania struktur drzewiastych w konsoli. Może to być szczególnie użyteczne na studiach na kursach, które są poświęcone algorytmom i strukturom danych.

Instalacja

Biblioteka nie znajduje się w repozytorium Maven Central, więc nie możemy po prostu dodać zależności do projektu opartego o Mavena, czy Gradle, ale autor zadbał o to, aby umieścić projekt w JitPacku.

W pom.xml dodaj:

<repositories>
    <repository>
        <id>jitpack.io</id>
        <url>https://jitpack.io</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.github.AharonSambol</groupId>
        <artifactId>PrettyPrintTreeJava</artifactId>
        <version>a680f94672</version>
    </dependency>
</dependencies>

Wersja jest generowana przez JitPack podczas budowania paczki, dlatego wygląda trochę nietypowo.

Przykład

Do stworzenia drzewa biblioteka nie wymaga używania żadnych specyficznych dla niej typów. Twoja klasa reprezentująca drzewo musi być dowolna, a dopiero później przy konfiguracji rysowania powinniśmy wskazać metodę pozwalającą pobrać wartość wierzchołka oraz jego dzieci.

Przykładowa klasa drzewa binarnego może wyglądać w najprostszej postaci tak:

import java.util.ArrayList;
import java.util.List;

public class Node<T> {
    private T value;
    private Node<T> left;
    private Node<T> right;

    public Node(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    public Node<T> getLeft() {
        return left;
    }

    public void setLeft(Node<T> left) {
        this.left = left;
    }

    public Node<T> getRight() {
        return right;
    }

    public void setRight(Node<T> right) {
        this.right = right;
    }

    public List<Node<T>> getChildren() {
        ArrayList<Node<T>> children = new ArrayList<>();
        children.add(left);
        children.add(right);
        return children;
    }
}

Istotne jest to, żeby klasa posiadała metodę, która zwróci kolekcję z dziećmi danego wierzchołja. Wadą biblioteki PrettyPrintTree jest to, że tymczasowo modyfikuje ona nasz obiekt drzewa, dlatego nie można wykorzystać kolekcji niemodyfikowalnych, czyli np. metody List.of.

Późniejsze rysowanie drzewa sprowadza się do utworzenia obiektu PrettyPrintTree i przekazanie do konstruktora dwóch wyrażeń lambda, albo referencji do metod:

  • pierwsza do pobrania dzieci danego wierzchołka.
  • druga do pobrania wartości wierzchołka w postaci Stringa.
import ajs.printutils.PrettyPrintTree;

class NodeExample {
    public static void main(String[] args) {
        Node<Integer> root = new Node<>(111);
        root.setLeft(new Node<>(222));
        root.setRight(new Node<>(333));
        root.getLeft().setLeft(new Node<>(444));
        root.getLeft().setRight(new Node<>(555));

        PrettyPrintTree<Node<Integer>> prettyTree = new PrettyPrintTree<>(
                Node::getChildren,
                node -> node.getValue().toString()
        );
        prettyTree.display(root);
    }
}

Drzewo nie musi być binarne. Każdy węzeł może mieć dowolną liczbę dzieci, np.:

package pl.javastart.printtree;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class MultiNode<T> {
    private T value;
    private List<MultiNode<T>> children = new ArrayList<>();

    public MultiNode(T value) {
        this.value = value;
    }

    public MultiNode<T> addChildren(MultiNode<T>... nodes) {
        children.addAll(Arrays.asList(nodes));
        return this;
    }

    public T getValue() {
        return value;
    }

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

W takiej sytuacji przykładowe drzewo może wyglądać tak:

package pl.javastart.printtree;

import ajs.printutils.PrettyPrintTree;

class MultiNodeExample {
    public static void main(String[] args) {
        MultiNode<String> root = new MultiNode<>("AAA");
        root.addChildren(
                new MultiNode<>("BBB").addChildren(
                        new MultiNode<>("EEE"),
                        new MultiNode<>("FFF")
                ), 
                new MultiNode<>("CCC"), 
                new MultiNode<>("DDD")
        );

        PrettyPrintTree<MultiNode<String>> prettyTree = new PrettyPrintTree<>(
                MultiNode::getChildren,
                MultiNode::getValue
        );
        prettyTree.display(root);
    }
}

W przypadku drzewa przechowującego wartości String sytuacja jest o tyle prostsza, że nie ma konieczności konwersji wartości do tego typu, więc można zastosować referencję do metody getValue() bez wywoływania metody toString().

Dodatkowa konfiguracja

Biblioteka daje kilka możliwości konfiguracji. Możesz ustawić wygląd węzłów i całego drzewa wywołując na obiekcie PrettyPrintTree odpowiednie metody:

  • trim(int) - ogranicza liczbę znaków wyświetlanych w każdym wierzchołku,
  • setColor(Color) - ustawia kolor wierzchołków (Color.NONE ustawia przezroczystość),
  • setBorder(boolean) - włącza, albo wyłącza obramowanie wierzchołków (domyślnie wyłączone),
  • pt.setEscapeNewline(boolean) - włącza, albo wyłącza łamanie wierszy przy pomocy symbolu \n,
  • pt.setMaxDepth(int) - pozwala określić maksymalną głębokość rysowanego drzewa.

Przykład z domyślną konfiguracją:

import ajs.printutils.PrettyPrintTree;

class MultiNodeExample {
    public static void main(String[] args) {
        MultiNode<String> root = new MultiNode<>("AAA");
        root.addChildren(
                new MultiNode<>("BBB").addChildren(
                        new MultiNode<>("EE\nEE"),
                        new MultiNode<>("FFF").addChildren(
                                new MultiNode<>("GGG"),
                                new MultiNode<>("HHH").addChildren(
                                        new MultiNode<>("XXX"),
                                        new MultiNode<>("YYY")
                                )
                        )
                ),
                new MultiNode<>("CCC"),
                new MultiNode<>("DDD")
        );

        PrettyPrintTree<MultiNode<String>> prettyTree = new PrettyPrintTree<>(
                MultiNode::getChildren,
                MultiNode::getValue
        );
        prettyTree.display(root);
    }
}

Zmodyfikowane ustawienia:

//...
PrettyPrintTree<MultiNode<String>> prettyTree = new PrettyPrintTree<>(
        MultiNode::getChildren,
        MultiNode::getValue
);
prettyTree.setColor(Color.RED);
prettyTree.setTrim(2);
prettyTree.setMaxDepth(3);
prettyTree.display(root);

Wady

Biblioteka jest stworzona przez pojedynczego autora i posiada drobne wady. Tymczasowo modyfikuje rysowany obiekt. Nie nadaje się do aplikacji wielowątkowych. Przydałby się także interfejs, który wymuszałby określony kształt metod getChildren() i getValue().

Link do biblioteki: https://github.com/AharonSambol/PrettyPrintTreeJava

Przykłady z tego artykułu: https://github.com/javastartpl/examples/tree/master/prettyprinttree

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.