【数据结构】初入数据结构的树(Tree)以及Java代码实现(二)

初入数据结构的树(Tree)以及Java代码实现(二)


这里注重的讲解树的Java代码实现,为了简单易懂,所以这里的实现主要是无序树

前提概念


我这里会分别使用两个角度来实现无序树:

  • 底层结构是数组还是链表
  • 树的五种表示法,双亲表示法,孩子表示法,双亲孩子表示法,孩子兄弟表示法,孩子兄弟双亲表示法

实现树的功能有:

  • 是否空树?
  • 查询树的结点数量
  • 查询树的高
  • 为某个结点插入子结点
  • 设置根结点
  • 查询返回根结点
  • 查询某个结点的所有子结点
  • 查询某个结点的父结点
  • 查询某个结点的度
  • 查询树的度
  • 清空树的所有结点

底层数据结构 | 数组


双亲表示法

结点构造TreeNode:

  • 双亲表示法的结点构造比较简单,只需要两个属性data, 和parent指针
  • data用于存储该结点的数据,parent用于存放其父结点在数组中的索引

使用数据结构去实现一颗树,非常的简单,一般树的根结点肯定是数组索引为0的位置,剩余的位置就看谁先插入, 谁就分配之后的索引位置;像双亲表示法,除了根结点没有父结点外,其余每个结点都有一个唯一的父结点。将所有结点存到一个数组中。每个结点都有一个数据域data和一个数值parent指示其父结点在数组中存放的位置。根结点由于没有父结点,其父节点的索引parent-1表示

TreeNode.java

/**
     * 双亲表示法的结点数据结构
     * 双亲表示法意味着每个结点需要知道自己的双亲是谁,需要记录自己的双亲
     *
     * @param <E>
     */
    public class TreeNode<E> {
        /**
         * 该结点的数据
         */
        private E data;
        /**
         * 该结点的父结点
         * 这里存在是父结点在数组中的索引,不是具体的结点
         */
        private int parent;

        public TreeNode(E data, int parent) {
            this.data = data;
            this.parent = parent;
        }

        public void setData(E data) {
            this.data = data;
        }

        public E getData() {
            return data;
        }

        public int getParent() {
            return parent;
        }

        public void setParent(int parent) {
            this.parent = parent;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof TreeNode)) {
                return false;
            }

            TreeNode<?> treeNode = (TreeNode<?>) o;

            if (getParent() != treeNode.getParent()) {
                return false;
            }
            return getData() != null ? getData().equals(treeNode.getData()) : treeNode.getData() == null;
        }

        @Override
        public int hashCode() {
            return Objects.hash(getData(), getParent());
        }

        @Override
        public String toString() {
            return "TreeNode{" +
                    "data=" + data +
                    ", parent=" + parent +
                    '}';
        }
    }

ParentRepresentationTree.java

/**
 * 树的双亲(父结点)表示法的Java实现
 * 这里是拿数组作为树的底层数据结构
 *
 * 1. 是否空树?
 * 2. 查询树的结点数量
 * 3. 查询树的高
 * 4. 为某个结点插入子结点
 * 5. 设置根结点
 * 6. 查询返回根结点
 * 7. 查询某个结点的所有子结点
 * 8. 查询某个结点的父结点
 * 9. 查询某个结点的度
 * 10. 查询树的度
 * 11. 清空树的所有结点
 *
 *
 * 比较有难度的应该是求树的度,和求树的高(深度)
 * 但是它们其实也只是一个遍历的过程,求所有可达结点的层级,然后取最大层级,就可以知道树的高了
 */
public class ParentRepresentationTree<T> {

    /**
     * 树的容量
     */
    private int capacity;

    /**
     * 树的结点数量
     */
    private int nodeCount;

    /**
     * 树的实际存储结构,由一个数组来做树的底层存储结构
     */
    private TreeNode<T>[] nodes;

    /**
     * 树的构造函数
     *
     * @param capacity
     */
    public ParentRepresentationTree(int capacity) {
        this.capacity = capacity;
        nodes = new TreeNode[capacity];
    }

    /**
     * 是否空树
     *
     * @return
     */
    public boolean isEmpty() {
        return nodeCount == 0;
    }

    /**
     * 查找树的结点数量
     *
     * @return
     */
    public int getNodeCount() {
        return nodeCount;
    }

    /**
     * 树高是多少,时间复杂度为O(n2),如果树的深度低的的话,约等于O(n)
     * 说白了, 求树高也只是求层级最大值的过程
     * 1. 因为我们TreeNode结构的特殊性,只记录了parent,所以为了迭代遍历,只能自地向上遍历,像链表一样
     * 2. 求每个结点能到达的父结点的层级
     * 3. 然后求最大值即可,得到的最值,那肯是可达的最大层级,那么肯定是树的高
     * @return
     */
    public int height() {
        int max = 0;
        //遍历所有结点
        for (int i = 0; i < nodeCount; i++) {
            //默认第一次层级为1
            int level = 1;
            int parentIndex = nodes[i].parent;
            //只要父结点不是根结点,就继续遍历下去
            while (parentIndex != -1){
                level++;
                parentIndex = nodes[parentIndex].parent;
            }
            //求层级最大值
            max = level > max ? level :max;
        }

        return max;
    }

    /**
     * 设置根结点
     *
     * @param data
     */
    public void setRootNode(T data) {
        this.nodes[0] = new TreeNode<>(data, -1);
        nodeCount++;
    }

    /**
     * 查询根结点
     *
     * @return
     */
    public TreeNode<T> getRootNode() {
        return this.nodes[0];
    }

    /**
     * 为某个结点添加子结点
     *
     * @param data
     * @param parent
     */
    public TreeNode<T> addChild(T data, TreeNode<T> parent) {
        if (nodeCount < capacity) {
            //为parent结点,添加子结点
            TreeNode<T> node = new TreeNode<>(data, indexOf(parent));
            nodes[nodeCount] = node;
            nodeCount++;
            return node;
        } else {
            throw new RuntimeException("树容量不够,请扩容");
        }
    }

    /**
     * 查找某个结点在索引中的位置
     *
     * @param node
     * @return
     */
    private int indexOf(TreeNode<T> node) {
        for (int i = 0; i < nodeCount; i++) {
            if (nodes[i].equals(node)) {
                return i;
            }
        }
        throw new RuntimeException("树中不存在该结点数据");
    }

    /**
     * 查找该结点的父结点,并返回
     *
     * @param node
     * @return
     */
    public TreeNode<T> getParentNode(TreeNode<T> node) {
        return nodes[node.parent];

    }

    /**
     * 查找某个结点的所有子结点
     *
     * @param parentNode
     * @return
     */
    public List<TreeNode<T>> listChildNode(TreeNode<T> parentNode) {
        List<TreeNode<T>> childrens = new ArrayList<>();
        for (int i = 0; i < nodeCount; i++) {
            if (nodes[i].parent == indexOf(parentNode)) {
                childrens.add(nodes[i]);
            }
        }
        return childrens;
    }

    /**
     * 树的度
     * 1. 求所有结点的子结点个数,既结点的度
     * 2. 比较所有结点的度,得到最大值,就是树的度
     *
     * @return
     */
    public int degreeForTree() {
        //说白了就是求所有结点度的最大值
        int max = 0;
        for (int i = 0; i < nodeCount; i++) {
            int compareNum = listChildNode(nodes[i]).size();
            max = compareNum > max ? compareNum : max;
        }
        return max;
    }


    /**
     * 某个结点的度
     *
     * @return
     */
    public int degreeForNode(TreeNode<T> node) {
        //求某个结点有多少个子结点,就代表该结点的度是多少
        return listChildNode(node).size();
    }

    /**
     * 清空树的所有数据
     */
    public void clear() {
        for (int i = 0; i < nodeCount; i++) {
            nodes[i] = null;
            nodeCount--;
        }
    }


    public static void main(String[] args) {
        ParentRepresentationTree<Integer> tree = new ParentRepresentationTree<>(20);
        tree.setRootNode(20);
        TreeNode<Integer> node = tree.addChild(12,tree.getRootNode());
        TreeNode<Integer> node2 = tree.addChild(34,node);
        tree.addChild(45,node);
        tree.addChild(56,node2);
        System.out.println("height: " + tree.height());
        System.out.println("nodeCount: " + tree.getNodeCount());
        System.out.println("degree: " + tree.degreeForTree());
        System.out.println("root degree: " + tree.degreeForNode(tree.getRootNode()));
    }

}

需要注意的点:

  • 因为是数组结构,所以存在容量问题capacity, 每次添加元素的时候,要做容量判断,这里为了简单,没有去做自动扩容,有兴趣可以自己去实现
  • 需要重点注意的就是height()求树高,degreeForTree()求树的度,listChildNode()求某结点的子结点,getParentNode()求某结点的父结点这几个方法
  • height()求树高是重中之重,就是遍历树中的所有结点,因为双亲表示法的特殊性,向下找子结点的效率不如向上找父结点;所以把遍历时把所有当做最低层的叶子结点,自底向上遍历,只要有父结点,层级 + 1,直到遍历到根结点为止,这样我们就可以得到这棵树所有结点的高度,然后再o(n)求所有结点高度的最大值,最大的那个高度就是该树的高度
  • height()求树高的时间复杂度为O(n2),如果树的深度低的的话,约等于O(n),一般树都不会很深,所以可以简单的说是O(n)
  • degreeForTree()求树的度,求树中所有结点的度,然后求最大值,因为listChildNode()的时间复杂度为O(n)所以求树的度的时间复杂度为O(n2)
  • listChildNode()求某结点的子结点,时间复杂度O(n), 树有多少个结点就遍历几次
  • getParentNode()求某结点的父结点, 时间复杂度是O(1) ,因为是双亲表示法,每个结点都存储了父结点的索引,所以查找父节点效率非常高

孩子表示法

孩子表示法其实就是在双亲表示法的想法上换了一下,既双亲表示法要查询某个结点的子结点比较麻烦,那我们索性就让每个结点记住自己有哪些孩子

  • 有数据域data和孩子域childrens
  • data存储该结点的数据,childrens就用一个自动扩容的集合来表示,用于存放子结点们的索引
    在这里插入图片描述

TreeNode.java

 /**
     * 孩子表示法的树
     * 孩子表示法则每个结点需要记录自己的子结点有哪些
     * 其实保存子结点集合的方式有两种,一种是直接保存TreeNode,另一种就是保存子结点在数组中的索引
     *
     * @param <E>
     */
    public class TreeNode<E>{

        private E data;
        private List<Integer> childrens;

        public TreeNode(E data) {
            this.data = data;
            this.childrens = new LinkedList<>();
        }

        public TreeNode(E data, List<Integer> childrens) {
            this.data = data;
            this.childrens = childrens;
        }

        public E getData() {
            return data;
        }

        public void setData(E data) {
            this.data = data;
        }

        public List<Integer> getChildrens() {
            return childrens;
        }

        public void setChildrens(List<Integer> childrens) {
            this.childrens = childrens;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            TreeNode<?> treeNode = (TreeNode<?>) o;

            if (data != null ? !data.equals(treeNode.data) : treeNode.data != null) return false;
            return childrens != null ? childrens.equals(treeNode.childrens) : treeNode.childrens == null;
        }

        @Override
        public int hashCode() {
            int result = data != null ? data.hashCode() : 0;
            result = 31 * result + (childrens != null ? childrens.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "TreeNode{" +
                    "data=" + data +
                    ", childrens=" + childrens +
                    '}';
        }
    }

ChildrenPepresentationTree.java

package com.snailmann.datastructure.tree;

import java.util.LinkedList;
import java.util.List;

/**
 * 孩子表示法的树
 * 孩子表示法的树也是以数组为底层数据结构
 *
 * 1. 是否空树?
 * 2. 查询树的结点数量
 * 3. 查询树的高
 * 4. 为某个结点插入子结点
 * 5. 设置根结点
 * 6. 查询返回根结点
 * 7. 查询某个结点的所有子结点
 * 8. 查询某个结点的父结点
 * 9. 查询某个结点的度
 * 10. 查询树的度
 * 11. 清空树的所有结点
 *
 *
 * 相比之下,孩子表示法的难度在于求树的深度,递归更容易实现,但需要好好理解
 * 每次递归的意图在于找到该结点所有子树的深度,然后找到最大深度的子树,然后 + 1就是本结点的最大深度
 * 每次递归的max是该结点所有子树共享的,就是个临时变量
 * @param <T>
 */
public class ChildrenPepresentationTree<T> {

    /**
     * 树的容量
     */
    private int capacity;

    /**
     * 树的结点数量
     */
    private int nodeCount;

    /**
     * 树的底层数据结构,数组
     */
    private TreeNode<T>[] nodes;


    public ChildrenPepresentationTree(int capacity) {
        this.capacity = capacity;
        this.nodeCount = 0;
        this.nodes = new TreeNode[capacity];
    }

    /**
     * 设置树的根结点
     * @param data
     */
    public TreeNode<T> setRootNode(T data){
        if (nodeCount < capacity){
            nodeCount++;
            nodes[0] = new TreeNode<>(data);
            return nodes[0];
        } else {
            throw new RuntimeException("超过树的容量,请扩容");
        }
    }

    /**
     * 查询根结点
     * @return
     */
    public TreeNode<T> getRootNode(){
        return nodes[0];
    }

    /**
     * 求树的高度
     * @return
     */
    public int height(){
        return nodeDepth(getRootNode());
    }

    /**
     * 求子树的深度,时间复杂度应该是0(n2)
     * 递归实现,不同于双亲表示法求高度的循环方式,因为双亲是自底向上,每次是一对一遍历,所以可以使用循环
     * 而孩子表示法是自上而下,每次往下就有多个分叉,而且每次的向下都要解决同一个问题,所以使用递归更好解决
     * @param node
     * @return
     */
    public int nodeDepth(TreeNode<T> node) {

        //空树
        if (node == null){
            return 0;
        }
        
        int max = 0;

        /**
         * 递归退出条件
         * 当我递归到叶子结点,说明我是最底层,则返回该层层级,既1
         */
        if (node.childrens.size() <= 0){
            return 1;
        }

        /**
         * 当不满足退出条件时,则说明还要继续往下递归
         * 每次的递归目的是比较该结点所有子树的深度,得到该结点子树的最大深度,然后 + 1 返回给上层
         * 为什么  + 1,因为本结点的最深的子树的高 + 1就是本结点的最大深度,然后才能返给上一结点
         */
        for (int index : node.childrens){
            //获取子树的深度
            int depth = nodeDepth(nodes[index]);
            //求所有子树深度的最大值
            max = max > depth ?max : depth;
        }
        //返回该结点的最大深度, max是该结点的子结点的最大深度, max + 1就是本结点的最大深度
        return max + 1;


    }


    /**
     * 是否空树
     * @return
     */
    public boolean isEmpty(){
        return nodeCount == 0;
    }

    /**
     * 查询树的结点数量
     * @return
     */
    public int getNodeCount(){
        return nodeCount;
    }

    /**
     * 为某个结点插入子结点
     * @param data
     * @param parent
     * @return
     */
    public TreeNode<T> addChild(T data,TreeNode<T> parent){
        //如果树有容量,则添加
        if (nodeCount < capacity){
            TreeNode<T> childNode = new TreeNode<>(data);
            //将新结点加入到数组中
            nodes[nodeCount] = childNode;
            //父结点要把新子结点纳入自己的子结点集合中
            parent.childrens.add(nodeCount);
            nodeCount++;
            return childNode;
        }else {
            throw new RuntimeException("超过树的容量,请扩容");
        }
    }

    /**
     * 查询某个结点的子结点集合
     * 返回某个结点的所有子结点
     * @param node
     * @return
     */
    public List<TreeNode<T>> listChildNode(TreeNode<T> node){
        List<TreeNode<T>> childNodes = new LinkedList<>();
        for (Integer index : node.childrens){
            childNodes.add(nodes[index]);
        }
        return childNodes;
    }

    /**
     * 找到某个结点的父结点
     * @param node
     * @return
     */
    public TreeNode<T> getParentNode(TreeNode<T> node){
        for (int i = 0; i < nodeCount; i++) {
            if (nodes[i].childrens.contains(indexOf(node))){
                return nodes[i];
            }

        }
        return null;
    }

    /**
     * 找到某个结点在数组中的索引
     * @param node
     * @return
     */
    private Integer indexOf(TreeNode<T> node) {
        for (int i = 0; i < nodeCount; i++) {
            if (nodes[i].equals(node)){
                return i;
            }
        }
        throw new RuntimeException("没有该结点");
    }

    /**
     * 求树的度
     * @return
     */
    public int degreeForTree(){
        int max = 0;
        for (int i = 0; i < nodeCount; i++) {
            max = max > nodes[i].childrens.size() ? max  : nodes[i].childrens.size();
        }
        return max;
    }

    /**
     * 求结点的度
     * @param node
     * @return
     */
    public int degreeForNode(TreeNode<T> node){
        return node.childrens.size();
    }

    public static void main(String[] args) {
        ChildrenPepresentationTree<Integer> tree = new ChildrenPepresentationTree<>(20);
        tree.setRootNode(20);
        TreeNode<Integer> node = tree.addChild(12,tree.getRootNode());
        TreeNode<Integer> node2 = tree.addChild(34,node);
        tree.addChild(45,node);
        tree.addChild(56,node2);
        System.out.println("height: " + tree.height());
        System.out.println("nodeCount: " + tree.getNodeCount());
        System.out.println("degree: " + tree.degreeForTree());
        System.out.println("root degree: " + tree.degreeForNode(tree.getRootNode()));
    }


}

需要注意的点:

  • 因为是数组结构,所以存在容量问题capacity, 每次添加元素的时候,要做容量判断,这里为了简单,没有去做自动扩容,有兴趣可以自己去实现
  • 需要重点注意的就是height()求树高,degreeForTree()求树的度,listChildNode()求某结点的子结点,getParentNode()求某结点的父结点这几个方法
  • height()求树高是重中之重,不同于双亲表示法可以通过遍历树的所有结点,自底向上遍历的找父结点的方式就可以实现求树的高度,孩子表示法没有记录唯一的父结点,只有孩子集合,所以不像双亲表示法一样有一对一的调用链 ,而是一对多的孩子链,所以使用递归的方式会更好;怎么实现呢?递归函数nodeDepth(),传入某个结点,求该结点作为根结点的树的高, 两个递归退出条件,当结点node等于null, 代表空树。当结点的没有子结点,既为叶子结点时,把叶子结点为根结点树的树高1返回。其余正常情况是找出本结点node的所有子结点的树高,求子结点树高的最大值max, 然后max + 1就是本结点的最大树高,然后给父结点(max变量是临时变量,是用于同结点下的所有子结点树高求最大值的临时变量,如果你熟悉在一群数中求最大值的函数就秒懂了) ; 总之每个递归的目的,既求树高的基本问题是找到该结点的高,怎么找,每次递归找本结点所有子结点的高,然后比较得最大值max, 然后max + 1就是本结点的高。递归到最深处就是叶子结点,返回1,然后层层 + 1,最终的到树的高
  • height()求树高的时间复杂度为O(n2)
  • degreeForTree()求树的度,求树中所有结点的度,然后求最大值,因为因为每个结点都有记录子结点结合的LinkedList, 求LinkedList.size()的时间复杂度为O(1) , 所以O(n) + O(1) =O(n)
  • listChildNode()求某结点的子结点,时间复杂度O(1), 没什么好说的,结点就自带有相关信息
  • getParentNode()求某结点的父结点, 时间复杂度是O(n) ,因为是孩子表示法,没有记录父结点的信息,所以需要遍历树的所有结点,只要有结点的孩子包括参数结点,那么该结点就是参数结点的父结点,时间复杂度是O(n)

双亲孩子表示法

从上面的双亲表示法和孩子表示法中,我们可以知道,如上面说的height()求树高,degreeForTree()求树的度,listChildNode()求某结点的子结点,getParentNode()求某结点的父结点这4个重点方法,两种表示方式互有优势和缺点,比如

双亲表示法:

  • height()效率比孩子表示法高,双亲是O(n), 孩子是O(n2), 为什么呢? 我们可以理解这棵树的遍历就是求该树所有结点的子树高是多少,所有就是求n个结点子树高,
  • getParentNode()效率比孩子表示法高,双亲是O(1),孩子是O(n)

孩子表示法:

  • degreeForTree()效率比双亲表示法高,双亲是O(n2),孩子是O(n)
  • listChildNode()效率比双亲表示法高,双亲是O(n),孩子是O(1)

所以我们就会想有没有可以办法可以综合他们之间的优势呢?其实也是有的,双亲孩子表示法就诞生了

在这里插入图片描述
那就是既记住树的每一个结点既记住父结点,也记住孩子结点,那就可以保证双亲和孩子表示法的优势被互相使用啦,仔细想想,是不是非常像线性表中双向链表的概念,哈哈哈。具体代码就不实现了,最后我们就可以得到它们合并之后的优势

  • height()效率是O(n),
  • getParentNode()效率是O(1)
  • degreeForTree()效率是O(n)
  • listChildNode()效率是O(1)

唯一的缺点就是双亲孩子表示法相对于双亲表示法或孩子表示法更占空间,既是其双亲和孩子要的空间的总和。属于空间换时间


底层数据结构 | 链表


双亲孩子表示法

从上面的实现,我们可以知道双亲孩子表示法可以使用数组结构实现,那么可以使用链表实现吗?当然可以啦

不过,双亲表示法或孩子表示法单独使用链表就没什么好处了,甚至双亲孩子表示法都使用链表也没有数组这么方便

  • 双亲表示法使用链表实现,会发现有些方法根本无法实现,比如求树的高,树的度,我们的结点只知道指向父结点的指针,这棵树除了根结点有成员变量指向,剩余的结点全部都无法通过唯一可知的根结点找到,所以双亲表示法根本使用链表实现
  • 孩子表示法可以虽然可以实现链表实现,毕竟我们知道根结点,也知道根结点的所有子结点,可以一层层遍历,但是所有的方法都必须通过根结点向叶子结点遍历,基本都是递归的实现,性能上不如数组的O(1)O(n)
  • 双亲孩子表示法,除了部分方法可以实现O(1)的性能,比如找某个结点的父结点,可以通过父结点指针马上找到,大部分主要方法还是需要递归实现,所以性能也不佳

所以我个人推荐双亲,孩子,双亲孩子表示法使用数组做为底层数据结构会更加的好,性能会更高

代码:

/**
 * 树的双亲孩子表示法的Java实现
 * 这里使用链表而不是数组作为底层结构
 *
 * 1. 是否空树?
 * 2. 查询树的结点数量
 * 3. 查询树的高
 * 4. 为某个结点插入子结点
 * 5. 设置根结点
 * 6. 查询返回根结点
 * 7. 查询某个结点的所有子结点
 * 8. 查询某个结点的父结点
 * 9. 查询某个结点的度
 * 10. 查询树的度
 * 11. 清空树的所有结点
 *
 *

 */
public class ParentRepresentationLinkedTree<T> {

    /**
     * 双亲表示法的结点数据结构
     * 双亲表示法意味着每个结点需要知道自己的双亲是谁,需要记录自己的双亲
     *
     * @param <E>
     */
    public static class TreeNode<E> {
        /**
         * 该结点的数据
         */
        private E data;
        /**
         * 该结点的父结点
         * 这里存在是父结点在数组中的索引,不是具体的结点
         */
        private TreeNode<E> parent;

        private List<TreeNode<E>> childrens;

        public TreeNode(E data, TreeNode<E> parent, List<TreeNode<E>> childrens) {
            this.data = data;
            this.parent = parent;
            this.childrens = childrens;
        }
    }


    /**
     * 树的结点数量
     */
    private int nodeCount;

    /**
     * 树的实际存储结构,由一个数组来做树的底层存储结构
     */
    private TreeNode<T> rootNode;

    /**
     * 树的构造函数
     *
     */
    public ParentRepresentationLinkedTree(T data) {
        this.rootNode = new TreeNode<>(data,null,new LinkedList<>());
        nodeCount++;
    }

    /**
     * 是否空树
     *
     * @return
     */
    public boolean isEmpty() {
        return nodeCount == 0;
    }

    /**
     * 查找树的结点数量
     *
     * @return
     */
    public int getNodeCount() {
        return nodeCount;
    }

    /**
     * 树高是多少
     * @return
     */
    public int height() {
        return nodeDepth(getRootNode());
    }

    /**
     * 本质上跟数组结构的孩子表示法,求深度是一样的
     * 因为链表也无法直接找到所有的树结点,也必须从头结点开始向下遍历
     * @param node
     * @return
     */
    private int nodeDepth(TreeNode<T> node) {

        if (node == null){
            return 0;
        }

        int max = 0;
        if (node.childrens.size() == 0){
            return 1;
        }

        for (TreeNode<T> childNode : node.childrens){
            int level = nodeDepth(childNode);
            max = max > level ? max : level;
        }

        return max + 1;

    }


    /**
     * 查询根结点
     *
     * @return
     */
    public TreeNode<T> getRootNode() {
        return this.rootNode;
    }

    /**
     * 为某个结点添加子结点
     *
     * @param data
     * @param parent
     */
    public TreeNode<T> addChild(T data, TreeNode<T> parent) {
        //子结点的父指针指向父结点
        TreeNode<T> node = new TreeNode<>(data, parent,new LinkedList<>());
        //父结点的子结点域,添加子结点指向
        parent.childrens.add(node);
        nodeCount++;
        return node;

    }



    /**
     * 查找该结点的父结点,并返回
     *
     * @param node
     * @return
     */
    public TreeNode<T> getParentNode(TreeNode<T> node) {
        return node.parent;

    }

    /**
     * 查找某个结点的所有子结点
     *
     * @param node
     * @return
     */
    public List<TreeNode<T>> listChildNode(TreeNode<T> node) {
        return node.childrens;
    }

    /**
     * 树的度
     *
     * @return
     */
    public int degreeForTree() {
        return degreeForNode(getRootNode());
    }


    /**
     * 某个结点的度
     *
     * @return
     */
    public int degreeForNode(TreeNode<T> node) {
        //空树的度为0
        if (node == null){
            return 0;
        }

        //默认最大值
        int max = node.childrens.size();

        //叶子结点的度也为0
        if (node.childrens.size() <= 0){
            return 0;
        }

        for (TreeNode<T> childNode : node.childrens){
            int degree = degreeForNode(childNode);
            max = max > degree ? max :degree;
        }
        return max;
    }

    /**
     * 清空树的所有数据
     */
    public void clear() {
        nodeCount = 0;
        clear(getRootNode());
        this.rootNode = null;
    }

    /**
     * 清空以某个结点为根结点的树的数据
     * @param node
     */
    private void clear(TreeNode<T> node) {

        //如果是空树,代表不用清除了,已经没有数据了
        if (node == null){
            return;
        }

        //如果该树只有一个结点,就清理这个结点就可以了,并退出递归
        if (node.childrens.size() == 0){
            node.parent = null;
            node.childrens.clear();
            node.data = null;
            node = null;
            return;
        }

        //其余情况,遍历子结点,一个一个结点清除
        for (TreeNode<T> childNode : node.childrens){
            clear(childNode);
        }

        //清理完子结点,再清理本结点
        node.parent = null;
        node.childrens.clear();
        node.data = null;
        node = null;



    }


    public static void main(String[] args) {
        ParentRepresentationLinkedTree<Integer> tree = new ParentRepresentationLinkedTree<>(20);
        TreeNode<Integer> node = tree.addChild(12,tree.getRootNode());
        TreeNode<Integer> node2 = tree.addChild(34,node);
        tree.addChild(45,node);
        tree.addChild(56,node2);
        System.out.println("height: " + tree.height());
        System.out.println("nodeCount: " + tree.getNodeCount());
        System.out.println("degree: " + tree.degreeForTree());
        System.out.println("root degree: " + tree.degreeForNode(tree.getRootNode()));
        tree.clear();
        System.out.println("after clear");
        System.out.println("height: " + tree.height());
        System.out.println("nodeCount: " + tree.getNodeCount());
        System.out.println("degree: " + tree.degreeForTree());
        System.out.println("root degree: " + tree.degreeForNode(tree.getRootNode()));
    }

}

  • 基本方法都是递归实现,都是要从根结点遍历到叶子结点的过程,性能较低

兄弟孩子表示法

兄弟孩子表示法,是一种比较特别的表示法,有一个数据域,两个指针域,使用链表来实现(兄弟孩子表示法不适合用数组来实现

  • data存放结点的数据
  • child指向当前结点第一个子结点
  • sibling指向自己的兄弟结点

从上图可以看出,兄弟孩子的表示法要查找某个结点的孩子结点,实际是通过child指针找到第一个孩子,然后通过第一个孩子的sibling指针依次找到其孩子的兄弟结点,然后就可以找到某个结点的所有孩子结点了; 实际是使用链表来管理某个结点的子节点关系的,所以兄弟孩子表示法的数据结构非常像是链表,这种链表又称为二叉链

垂直上看呢,就是所有结点长子的族谱图链表,哈哈哈,水平上看,就是一家子的兄弟姐妹链表,同时呢兄弟孩子表示法其实是很容易就切换成二叉树模式,因为它的结构实际上就是一个二叉树,所以在某些情况下可以很好的利用二叉树的特性去做一些事情,所以兄弟孩子表示法通常又被称为"二叉树表示法""二叉链表表示法"

代码:

package com.snailmann.datastructure.tree;

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

/**
 * 孩子兄弟表示法
 * 这种表示法只能使用链表来做底层数据结构,而不能使用数组
 */
public class ChildSiblingPepresentationTree<T> {

    public static class TreeNode<E> {

        /**
         * 数据域
         */
        private E data;

        /**
         * 指针域
         * child指向第一个子结点
         * sibling指向跟在后面的第一个兄弟结点
         */
        private TreeNode<E> child, sibling;

        public TreeNode(E data) {
            this.data = data;
            this.child = null;
            this.sibling = null;
        }

        public TreeNode(E data, TreeNode<E> child, TreeNode<E> sibling) {
            this.data = data;
            this.child = child;
            this.sibling = sibling;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            TreeNode<?> treeNode = (TreeNode<?>) o;

            if (data != null ? !data.equals(treeNode.data) : treeNode.data != null) return false;
            if (child != null ? !child.equals(treeNode.child) : treeNode.child != null) return false;
            return sibling != null ? sibling.equals(treeNode.sibling) : treeNode.sibling == null;
        }

        @Override
        public int hashCode() {
            int result = data != null ? data.hashCode() : 0;
            result = 31 * result + (child != null ? child.hashCode() : 0);
            result = 31 * result + (sibling != null ? sibling.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "TreeNode{" +
                    "data=" + data +
                    ", child=" + child +
                    ", sibling=" + sibling +
                    '}';
        }
    }


    /**
     * 树的结点数量
     */
    private int nodeCount;

    /**
     * 树的根结点
     */
    private TreeNode<T> rootNode;

    /**
     * 树的构造函数
     *
     * @param rootNode
     */
    public ChildSiblingPepresentationTree(TreeNode<T> rootNode) {
        this.rootNode = rootNode;
        this.nodeCount++;
    }

    /**
     * 是否空树
     *
     * @return
     */
    public boolean isEmpty() {
        return nodeCount == 0;
    }

    /**
     * 查找树的结点数量
     *
     * @return
     */
    public int getNodeCount() {
        return nodeCount;
    }

    /**
     * 求树的高
     *
     * @return
     */
    public int height() {
        return nodeDepth(rootNode);
    }


    /**
     * 本质上跟数组结构的孩子表示法,求深度是一样的
     * 因为链表也无法直接找到所有的树结点,也必须从头结点开始向下遍历
     *
     * @param node
     * @return
     */
    private int nodeDepth(TreeNode<T> node) {

        if (node == null) {
            return 0;
        }


        if (listChildNode(node).size() == 0) {
            return 1;
        }

        //说白了就是比较子树层级的最大值,的最大子树高
        int max = 0;
        for (TreeNode<T> childNode : listChildNode(node)) {
            int level = nodeDepth(childNode);
            max = max > level ? max : level;
        }
        //然后最大子树的高 + 1 ,就是本树的高,反馈给上一级别
        return max + 1;

    }

    /**
     * 查询根结点
     *
     * @return
     */
    public TreeNode<T> getRootNode() {
        return this.rootNode;
    }

    /**
     * 为某个结点添加子结点
     *
     * @param data
     * @param parent
     */
    public TreeNode<T> addChild(T data, TreeNode<T> parent) {
        TreeNode<T> node = new TreeNode<>(data);
        nodeCount++;

        //如果该结点还没有子结点,那么该结点的child就指向新结点
        if (parent.child == null) {
            parent.child = node;
            //如果该结点已经有子结点了,但是只有一个子结点,既其子结点没有兄弟结点,那么该结点的子结点的兄弟结点就指向新结点
        } else if (parent.child.sibling == null) {
            parent.child.sibling = node;
            //如果该结点的子结点已经有兄弟结点了,那么就要遍历该链表的尾节点,尾节点的sibling则是新结点,既新结点为新的尾节点
        } else {
            TreeNode<T> sibling = parent.child.sibling;
            //找到原链表的尾节点
            while (sibling.sibling != null) {
                sibling = sibling.sibling;
            }
            //新结点成为新的尾节点
            sibling.sibling = node;
        }

        return node;
    }


    /**
     * 查找该结点的父结点,并返回
     *
     * @param node
     * @return
     */
    public TreeNode<T> getParentNode(TreeNode<T> node) {
        //如果当前树的根结点为空,或参数结点就是根结点,那么其没有父结点
        if (rootNode == null || node == getRootNode()) {
            return null;
        }
        //查询递归查询某结点的子结点们是否含有参照结点
        return containsNode(rootNode, node);

    }

    /**
     * 递归查询以rootNode为根结点的子树,看其根结点的子结点们是否含有参照结点,如果有则返回该根结点为参数结点的父结点
     * 比较抽象
     *
     * @param rootNode
     * @param node
     * @return
     */
    private TreeNode<T> containsNode(TreeNode<T> rootNode, TreeNode<T> node) {

        //默认父结点为空
        TreeNode<T> parent = null;

        //如果该根结点是Null,则该父结点为Null
        if (rootNode == null) {
            return parent;
        }

        //如果根结点的子结点们有参照结点,则根结点为参数结点的父结点
        if (listChildNode(rootNode).contains(node)) {
            parent = rootNode;
            return parent;
            //如果没有,则递归子树 则看子树的根结点的孩子们是否有node
        }

        //遍历根结点的所有子结点,只要以子结点为根的树含有参数node,则返回其父结点给parent
        for (TreeNode<T> childNode : listChildNode(rootNode)) {
            if (containsNode(childNode, node) != null) {
                parent = containsNode(childNode, node);
            }
        }
        //返回父结点
        return parent;

    }

    /**
     * 查找某个结点的所有子结点
     *
     * @param node
     * @return
     */
    public List<TreeNode<T>> listChildNode(TreeNode<T> node) {
        List<TreeNode<T>> childrens = new ArrayList<>();
        //如果结点为空,空树没有子结点
        if (node == null) {
            return childrens;
        }
        //如果结点孩子指针域为空,则没有子结点
        if (node.child == null) {
            return childrens;
        }

        //node有子结点
        childrens.add(node.child);
        TreeNode<T> siblingNode = node.child.sibling;
        //如果兄弟链不为空,则代表还有node结点不止一个子结点
        if (siblingNode != null) {
            childrens.add(node.child.sibling);
            while (siblingNode.sibling != null) {
                childrens.add(siblingNode.sibling);
                siblingNode = siblingNode.sibling;
            }
        }
        return childrens;
    }

    /**
     * 树的度
     *
     * @return
     */
    public int degreeForTree() {
        return degreeForTree(rootNode);
    }

    /**
     * 求子树的度
     *
     * @param node
     * @return
     */
    private int degreeForTree(TreeNode<T> node) {
        //空树的度为0
        if (node == null) {
            return 0;
        }

        //默认最大值
        int max = listChildNode(node).size();

        //叶子结点的度也为0
        if (listChildNode(node).size() <= 0) {
            return 0;
        }

        for (TreeNode<T> childNode : listChildNode(node)) {
            int degree = degreeForTree(childNode);
            max = max > degree ? max : degree;
        }
        return max;
    }


    /**
     * 求某个结点的度
     *
     * @return
     */
    public int degreeForNode(TreeNode<T> node) {
        //求某个结点有多少个子结点,就代表该结点的度是多少
        return listChildNode(node).size();
    }

    /**
     * 清空树的所有数据
     */
    public void clear() {

    }

    public static void main(String[] args) {
        ChildSiblingPepresentationTree<Integer> tree = new ChildSiblingPepresentationTree<>(new TreeNode<>(20));
        TreeNode<Integer> node = tree.addChild(12, tree.getRootNode());
        TreeNode<Integer> node2 = tree.addChild(34, node);
        tree.addChild(45, node);
        TreeNode<Integer> node3 = tree.addChild(56, node2);
        System.out.println("height: " + tree.height());
        System.out.println("nodeCount: " + tree.getNodeCount());
        System.out.println("degree: " + tree.degreeForTree());
        System.out.println("root degree: " + tree.degreeForNode(tree.getRootNode()));
        System.out.println(tree.getParentNode(node3));
    }

}

其实写到这里,我的脑子已经昏昏沉沉了,不太够用了,哈哈哈,所以可能会存在一些错误啥的,如有发现,请见谅并指出

好处?

  • 好处嘛,其实我目前还没有感受到什么非常牛逼好处,跟链表形式的孩子双亲表示法相比,就是可以全部用链表去实现
  • 可以很好的切换成二叉树,使用二叉树的一些特性

兄弟孩子双亲表示法

其实嘛,兄弟孩子表示法也不是最终的情况,有时候我们就会想,是不是可以做个兄弟孩子双亲表示法
其实本质上也是可以的,就是多一个指针域的事情,这样求某个结点的父结点时,就不用想兄弟孩子表示法这么的麻烦了。

  • data 存放当前结点的数据
  • parent 指向当前结点的父结点
  • child 指向当前结点的长子结点,既第一个子结点
  • sibling 指向自己的第一个兄弟,既第一个弟弟结点
    在这里插入图片描述

代码就不是实现了,毕竟懒


参考资料


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章