数据结构第一季 Day07 二叉搜索树

一、二叉搜索树

1.O(logn)的计算规模有什么特点?

  • 经典的一句话:像计算规模不断除以 2 的,那么它的复杂度就是 O(log2n)

2. 在 n 个动态的整数中搜索某个整数(从这里理解为什么我们需要学习二叉搜索树)?

image.png

3. 二叉搜索树的英文名是什么?基本定义是什么?

  • 二叉搜索树:Binary Search Tree
image.png

4. 观察如下二叉搜索树的接口设计,思考对比普通数组的接口,少了什么概念?为什么会少掉?

  • 少了索引的概念
  • 因为二叉搜索树的索引概念比较难以定义
image.png

二、对二叉树的遍历

1、线性数据结构的遍历比较简单,通常有哪两种方法?

  • 正序遍历
  • 逆序遍历

2、根据节点访问顺序的不同,二叉树的场景遍历方式有 4 种,分别是什么?

  • 前序遍历(Preorder Traversal)
  • 中序遍历(Inorder Traversal)
  • 后续遍历(Postorder Traversal)
  • 层序遍历 (Level Order Traversal)

3、什么是前序遍历(Preorder Traversal)?代码如何实现?

  • 访问顺序:根节点、前序遍历左子树、前序遍历右子树
    public void preorderTraversal() {
        preorderTraversal(root);
    }
    private void  preorderTraversal(Node node) {
        if (node == null) return;
        System.out.print(node.element + ",");
        preorderTraversal(node.left);
        preorderTraversal(node.right);
    }

4、什么是中序遍历(Inorder Traversal)?代码如何实现?

  • 访问顺序:中序遍历左子树、根节点、中序遍历右子树
    public void inorderTraversal() {
        inorderTraversal(root);
    }
    private void inorderTraversal(Node node) {
        if (node == null) return;
        inorderTraversal(node.left);
        System.out.print(node.element + ",");
        inorderTraversal(node.right);
    }

5、什么是后序遍历(Postorder Traversal)?代码如何实现?

  • 访问顺序:后续遍历左子树、后续遍历右子树、根节点
    public void postorderTraversal() {
        postorderTraversal(root);
    }
    private void postorderTraversal(Node node) {
        if (node == null) return;
        postorderTraversal(node.left);
        postorderTraversal(node.right);
        System.out.print(node.element + ",");
    }

6、什么是层序遍历(Level Order Traversal)?代码如何实现?

  • 访问顺序:从上到下,从左到右依次访问每一个节点
    public void levelOrderTraversal() {
        if (root == null) return;
        Queue> queue = new LinkedList<>();
        queue.offer(root);
        while(!queue.isEmpty()) {
            Node node = queue.poll();
            System.out.print(node.element + ",");
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }

7、上面四种遍其实还存在问题,比如外部想拿到二叉树每个节点做点事情,是无法办到的,怎么办?

  • 主要思路:让外部代码逻辑传递给搜索二叉树,让内部在合适的实际进行调用。
    public static interface Visitor {
        public boolean visit(E element);
    }
    public void levelOrder(Visitor visitor) {
        if (root == null) return;
        Queue> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            Node node = queue.poll();
            if (visitor.visit(node.element)) return;
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }

8、如果还想对上面的遍历方法做一次增加,客户端可以自由控制遍历停止时机,要怎么做?

  • 对于层级遍历非常容易,仅需提供一个返回值就可以达到要求。
  • 对于其他的递归遍历,需要将 Visitor 接口变成抽象类,存储一个 stop 值。(因为 visitor 是跟随整个过程的)
    //抽象类
    public static abstract class Visitor {
        boolean stop = false;
        public abstract boolean visit(E element);
    }
    //后续遍历
    public void postorder(Visitor visitor) {
        postorder(root, visitor);
    }
    private void postorder(Node node, Visitor visitor) {
        if (node == null || visitor.stop) return;
        postorder(node.left, visitor);
        postorder(node.right, visitor);
        if (visitor.stop) return;
        visitor.stop = visitor.visit(node.element);
    }

三、打印二叉树、练习获取二叉树高度、是否为完全二叉树,翻转二叉树

1、当你在网络上看到一棵二叉搜索树图片,如何将二叉搜索树在你的代码里面还原?

  • 使用层序遍历的顺序,将节点填入数组

2、如何有层次结构的打印一棵二叉树?

  • 通常使用前序遍历的逻辑进行打印,优先打印根节点。
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        toString(root, sb, "");
        return sb.toString();
    }
    private void toString(Node node, StringBuilder sb, String prefix) {
        if (node == null) return;
        sb.append(prefix).append(node.element).append("\n");
        toString(node.left, sb, prefix + "L__");
        toString(node.right, sb, prefix + "R__");
    }

3、获取二叉树的高度?

  • 递归实现:①第一层节点高度 = max(第二层左子树高度+1, 第二层右子树高度+1) ②如果节点为空,则返回 0
    public int height1() {
        return height1(root);
    }
    //递归实现:获取二叉树高度
    private int height1(Node node) {
        if (node == null) return 0;
        return 1 + Math.max(height1(node.left), height1(node.right));
    }
  • 层级遍历实现:每遍历完一层 height++(在即将遍历下一层时,可以拿到下一层的节点总数)
    public int height() {
        Queue> queue = new LinkedList<>();
        queue.offer(root);
        //树的高度
        int height = 0;
        //存储着每一层的元素数量
        int levelSize = queue.size();
        while (!queue.isEmpty()) {
            Node node = queue.poll();
            levelSize--;
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
            if (levelSize == 0) { //意味着即将要访问下一层
                height ++;
                levelSize = queue.size();
            }
        }
        return height;
    }

4、判断是否为完全二叉树?

    public boolean isComplete() {
        if (root == null) return false;

        Queue> queue = new LinkedList<>();
        queue.offer(root);
        boolean leaf = false;
        while (!queue.isEmpty()) {
            Node node = queue.poll();
            if (!node.isLeaf() && leaf) {
                return false;
            }

            if (node.left != null) {
                queue.offer(node.left);
            } else if (node.right != null) {//①如果node.left == null && node.right != null 返回 false
                return false;
            }

            if (node.right != null) {
                queue.offer(node.right);
            }

            if (!node.hasTwoChildren()) {
                leaf = true;
            }
        }
        return true;
    }

5、如何翻转一棵二叉树?

    public TreeNode invertTree(TreeNode root) {
        if (root == null) return null;
        Queue queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            TreeNode tempNode = node.left;
            node.left = node.right;
            node.right = tempNode;
            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
        return root;
    }

四、前驱节点、后继节点、删除节点

1、根据遍历结果如何重构二叉树?

以下结果可以保证重构出唯一的一棵二叉树

  • ① 前序遍历 + 中序遍历
  • ② 后序遍历 + 中序遍历
  • ③ 前序遍历 + 后序遍历 + 它是一棵真二叉树(Proper Binary Tree)

2、前驱节点(predecessor)的定义?如何查找前驱节点?

  • 前驱节点:中序遍历时的前一个节点;如果是二叉搜索树,前驱节点就是前一个比它小的节点。
    private Node predecessor(Node node) {

        //前驱节点在左子树当中:predecessor = node.left.right.right.right...
        //终止条件:right 为 null
        Node p = node.left;
        if (p != null) {
            while (p.right != null) {
                p = p.right;
            }
            return p;
        }

        //前驱节点在父节点当中predecessor = node.parent.parent.parent...
        //终止条件:node 在 parent 的右子树
        while (node.parent != null && node == node.parent.left) {
            node = node.parent;
        }
        return node.parent;
    }

3、后继节点(successor)的定义?如何查找后继节点?

后继节点:中序遍历时的后一个节点;如果是二叉搜索树,后继节点就是后一个比它大的节点。

    private Node successor(Node node) {
        //后继节点在右子树当中:successor = node.right.left.left.left...
        //终止条件 left 为 null
        Node p = node.right;
        if (p != null) {
            while (p.left != null) {
                p = p.left;
            }
            return p;
        }

        //当后继节点在父节点中 successor = node.parent.parent.parent...
        //终止条件:node 在 parent 的左子树
        while (node.parent != null && node == node.parent.right) {
            node = node.parent;
        }
        return node.parent;
    }

4、删除分段思考--如何删除度为 0 的节点?

  • 直接删除
if (node == node.parent.left) node.parent.left = null;

if (node == node.parent.right) node.parent.right = null;

if (node.parent == null) root = null;

5、删除分段思考--如何删除度为 1 的节点?

  • 用子节点代替原节点的位置
  • child 是 node.left 或者 child 是 node.right
if(child == node.left) {
    child.parent = node.parent;
    child.parent.left = child;
}

if(child == node.right) {
    child.parent = node.parent;
    child.parent.right = child;
}

if(child == root) {
    root = child;
    child.parent = null;
}

6、删除分段思考--如何删除度为 2 的节点?

  • 先用前驱或后继节点的值覆盖原节点的值
  • 然后删除相应前驱或后继节点
  • 如果度为 2 的节点的前驱或后继节点,它们度要么为 0 要么为 1(因为前驱节点的查找特点,度为 2 的节点不会向父节点找前驱或后继)

7、代码删除节点

    private void remove(Node node) {
        if (node == null) return;
        size--;
        //先处理度为 2 的节点:①用前驱节点的值覆盖要删除的节点
        if (node.hasTwoChildren()) {
            Node predecessor = predecessor(node);
            node.element = predecessor.element;
            node = predecessor;
        }
        if (node.isLeaf()) { //②删除度为 0 的节点
            if (node.parent == null) {
                root = null;
            } else if (node == node.parent.left) {
                node.parent.left = null;
            } else {
                node.parent.right = null;
            }
        } else { //②删除度为 1 的节点
            Node replacement = node.left != null ? node.left : node.right;
            replacement.parent = node.parent;
            if (node.parent == null) {
                root = replacement;
            } else if (node == node.parent.left) {
                node.parent.left = replacement;
            } else {
                node.parent.right = replacement;
            }
        }
    }

你可能感兴趣的:(数据结构第一季 Day07 二叉搜索树)