树(基础部分)

章节目录:

    • 一、二叉树
      • 1.1 为什么要使用树?
      • 1.2 树的常用术语
      • 1.3 二叉树概念
      • 1.4 二叉树应用
    • 二、顺序存储二叉树
      • 2.1 概述
      • 2.2 基本应用
    • 三、线索化二叉树
      • 3.1 问题引出
      • 3.2 概述
      • 3.3 基本应用
    • 四、结束语

一、二叉树

1.1 为什么要使用树?

  • 数组存储方式:

    • 优点:通过下标方式访问元素,速度快,并且对于有序数组,还可使用二分查找提高检索速度。
    • 缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动效率较低
  • 链式存储方式:

    • 优点:在一定程度上对数组存储方式有优化,比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很好
    • 缺点:在进行检索时,效率仍然较低,比如:检索某个值,需要从头节点开始遍历
  • 存储方式:

    • 能提高数据存储,读取的效率,比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度

    • 示意图

树(基础部分)_第1张图片

  • 总结:不难看出,上面通过二叉排序树来存储数据,效率相较于数组及链式都有了一定程度的提升。

1.2 树的常用术语

树(基础部分)_第2张图片

  • 节点:树中的一个连接点

  • 根节点仅有一个):非空树中无前驱节点的节点,称之为根节点。

  • 父节点:若一个节点含有子节点,则这个节点成为其子节点的父节点。

  • 子节点:一个节点含有的子树的根节点成为该节点的子节点。

  • 叶子节点 (没有子节点的节点):一棵树中没有子节点的节点成为叶子节点。

  • 节点的权:节点的具体值

  • 路径(从 root 节点找到该节点的路线):从根节点到某一个具体节点所走过的路。

  • :根节点在1层,其它任一节点的层数是其父节点的层数加1

  • 子树:只要包含了一个节点,就得包含这个节点下的所有节点。

  • 树的高度(最大层数):树内所有节点高度的最大值,也就是根节点的高度,也就是树的层数。

  • 森林 (多颗子树构成森林):森林是由若干棵树组成,可以将森林中的每棵树的根节点看作是兄弟。

1.3 二叉树概念

树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树

树(基础部分)_第3张图片

  • 二叉树的子节点分为节点和节点。

树(基础部分)_第4张图片

  • 如果该二叉树的所有叶子节点都在最后一层,并且节点总数= 2^n -1 , n 为层数,则我们称为满二叉树

树(基础部分)_第5张图片

  • 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树

1.4 二叉树应用

需求:使用前序,中序和后序对二叉树进行遍历查找,并编写删除节点方法。

  • 前序遍历: 先输出父节点,再遍历左子树和右子树。(父节点 -> 左子树 -> 右子树)

  • 中序遍历: 先遍历左子树,再输出父节点,再遍历右子树。(左子树 -> 父节点 -> 右子树)

  • 后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点。(左子树 -> 右子树 -> 父节点

  • 小结: 看输出父节点的顺序,就确定是前序,中序还是后序。

  • 代码示例

public class BinaryTreeDemo {

    public static void main(String[] args) {
        // 1.创建一棵二叉树。
        BinaryTree binaryTree = new BinaryTree();

        // 2.创建节点。
        Node root = new Node(1, "data1");
        Node node2 = new Node(2, "data2");
        Node node3 = new Node(3, "data3");
        Node node4 = new Node(4, "data4");
        Node node5 = new Node(5, "data5");

        // 3.手动给二叉树赋值。
        root.setLeft(node2);
        root.setRight(node3);
        node3.setRight(node4);
        node3.setLeft(node5);
        binaryTree.setRoot(root);

        // 4.测试[遍历]。
        System.out.println("前序遍历结果如下:");
        binaryTree.preOrder();
        // Node:id=1,2,3,5,4

        System.out.println();
        System.out.println("中序遍历结果如下:");
        binaryTree.infixOrder();
        // Node:id=2,1,5,3,4

        System.out.println();
        System.out.println("后序遍历结果如下:");
        binaryTree.postOrder();
        // Node:id=2,5,4,3,1

        // 5.测试[查找]。
        // 前序遍历查找的次数 :4
        System.out.println();
        System.out.println("前序遍历查找结果如下:");
        int id = 5;
        Node res = binaryTree.preOrderSearch(id);
        if (null != res) {
            System.out.printf(
                    "preOrderSearch result: id=[%d], data=[%s]\n",
                    res.getId(),
                    res.getData()
            );
        } else {
            System.out.printf("preOrderSearch not found id=[%d] node.\n", id);
        }

        // 中序遍历查找的次数 :3
        System.out.println();
        System.out.println("中序遍历查找结果如下:");
        Node res1 = binaryTree.infixOrderSearch(id);
        if (null != res1) {
            System.out.printf(
                    "infixOrderSearch result: id=[%d], data=[%s]\n",
                    res1.getId(),
                    res1.getData()
            );
        } else {
            System.out.printf("infixOrderSearch not found id=[%d] node.\n", id);
        }

        // 后序遍历查找的次数 :2
        System.out.println();
        System.out.println("后序遍历查找结果如下:");
        Node res2 = binaryTree.postOrderSearch(id);
        if (null != res2) {
            System.out.printf(
                    "postOrderSearch result: id=[%d], data=[%s]\n",
                    res2.getId(),
                    res2.getData()
            );
        } else {
            System.out.printf("postOrderSearch not found id=[%d] node.\n", id);
        }

        // 6.测试[删除]。
        System.out.println();
        System.out.println("删除前,前序遍历结果如下:");
        binaryTree.preOrder();
        // Node:id=1,2,3,5,4

        // 删除id为5的节点。
        binaryTree.del(id);

        System.out.println("删除后,前序遍历结果如下:");
        binaryTree.preOrder();
        // Node:id=1,2,3,4
    }
}


/**
 * 定义二叉树。
 */
class BinaryTree {

    /**
     * 根节点。
     */
    private Node root;

    public void setRoot(Node root) {
        this.root = root;
    }

    public void preOrder() {
        if (null != this.root) {
            this.root.preOrder();
        } else {
            System.out.println("preOrder error : binary tree is null.");
        }
    }

    public void infixOrder() {
        if (null != this.root) {
            this.root.infixOrder();
        } else {
            System.out.println("infixOrder error : binary tree is null.");
        }
    }

    public void postOrder() {
        if (null != this.root) {
            this.root.postOrder();
        } else {
            System.out.println("postOrder error : binary tree is null.");
        }
    }

    public Node preOrderSearch(int id) {
        if (null != this.root) {
            return root.preOrderSearch(id);
        } else {
            return null;
        }
    }

    public Node infixOrderSearch(int id) {
        if (null != this.root) {
            return root.infixOrderSearch(id);
        } else {
            return null;
        }
    }

    public Node postOrderSearch(int id) {
        if (null != this.root) {
            return this.root.postOrderSearch(id);
        } else {
            return null;
        }
    }

    public void del(int id) {
        if (null != this.root) {
            // 若只有一个根节点时,直接进行判断。
            if (id == this.root.getId()) {
                root = null;
            } else {
                this.root.del(id);
            }
        } else {
            System.out.println("del node error : binary tree is null.");
        }
    }
}


/**
 * 定义节点。
 */
@Setter
@Getter
class Node {
    private int id;
    private Object data;
    private Node left;
    private Node right;

    public Node(int id, Object data) {
        this.id = id;
        this.data = data;
    }

    @Override
    public String toString() {
        return "Node:[id=" + this.id + ", data=" + this.data + "]";
    }

    /**
     * 1.前序遍历。
     */
    public void preOrder() {
        // 输出父节点。
        System.out.println(this);
        if (null != this.left) {
            this.left.preOrder();
        }
        if (null != this.right) {
            this.right.preOrder();
        }
    }

    /**
     * 2.中序遍历。
     */
    public void infixOrder() {
        if (null != this.left) {
            this.left.infixOrder();
        }
        // 输出父节点。
        System.out.println(this);
        if (null != this.right) {
            this.right.infixOrder();
        }
    }

    /**
     * 3.后序遍历。
     */
    public void postOrder() {
        if (null != this.left) {
            this.left.postOrder();
        }
        if (null != this.right) {
            this.right.postOrder();
        }
        // 输出父节点。
        System.out.println(this);
    }

    // -----------分割线-----------

    /**
     * 1.前序查找。
     *
     * @param id id
     * @return {@link Node}
     */
    public Node preOrderSearch(int id) {
        if (id == this.getId()) {
            return this;
        }
        Node res = null;
        // 向左递归。
        if (null != this.left) {
            res = this.left.preOrderSearch(id);
        }

        // 左子树找到。
        if (null != res) {
            return res;
        }

        // 向右递归。
        if (null != this.right) {
            res = this.right.preOrderSearch(id);
        }
        return res;
    }

    /**
     * 2.中序查找。
     *
     * @param id id
     * @return {@link Node}
     */
    public Node infixOrderSearch(int id) {
        // 左递归。
        Node res = null;
        if (null != this.left) {
            res = this.left.infixOrderSearch(id);
        }
        if (null != res) {
            return res;
        }
        // 找到则返回。
        if (id == this.getId()) {
            return this;
        }
        // 右递归。
        if (null != this.right) {
            res = this.right.infixOrderSearch(id);
        }
        return res;
    }

    /**
     * 3.后序查找。
     *
     * @param id id
     * @return {@link Node}
     */
    public Node postOrderSearch(int id) {
        Node res = null;
        if (null != this.left) {
            res = this.left.postOrderSearch(id);
        }
        if (null != res) {
            return res;
        }

        if (null != this.right) {
            res = this.right.postOrderSearch(id);
        }
        if (null != res) {
            return res;
        }

        if (id == this.getId()) {
            return this;
        }
        return res;
    }

    // -----------分割线-----------

    /**
     * 删除节点。
     *
     * 

* 思路分析如下: * 1. 因为我们的二叉树是[单向]的,所以我们是判断当前节点的子节点是否需要删除节点,而不能去判断当前这个节点是不是需要删除节。 * 2. 如果当前节点的左子节点不为空,并且左子节点就是要删除节点,就将this.left = null; 并且就返回(结束递归删除)。 * 3. 如果当前节点的右子节点不为空,并且右子节点就是要删除节点,就将this.right= null;并且就返回(结束递归删除)。 * 4. 如果第2和第3步没有删除节点,那么我们就需要向左子树进行递归删除。 * 5. 如果第4步也没有删除节点,则应当向右子树进行递归删除。 *

* * @param id id */ public void del(int id) { if (null != this.left && id == this.left.getId()) { this.left = null; return; } if (null != this.right && id == this.right.getId()) { this.right = null; return; } // 向左,向右进行递归删除。 if (null != this.left) { this.left.del(id); } if (null != this.right) { this.right.del(id); } } }

二、顺序存储二叉树

2.1 概述

  • 说明:从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组

  • 示意图

树(基础部分)_第6张图片

  • 特点(n : 表示二叉树中的第几个元素):
    • 顺序二叉树通常只考虑完全二叉树
    • 第 n 个元素的子节点为 2 * n + 1
    • 第 n 个元素的子节点为 2 * n + 2
    • 第 n 个元素的节点为 (n-1) / 2

2.2 基本应用

需求: 给定一个数组 {1,2,3,4,5,6,7},要求以二叉树前序遍历的方式进行遍历。 前序遍历的结果应当为1,2,4,5,3,6,7。

public class ArrBinaryTreeDemo {

    public static void main(String[] args) {
        int[] array = {1, 2, 3, 4, 5, 6, 7};
        ArrBinaryTree arrBinaryTree = new ArrBinaryTree(array);
        arrBinaryTree.preOrder();
        // 1,2,4,5,3,6,7
    }

}

class ArrBinaryTree {

    /**
     * 存储数据节点的数组。
     */
    private final int[] array;

    public ArrBinaryTree(int[] array) {
        this.array = array;
    }

    /**
     * 重载 preOrder()。
     */
    public void preOrder() {
        // 固定从下标 0 开始。
        this.preOrder(0);
    }

    /**
     * 前序遍历。
     *
     * @param index 数组的下标
     */
    public void preOrder(int index) {

        if (array == null || array.length == 0) {
            System.out.println("preOrder error : binary tree is null.");
        }

        if (null != array) {
            // 输出当前这个元素。
            System.out.println(array[index]);

            // 向左递归遍历。
            if ((index * 2 + 1) < array.length) {
                preOrder(2 * index + 1);
            }
            // 向右递归遍历。
            if ((index * 2 + 2) < array.length) {
                preOrder(2 * index + 2);
            }
        }
    }
}

三、线索化二叉树

3.1 问题引出

  • 示意图

树(基础部分)_第7张图片

  • 当我们对上面的二叉树进行中序遍历时,数列为 {8, 3, 10, 1, 6, 14 }。
  • 但是 6, 8, 10, 14 这几个节点的左右指针,并没有完全的利用上
  • 如果我们希望充分的利用 各个节点的左右指针, 让各个节点可以指向自己的前后节点,怎么办?
  • 解决方案线索二叉树

3.2 概述

  • n 个节点的二叉链表中含有 n+1 (参考公式: 2n-(n-1)=n+1) 个空指针域。利用二叉链表中的空指针域,存放指向该节点在某种遍历次序下的前驱和后继节点的指针(这种附加的指针称为"线索")。
  • 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree),根据线索性质的不同,线索二叉树可分为序线索二叉树、序线索二叉树和序线索二叉树三种
  • 一个节点的前一个节点,称为前驱节点。
  • 一个节点的后一个节点,称为后继节点。

3.3 基本应用

需求:将下面的二叉树,进行中序线索二叉树。中序遍历的数列结果为 {8, 3, 10, 1, 14, 6}。

  • 示意图

树(基础部分)_第8张图片

  • left 指向的是左子树,也可能是指向的前驱节点。(比如:节点①-left 指向的左子树, 而 节点⑩-left 指向的就是前驱节点。)

  • right 指向的是右子树,也可能是指向的后继节点。(比如:节点①-right 指向的是右子树,而节点⑩-right 指向的是后继节点。)

  • 代码示例

public class ThreadedBinaryTreeDemo {
    
    public static void main(String[] args) {

        // 准备节点。
        Node1 root = new Node1(1, "data1");
        Node1 node3 = new Node1(3, "data3");
        Node1 node6 = new Node1(6, "data6");
        Node1 node8 = new Node1(8, "data8");
        Node1 node10 = new Node1(10, "data10");
        Node1 node14 = new Node1(14, "data14");

        // 手动创建二叉树。
        root.setLeft(node3);
        root.setRight(node6);
        node3.setLeft(node8);
        node3.setRight(node10);
        node6.setLeft(node14);

        // 中序线索化。
        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
        threadedBinaryTree.setRoot(root);
        threadedBinaryTree.threadNodes();

        // 以节点⑩来进行测试。
        int leftId = node10.getLeft().getId();
        int rightId = node10.getRight().getId();
        System.out.println("节点⑩的前驱节点id=[" + leftId + "], 后继节点id=[" + rightId + "]");
        // 节点⑩的前驱节点id=[3], 后继节点id=[1]

        System.out.println();
        System.out.println("使用线索化二叉树遍历结果如下:");
        threadedBinaryTree.foreach();
        // 使用线索化二叉树遍历结果如下:
        // Node1:[id=8, data=data8]
        // Node1:[id=3, data=data3]
        // Node1:[id=10, data=data10]
        // Node1:[id=1, data=data1]
        // Node1:[id=14, data=data14]
        // Node1:[id=6, data=data6]
    }
}


/**
 * 定义线索二叉树。
 */
class ThreadedBinaryTree {

    /**
     * 根节点。
     */
    private Node1 root;

    /**
     * 为了实现线索化,需要创建一个指向当前节点的前驱节点指针。
     * 递归线索化时,pre总是保留前一个节点。
     */
    private Node1 pre = null;

    public void setRoot(Node1 root) {
        this.root = root;
    }


    /**
     * 中序遍历线索化二叉树。
     */
    public void foreach() {
        Node1 cur = root;
        while (null != cur) {
            // 找到线索化后的节点。(第一个找到的是节点⑧)
            // 0:表示指向的[左子树]。
            while (0 == cur.getLeftType()) {
                // 左移。
                cur = cur.getLeft();
            }

            // 输出当前节点。
            System.out.println(cur);

            // 如果当前节点的右指针指向的是后继节点,就一直输出。
            while (1 == cur.getRightType()) {
                // 获取后继节点。
                cur = cur.getRight();
                System.out.println(cur);
            }
            // 替换遍历节点。
            cur = cur.getRight();
        }
    }

    /**
     * 重载线索化方法。(便于调用)。
     */
    public void threadNodes() {
        this.threadNodes(root);
    }

    /**
     * 将树中的节点进行线索化。
     *
     * @param node 节点
     */
    private void threadNodes(Node1 node) {
        if (null == node) {
            return;
        }

        // 1.先线索化[左子树]。
        threadNodes(node.getLeft());

        // 2.线索化[当前节点]。
        // 2.1 先处理当前节点的[前驱节点] --- (此处以节点⑧为例,他的left=null,则leftType=1)。
        if (null == node.getLeft()) {
            // 左指针指向前驱节点,并修改左指针类型,指向前驱节点。
            node.setLeft(pre);
            node.setLeftType(1);
        }

        // 2.2 再处理[后继节点]。
        if (null != pre && null == pre.getRight()) {
            pre.setRight(node);
            pre.setRightType(1);
        }
        // 处理完每一个节点后,让当前节点成为下一个节点的前驱节点。
        pre = node;

        // 3.线索化[右子树]。
        threadNodes(node.getRight());
    }
}


/**
 * 定义节点。
 */
@Setter
@Getter
class Node1 {
    private int id;
    private Object data;
    private Node1 left;
    private Node1 right;

    /**
     * 0 表示指向的[左子树]。
     * 1 表示指向[前驱节点]。
     */
    private int leftType;

    /**
     * 0 表示指向的[右子树]。
     * 1 表示指向[后继节点]。
     */
    private int rightType;

    public Node1(int id, Object data) {
        this.id = id;
        this.data = data;
    }

    @Override
    public String toString() {
        return "Node1:[id=" + this.id + ", data=" + this.data + "]";
    }
}

四、结束语


“-------怕什么真理无穷,进一寸有一寸的欢喜。”

微信公众号搜索:饺子泡牛奶

你可能感兴趣的:(数据结构与算法,java,数据结构,算法)