Java 树 笔记+作业

1.逻辑结构定义

树 ---> 二叉树:左孩子,右兄弟原则:

  • 每个节点的左指针指向她的第一个孩子,右指针指向她在树中相邻的右兄弟。

2.存储结构实现

二叉树存储结构:

  1. 数组存储,浪费空间
    • // 前提
      如果我们对一个完全二叉树按照层级进行编号(根节点编号1) , 这个完全二叉树上的所有结点, 都满足一个特点
          父结点编号 * 2 = 该结点的left结点编号
          父结点编号 * 2 + 1= 该结点的right结点编号
      这样即使给定我们任意一个x位置, 我们都能很快找到它的父结点位置(x/2)以及子结点位置(2x, 2x+1)
  2. 链式存储
    • private int value;
      private Node leftChild;
      private Node rightChild;
      


3.二叉树的操作

二叉树的遍历:

  1. 广度遍历(层序遍历):用队列实现
    1. 广度优先遍历:广度优先遍历是连通图的一种遍历策略,因为它的思想是从一个顶点V0开始,辐射状地优先遍历其周围较广的区域故得名。

      根据广度优先遍历的特点我们利用Java数据结构队列Queue来实现。

    2. 出队即访问,访问则让所有孩子入队

      广度优先搜索的步骤为:

      (1)、节点1进队,节点1出队,访问节点1

      (2)、节点1的孩子节点2进队,节点3进队。

      (3)、节点2出队,访问节点2,节点2的孩子节点4进队,节点5进队;

      (4)、节点3出队,访问节点3,节点3的孩子节点6进队,节点7进队;

      (5)、节点4出队,访问节点4,节点4没有孩子节点。

      (6)、节点5出队,访问节点5,节点5没有孩子节点。

      (7)、节点6出队,访问节点6,节点6没有孩子节点。

      (8)、节点7出队,访问节点7,节点7没有孩子节点,结束。

    3. 树一开始是为了存储大量数据时,高度增加为对数,但是要对一棵二叉树层序遍历,不如一开始就用线性表存储。
       

  2. 深度优先遍历:用栈实现
    1. 深度优先遍历是图论中的经典算法。其利用了深度优先搜索算法可以产生目标图的相应拓扑排序表,采用拓扑排序表可以解决很多相关的图论问题,如最大路径问题等等。

      根据深度优先遍历的特点我们利用Java集合类的栈Stack先进后出的特点来实现。我用二叉树来进行深度优先搜索。

    2. 出栈即访问,访问则让所有孩子入栈

      深度优先搜索的步骤为:

      (1)、首先节点 1 进栈,节点1在栈顶;

      (2)、然后节点1出栈,访问节点1,节点1的孩子节点3进栈,节点2进栈;

      (3)、节点2在栈顶,然后节点2出栈,访问节点2

      (4)、节点2的孩子节点5进栈,节点4进栈

      (5)、节点4在栈顶,节点4出栈,访问节点4,

      (6)、节点4左右孩子为空,然后节点5在栈顶,节点5出栈,访问节点5

      (7)、节点5左右孩子为空,然后节点3在站顶,节点3出栈,访问节点3;

      (8)、节点3的孩子节点7进栈,节点6进栈

      (9)、节点6在栈顶,节点6出栈,访问节点6;

      (10)、节点6的孩子为空,这个时候节点7在栈顶,节点7出栈,访问节点7

      (11)、节点7的左右孩子为空,此时栈为空,遍历结束。
       

        

KEY:获得BST的先序遍历的核心逻辑
         用来实现先序遍历的栈stack
         用链表list保存先序遍历结果
         根结点入栈,但不算访问
         当栈不空,出栈一个节点,把他的 右 左 孩子(如果不为null)入栈,循环遍历
         出栈并访问一个结点,加到list后面
         栈空,退出循环,返回list。

 KEY:中序序列,
         用栈stack实现,两层递归
         从根结点到 root的左子树最左下结点路径依次入栈
         弹栈并访问(加入list)
         让当前结点的右子树成为下一轮循环的node。   
 

KEY:获得BST的后序遍历的核心逻辑
         用来实现后序遍历的栈stack
         用链表list保存后序遍历结果
         根结点入栈,但不算访问
         当栈不空,出栈一个节点,把他的 左 右 孩子(如果不为null)入栈,循环遍历
         出栈并访问,的结点,加到list前面(头插法)
         栈空,退出循环,返回list。

          // 递归出口
        if (inOrder.size() == 0) {
            return null;
        }

        // 先序序列的第一个结点,就是根结点
        T value = preOrder.get(0);
        // 找到根节点在中序序列的位置
        int index = inOrder.indexOf(value);
        // 构建根节点
        Node root = new Node(value);


        // 在先序序列中,切割出左右子树
        //       left子树的先序:  [1  ~  index + 1)
        //       right子树的先序: [index + 1  ~  preOrder.size())
        List<T> preOrderLeft = preOrder.subList(1, index + 1);
        List<T> preOrderRight = preOrder.subList(index + 1, preOrder.size());

        // 在中序序列中,切割出对应的左右子树的,先序序列,用来递归
        //       left子树的中序:  [0  ~  index)
        //       right子树的中序: [index + 1  ~ inOrder.size())
        List<T> inOrderLeft = inOrder.subList(0, index);
        List<T> inOrderRight = inOrder.subList(index + 1, inOrder.size());

        // 根据left子树的中序和先序, 递归构建left子树, 返回left子树的根结点
        root.left = buildTreeByInAndPreOrderHelper(inOrderLeft, preOrderLeft);
        // 根据right子树的中序和先序, 递归构建right子树, 返回right子树的根结点
        root.right = buildTreeByInAndPreOrderHelper(inOrderRight, preOrderRight);

        return root;

package com._28_datastructure._04_tree.bstree;

import com._28_datastructure._01_linkedlist._01mylinkedlist.MyLinkedList;
import com._28_datastructure._02_stack.MyArrayStack;
import com._28_datastructure._03_queue.MyLinkedQueue;

import java.io.OutputStream;
import java.util.List;

/**
 * 手写实现二叉搜索树(不一定平衡)
 * 操作:add
 * remove
 * contains
 * <p>
 * 限定:T 是 comparable 的子类
 * 区别:不能存null,因为不能比大小
 *
 * @author zxcsjf
 * @since 2022/07/09 10:58
 */
public class MyBSTree<T extends Comparable> {

    private Node root;
    private int size;

    // add
    // remove
    // contains

    /**
     * BST 的添加方法
     *
     * @param value 要添加的内容
     * @return 成功,返回true
     * @author zxcsjf
     * @since 2022/07/09 11:05:08
     */
    public boolean add(T value) {
        // 不允许存null
        if (value == null) {
            throw new IllegalArgumentException("BST不能存null");
        }

        // 树为空,直接存为root
        if (isEmpty()) {
            root = new Node(value);
            size++;
            return true;
        }

        // 树不为空,从根结点遍历,找到要添加的位置
        Node mid = root;  // 定义一个遍历结点
        Node midF = null; // 保存mid的父节点
        int com = 0;    // 保存比较结果

        // 循环遍历
        while (mid != null) {
            com = mid.value.compareTo(value);

            // 保存mid的父节点
            midF = mid;

            // 如果com > 0, 说明mid.value > value
            if (com > 0) {
                // 往左子树查找
                mid = mid.left;
            } else if (com < 0) {
                // 往右子树查找
                mid = mid.right;
            } else {
                // mid 存储内容相同,不能存
                return false;
                // 3种解决办法:
                //     1.拉链法:value存链表;
                //     2.Node里面再加一个计数器
                //     3.修正的BST:让重复的10,放在原来的10的左子树的最右下位置
            }

        }
        if (com > 0) {
            midF.left = new Node(value);
        } else {
            midF.right = new Node(value);
        }

        size++;
        return true;

    }

    /**
     * 删除分为三种情况:
     * 1.删除叶子:直接删
     * 2.删除单分支:让非空子树代替他
     * 3.删除双分支:先替换,再删除
     * 3.a.替换:左子树的最大值 或者 柚子树的最小值,拿来替换
     * 3.b.删除:左子树的最大值 或者 右子树的最小值
     *
     * @param value 要删除的结点值
     * @return
     * @author zxcsjf
     * @since 2022/07/09 11:33:21
     */
    public boolean remove(T value) {
        if (value == null) {
            throw new IllegalArgumentException("不能删除null!");
        }

        if (isEmpty()) {
            throw new RuntimeException("树为空!不能删除!");
        }

        // TODO:总体思路:
        //  先查找,再删除

        // 定义一个mid指向要删除的节点
        Node mid = root;
        // midF指向要删除的节点的父节点
        Node midF = null;

        // 1.循环查找到要 删除的节点mid 和 父节点midF
        while (mid != null) {
            int com = mid.value.compareTo(value);
            // 当前结点的value比要找的大,
            if (com > 0) {
                midF = mid;// 保存父节点
                mid = mid.left; // 就去左子树中找
            } else if (com < 0) {
                // 当前结点的value比要找的小,
                midF = mid;
                mid = midF.right;
            } else {
                // com = 0, 找到要删除的节点mid
                // 这里不处理,留到后面统一处理,
                break;
            }
        }

        // 2.跳出循环时,有两种可能
        // mid == null,没找到
        // mid是要删除的结点,midF是它的父节点(midF也有可能为null)
        if (mid == null) {
            return false;
        }

        // 3.如果mid为双分支节点
        if (mid.left != null && mid.right != null) {
            // 先查找,再替换
            // 让mid的左子树的最右下结点替换到mid

            Node max = mid.left; // max 就是mid左子树的最大值
            Node maxF = mid; // maxF 保存用于替换的节点 的父节点

            while (max.right != null) {
                maxF = max;
                max = max.right;
            }

            // 退出循环说明找到max结点,替换
            mid.value = max.value;

            // 再删除这个儿子的原地位,转化为处理单分支情况
            mid = max;
            midF = maxF;
        }

        // 4.统一处理叶子、单分支的情况

        // ch是mid的 非空子树或者 null
        Node ch = mid.left != null ? mid.left : mid.right;

        // 如果没有跑双分支的情况,且要删的的就是单分支根结点
        if (midF == null) {
            root = ch;
            size--;  // 别忘了size--  !!!!!
            return true;
        }

        // 子树上移
        // 如果要删除的结点是midF的右孩子
        if (mid == midF.right) {
            midF.right = ch;
        } else {
            // 如果要删除的结点是midF的左孩子
            midF.left = ch;
        }

        size--;
        return true;

    }

    /**
     * BST 查找方法
     *
     * @param null
     * @return
     * @author zxcsjf
     * @since 2022/07/11 14:32:25
     */
    public boolean contains(T value) {
        // TODO
        return false;
    }

    // --------------------------------------------------------------------
    // ------------------为了练习代码---------------------------------------
    // 树遍历: 深度遍历, 广度遍历
    // 还原一棵树

    /**
     * BST的层序遍历levelOrder
     *
     * @param
     * @return void
     * @author zxcsjf
     * @since 2022/07/11 9:56:06
     */
    public MyLinkedList leOrder() {

        // 创建一个队列
        MyLinkedQueue<Node> queue = new MyLinkedQueue<>();

        // 创建一个存储遍历结果的线性表
        MyLinkedList<T> list = new MyLinkedList<>();

        //1, 把根结点入队列
        queue.offer(root);

        // 2, 循环: 队列不空
        while (!queue.isEmpty()) {
            // 出队列一个结点
            Node poll = queue.poll();

            // 访问这个结点
            list.add(poll.value);

            // 把出队列结点的左右孩子入队列
            if (poll.left != null) {
                queue.offer(poll.left);
            }
            if (poll.right != null) {
                queue.offer(poll.right);
            }
        }
        return list;
    }


    /**
     * 获得BST的先序遍历序列
     *
     * @param
     * @return MyLinkedList
     * @author zxcsjf
     * @since 2022/07/11 10:11:07
     */
    public MyLinkedList preOrder() {
        // KEY:获得BST的先序遍历的核心逻辑
        //  用来实现先序遍历的栈stack
        //  用链表list保存先序遍历结果
        //  根结点入栈,但不算访问
        //  当栈不空,出栈一个节点,把他的 右 左 孩子(如果不为null)入栈,循环遍历
        //  出栈并访问一个结点,加到list后面
        //  栈空,退出循环,返回list。
        // 用来实现先序遍历的栈
        MyArrayStack<Node> stack = new MyArrayStack<>();

        // 保存先序遍历结果的链表
        MyLinkedList<T> list = new MyLinkedList<>();
        // 根结点入栈,但不算访问
        stack.push(root);

        // 当栈不空,出栈一个节点,把他的 右 左 孩子入栈,循环遍历
        while (!stack.isEmpty()) {
            // 出栈并访问,的结点,加到list后面
            Node node = stack.pop();
            list.add(node.value);

            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left != null) {
                stack.push(node.left);
            }
        }

        // 栈空,退出循环,返回list
        return list;
    }


    /**
     * BST的中序遍历
     *
     * @param Node
     * @return
     * @author zxcsjf
     * @since 2022/07/11 10:11:07
     */
    public MyLinkedList inOrder() {
        // KEY:中序序列,
        //  用栈stack实现,两层递归
        //  从根结点到 root的左子树最左下结点路径依次入栈
        //  弹栈并访问(加入list)
        //  让当前结点的右子树成为下一轮循环的node。
        // 用来实现中序遍历的栈
        MyArrayStack<Node> stack = new MyArrayStack<>();

        // 保存中序遍历结果的链表
        MyLinkedList<T> list = new MyLinkedList<>();
        // 标记结点
        Node node = root;
        // stack.push(node);  这里不能先让root压栈,第一个压栈的地方是在小循环里面

        // 当栈不空,或者node不为null,循环遍历
        while (!stack.isEmpty() || node != null) {
            while (node != null) {
                stack.push(node);
                node = node.left;
            }

            node = stack.pop();
            list.add(node.value);

            node = node.right;
        }

        // 栈空,退出循环
        return list;

    }


    /**
     * BST的后序遍历
     *
     * @param
     * @return
     * @author zxcsjf
     * @since 2022/07/11 10:11:07
     */
    public MyLinkedList postOrder() {
        // KEY:获得BST的后序遍历的核心逻辑
        //  用来实现后序遍历的栈stack
        //  用链表list保存后序遍历结果
        //  根结点入栈,但不算访问
        //  当栈不空,出栈一个节点,把他的 左 右 孩子(如果不为null)入栈,循环遍历
        //  出栈并访问,的结点,加到list前面(头插法)
        //  栈空,退出循环,返回list。
        // 用来实现后序遍历的栈
        MyArrayStack<Node> stack = new MyArrayStack<>();

        // 保存后序遍历结果的链表
        MyLinkedList<T> list = new MyLinkedList<>();
        // 根结点先入栈,但不算访问
        stack.push(root);

        // 当栈不空,出栈一个节点,把他的左右孩子入栈,循环遍历
        while (!stack.isEmpty()) {
            // 出栈的结点,视为被访问,头插到root前面
            Node node = stack.pop();
            list.add(0, node.value);

            if (node.left != null) {
                stack.push(node.left);
            }
            if (node.right != null) {
                stack.push(node.right);
            }
        }

        // 栈空,退出循环
        return list;
    }

    /**
     * 递归得到先序序列
     *
     * @param null
     * @return
     * @author zxcsjf
     * @since 2022/07/11 19:58:58
     */
    public MyLinkedList preOrderRecursive() {
        MyLinkedList<T> list = new MyLinkedList<>();

        preOrderRecursiveHelper(list, root);

        return list;
    }

    // 递归遍历某个结点
    private void preOrderRecursiveHelper(MyLinkedList<T> list, Node node) {
        if (node == null) {
            return;
        }

        list.add(node.value);

        // 递归遍历左子树
        preOrderRecursiveHelper(list, node.left);

        // 递归遍历右子树
        preOrderRecursiveHelper(list, node.right);
    }

    /**
     * 递归得到后序序列
     *
     * @param null
     * @return list
     * @author zxcsjf
     * @since 2022/07/11 19:58:33
     */
    public MyLinkedList postOrderRecursive() {
        MyLinkedList<T> list = new MyLinkedList<>();

        postOrderRecursiveHelper(list, root);

        return list;
    }

    // 递归遍历某个结点
    private void postOrderRecursiveHelper(MyLinkedList<T> list, Node node) {
        if (node == null) {
            return;
        }

        // 递归遍历左子树
        postOrderRecursiveHelper(list, node.left);

        // 递归遍历右子树
        postOrderRecursiveHelper(list, node.right);

        list.add(node.value);
    }

    /**
     * 递归得到中序序列
     *
     * @param null
     * @return
     * @author zxcsjf
     * @since 2022/07/11 19:58:38
     */
    public MyLinkedList inOrderRecursive() {
        MyLinkedList<T> list = new MyLinkedList<>();

        inOrderRecursiveHelper(list, root);

        return list;
    }

    // 递归遍历某个结点
    private void inOrderRecursiveHelper(MyLinkedList<T> list, Node node) {
        if (node == null) {
            return;
        }

        // 递归遍历左子树
        inOrderRecursiveHelper(list, node.left);

        list.add(node.value);

        // 递归遍历右子树
        inOrderRecursiveHelper(list, node.right);
    }

    /*------------------------建树(还原一棵树)---------------------------*/

    /**
     * 递归实现
     * 由先序序列和中序序列还原一棵树
     *
     * @param inOrder  中序序列
     * @param preOrder 先序序列
     * @return node 树的根结点
     * @author zxcsjf
     * @since 2022/07/11 20:07:53
     */
    public void buildTreeByInAndPreOrder(List<T> inOrder, List<T> preOrder) {
       root = buildTreeByInAndPreOrderHelper(inOrder, preOrder);
       size = inOrder.size();

    }

    private Node buildTreeByInAndPreOrderHelper(List<T> inOrder, List<T> preOrder) {
        // 递归出口
        if (inOrder.size() == 0) {
            return null;
        }

        // 先序序列的第一个结点,就是根结点
        T value = preOrder.get(0);
        // 找到根节点在中序序列的位置
        int index = inOrder.indexOf(value);
        // 构建根节点
        Node root = new Node(value);


        // 在先序序列中,切割出左右子树
        //       left子树的先序:  [1  ~  index + 1)
        //       right子树的先序: [index + 1  ~  preOrder.size())
        List<T> preOrderLeft = preOrder.subList(1, index + 1);
        List<T> preOrderRight = preOrder.subList(index + 1, preOrder.size());

        // 在中序序列中,切割出对应的左右子树的,先序序列,用来递归
        //       left子树的中序:  [0  ~  index)
        //       right子树的中序: [index + 1  ~ inOrder.size())
        List<T> inOrderLeft = inOrder.subList(0, index);
        List<T> inOrderRight = inOrder.subList(index + 1, inOrder.size());

        // 根据left子树的中序和先序, 递归构建left子树, 返回left子树的根结点
        root.left = buildTreeByInAndPreOrderHelper(inOrderLeft, preOrderLeft);
        // 根据right子树的中序和先序, 递归构建right子树, 返回right子树的根结点
        root.right = buildTreeByInAndPreOrderHelper(inOrderRight, preOrderRight);

        return root;

    }


    /**
     * 递归实现
     * 中序 + 后序 还原一棵树
     * @param inOrder  中序序列
     * @param postOrder 后序序列
     * @return
     * @author zxcsjf
     * @since 2022/07/11 20:41:00
     */
    public void buildTreeByInAndPostOrder(List<T> inOrder, List<T> postOrder) {
        root = buildTreeByInAndPostOrderHelper(inOrder, postOrder);
        size = inOrder.size();

    }

    private Node buildTreeByInAndPostOrderHelper(List<T> inOrder, List<T> postOrder) {
        // 递归出口
        if (inOrder.size() == 0) {
            return null;
        }

        // 找到根结点内容: 后序中最后一个位置
        T value = postOrder.get(postOrder.size() - 1);

        // 构建根结点
        Node root = new Node(value);

        // 在中序中, 根据根的内容, 确定根的下标位置
        int index = inOrder.indexOf(value);


        // 在后序序列中,切割出左右子树
        //       left子树的后序:  [0 ~  index)
        //       right子树的后序: [index ~  postOrder.size() - 1)
        List<T> postOrderLeft = postOrder.subList(0, index);
        List<T> postOrderRight = postOrder.subList(index, postOrder.size() - 1);

        // 在中序序列中,切割出对应的左右子树的,先序序列,用来递归
        //       left子树的中序:  [0  ~  index)
        //       right子树的中序: [index + 1  ~ inOrder.size())
        List<T> inOrderLeft = inOrder.subList(0, index);
        List<T> inOrderRight = inOrder.subList(index + 1, inOrder.size());

        // 根据left子树的中序和后序, 递归构建left子树,  返回left子树的根结点
        root.left = buildTreeByInAndPostOrderHelper(inOrderLeft, postOrderLeft);

        // 根据right子树的中序和后序, 递归构建right子树,  返回right子树的根结点
        root.right = buildTreeByInAndPostOrderHelper(inOrderRight, postOrderRight);

        return root;

    }



    private boolean isEmpty() {
        return root == null;
    }

    public Node getRoot() {
        return root;
    }

    // 树的结点
    class Node {
        T value; // 值域
        Node left; // 左孩子
        Node right; // 右孩子

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

}

4.自平衡的二叉搜索树

树本身适合存储大批量的数据, 但是我们不希望稀疏树产生, 稀疏树的性能太差

自平衡的二叉搜索树, 存在意义, 是为了解决普通二叉搜索树, 在不断添加和删除的过程中变成稀疏树导致效率降低的问题的

自平衡的二叉搜索树 ----> 平衡性能是很好的

由于平衡性能太好, 所以导致这个树高度非常严格: 有可能会新加一个结点, 导致很上层的结构变得不平衡, 所以导致变换操作(旋转)变成非局部性操作, 代码实现过于复杂

5.红黑树

// 红黑树是一个二叉搜索树  /  红黑树是一个自平衡的二叉搜索树
// 红黑树中每一个结点都有颜色:  红色, 黑色
// 红黑树中根结点是黑色的,   叶子结点也是黑色的(nil)
// 红黑树中, 在父子关系上没有连续的红色结点
// 红黑树, 要保证黑高平衡(叶子结点,到根结点的简单路径上, 经过相同数目的黑色结点)

注意:在实际工程中, 用到的都是红黑树, 而非自平衡的二叉搜索树:  原因自平衡的二叉搜索树有可能旋转是个非局部性操作, 代码实现比较复杂, 红黑树虽然平衡性能, 不如自平衡的二叉搜索树, 但是操作是局部性操作.

// Java什么地方用到了红黑树? 为什么要在这个地方用到红黑树?

// 红黑树的有什么好处? 
相比较线性表: 好处是树比较适合大批量的数据存储
和普通的树比较: 二叉, 结点用链表实现好定义
和二叉树比较: 可以根据大小查找/添加/删除, 效率更高
和二叉搜索树比较: 红黑树不可能变成稀疏树
和自平衡的二叉搜索树比较: 虽然平衡性能没有自平衡的二叉搜索树好, 但是操作是局部性操作
    
   
// 红黑树是怎么保证没有连续的红色结点? 红黑树是怎么保证黑高平衡?
通过旋转, 以及向上分裂和向下分裂
 
旋转: 左旋, 右旋,先左旋再右旋,  先右旋转再左旋
      旋转过程中, 如果颜色不同, 发生颜色交换
    
向上分裂: 左右孩子由红变黑, 自己由黑变红
向下分裂: 自己由红变黑, 左右孩子由黑变红
    

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值