【數據結構】初入數據結構的樹(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 指向自己的第一個兄弟,既第一個弟弟結點
    在這裏插入圖片描述

代碼就不是實現了,畢竟懶


參考資料


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