5.二分搜索树

一. 为什么要研究树结构

  • 树结构的本身是一种天然的组织结构
  • 将数据使用树结构存储后,出奇的高效
二分搜索树(Binary Search Tree)
平衡二叉树:AVL; 红黑树
堆;并查集
线段树;Trie(字典树, 前缀树)

二. 二分搜索树基础

1.二叉树

image

image

image

image

2. 二分搜索树

image

代码框架搭建

新建项目BST

·
├── BST.iml
└── src
    ├── BST.java
    └── Main.java

BST.java

public class BST<E extends Comparable> {   // 对类型进行限制,并非可以存储所有类型, 要求必须是可以比较的类型

    private class Node{
        public E e;
        public Node left, right;

        public Node(E e){
            this.e = e;
            left = null;
            right = null;
        }
    }

    private Node root;
    private int size;

    public BST(){
        root = null;
        size = 0;
    }


    public int size(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }
}


三. 向二分搜索树添加元素

  • 插入的节点 比 根结点

    • 小:放左子树
    • 大:放右子树。
    • 递归比较,最后找到合适的位置。
  • 此处二分搜索树不包含重复元素

    • 如果想包含重复元素,需要定义: 左子树小于等于节点;或右子树大于等于节点
  • 代码实现

BST.java

public class BST<E extends Comparable> {

    private class Node{
        public E e;
        public Node left, right;

        public Node(E e){
            this.e = e;
            left = null;
            right = null;
        }
    }

    private Node root;
    private int size;

    public BST(){
        root = null;
        size = 0;
    }


    public int size(){
        return size;
    }

    public boolean isEmpty(){
        return size == 0;
    }

    // 向二分搜索树中添加新元素e
    public void add(E e){

        if(root == null){
            root = new Node(e);
            size++;
        }
        else{
            add(root, e);
        }
    }

    // 向以node为根的二分搜索树中插入元素e 递归算法
    private void add(Node node, E e){
        // 递归结束的情况
        if(e.equals(node.e)){
            return;
        }
        else if(e.compareTo(node.e) < 0 && node.left == null){
            node.left = new Node(e);
            size++;
            return;
        }
        else if(e.compareTo(node.e) > 0 && node.right == null){
            node.right = new Node(e);
            size++;
            return
        }

        if(e.compareTo(node.e) < 0){
            add(node.left, e);  // 左子树继续插入元素e
        }
        else{
            add(node.right, e); // 右子树继续插入元素e
        }
    }
}

三. 改进添加操作: 深入理解递归终止条件

修改递归终止条件:

  • null也为节点, 以给null节点赋值为终止条件
    BST.java
public class BST<E extends Comparable> {

    ...
    
    // 向二分搜索树中添加新元素e
    public void add(E e) {

        root = add(root, e);
    }

    // 向以node为根的二分搜索树中插入元素e 递归算法
    // 返回插入新节点后二分搜索树的根
    private Node add(Node node, E e) {
        if (node == null) {
            size++;
            return new Node(e);
        }
        if (e.compareTo(node.e) < 0) {
            node.left = add(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            node.right = add(node.right, e);
        }

        return node;
    }
}


四. 二分搜索树的查询操作

新增方法: 查看二分搜索树是否包含元素e BST.java

public class BST<E extends Comparable> {

    ...
    
    // 查看二分搜索树是否包含元素e
    public boolean contains(E e){
        return conatins(root, e);
    }
    
    // 查看以node为根的二分搜索树是否包含元素e 递归算法
    private boolean contains(Node node, E e){
        if(node == null){
            return false;
        }
        
        if(e.compareTo(node.e) == 0){
            return true;
        }
        else if(e.compareTo(node.e) < 0){
            return contains(node.left, e);
        }
        else{
            return contains(node.right, e);
        }
    }
}


六. 二分搜索树的前序遍历

image

前序遍历:先访问该节点,再遍历该节点的子节点

image

代码实现

BST.java

public class BST<E extends Comparable> {

    ...

    // 二分搜索树的前序遍历
    public void preOder(){
        preOrder(root);
    }

    //前序遍历以node为根的二分搜索树
    private void preOrder(Node node){

        if(node == null){
            return;
        }

        System.out.println(node.e);

        preOrder(node.left);
        preOrder(node.right);
    }
}

测试

Main.java

public class Main {

    public static void main(String[] args) {
        BST<Integer> bst = new BST<>();
        int[] nums = {5,3,6,8,4,2};
        for(int num: nums){
            bst.add(num);
        }

        bst.preOder();
    }
}

运行结果:

5
3
2
4
6

基于前序遍历,重写toString

BST.java

public class BST<E extends Comparable> {

    ...

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        generateBSTString(root, 0, res);
        return res.toString();
    }

    // 基于前序遍历,生成以node为根结点,深度为depth的描述二叉树字符串
    private void generateBSTString(Node node, int depth, StringBuilder res) {

        if (node == null) {
            res.append(generateDepthString(depth) + "null\n");
            return;
        }

        res.append(generateDepthString(depth) + node.e + "\n");
        generateBSTString(node.left, depth + 1, res);
        generateBSTString(node.right, depth + 1, res);

    }

    // 打印节点深度
    private String generateDepthString(int depth) {
        StringBuilder res = new StringBuilder();
        for (int i = 0; i < depth; i++) {
            res.append("--");
        }
        return res.toString();
    }
}

Main.java

public class Main {

    public static void main(String[] args) {
        BST<Integer> bst = new BST<>();
        int[] nums = {5,3,6,8,4,2};
        for(int num: nums){
            bst.add(num);
        }

        //bst.preOder();

        System.out.println(bst);
    }
}


结果可以看到节点层次很清晰:

5
--3
----2
------null
------null
----4
------null
------null
--6
----null
----8
------null
------null

七. 二分搜索树的中序遍历和后序遍历

前序遍历是最自然的遍历方式, 也是最常用的。

但既然有前序遍历肯定也有中序遍历和后续遍历。

  • 中序遍历(按大小顺序排序)

中序遍历的顺序:
1. 先访问   node.left
2. 再访问   node.e
3. 最后访问 node.right

中序遍历的应用: 按大小排序

BST.java


public class BST<E extends Comparable> {
    ...
    
    // 中序遍历
    public void inOrder(){
        inOrder(root);
    }

    // node为根的中序遍历
    private void inOrder(Node node){
        if(node == null){
            return;
        }

        inOrder(node.left);
        System.out.println(node.e);
        inOrder(node.right);
    }
}

测试 MAin.java

public class Main {

    public static void main(String[] args) {
        BST<Integer> bst = new BST<>();
        int[] nums = {5,3,6,8,4,2};
        for(int num: nums){
            bst.add(num);
        }

        //bst.preOder();

        //System.out.println(bst);

        bst.inOrder();
    }
}

运行结果, 正好是排序结果

2
3
4
5
6
8

  • 后序遍历

后序遍历的应用:
    为二分搜索树释放内存

BST.java

public class BST<E extends Comparable> {

    ...
    
    // 后序遍历
    public void postOrder(){
        postOrder(root);
    }

    // node为根的后序遍历
    private void postOrder(Node node){
        if(node == null){
            return;
        }

        inOrder(node.left);
        inOrder(node.right);
        System.out.println(node.e);
    }

八. 再看二分搜索树的遍历

我要做到给一个二分搜索树, 能够立马看出三种遍历得到的结果。

image

image

image


九. 二分搜索树前序遍历的非递归实现

  • 使用栈来实现前序遍历流程:

流程:
1. node压入栈
2. 取出node
3. node.right压入栈
4. node.left压入栈
5. 取出node.left
6. node.left.right压入栈
7. node.left.left压入栈(假设node.left.left之后,node的左子树没有剩余元素了)
8. 取出node.left.left
9. 取出node.left.right
10.取出node.right
11.node.right.right压入栈
12.node.right.left压入栈(假设node.right.left之后,node的右子树没有剩余元素了)
13.取出node.right.left
14.取出node.right.right

image

  • 代码实现

BST.java

import java.util.Stack;

public class BST<E extends Comparable> {
    ...
    
    // 非递归前序遍历
    public void preOrderNR(){
        Stack<Node> stack = new Stack<>();
        stack.push(root);
        while(!stack.isEmpty()){
            Node cur = stack.pop();
            System.out.println(cur.e);

            if(cur.right != null) {
                stack.push(cur.right);
            }
            if(cur.left!=null){
                stack.push(cur.left);
            }
        }
    }
}
- 二分搜索树的非递归实现,比递归实现复杂很多
- 中序遍历和后序遍历的非递归实现更复杂
- 中序遍历和后序遍历的非递归实现,实际应用不广

十. 二分搜索树的层序遍历

  • 层序遍历即 广度优先的遍历。需要使用队列实现。


image

  • 代码实现

BST.java

import java.util.Stack;
import java.util.Queue;
import java.util.LinkedList;

public class BST<E extends Comparable> {
    ...

    // 层序遍历
    public void levelOrder(){
        Queue<Node> q = new LinkedList<>();    // javaQueue只是接口, 我们需要使用LinkedList来具体承载Queue
        q.add(root);
        while(!q.isEmpty()){
            Node cur = q.remove();
            System.out.println(cur.e);
            
            if(cur.left != null){
                q.add(cur.left);
            }
            if(cur.right != null){
                q.add(cur.right);
            }
        }
    }

}
  • 广度优先遍历的意义

    • 更快的找到问题的解
    • 常用于算法设计中 - 最短路径

十一. 删除二分搜索树的最大元素和最小元素

二分搜索树最左边的节点是最小节点, 最右边的节点是最大节点。

BST.java

import java.util.Stack;
import java.util.Queue;
import java.util.LinkedList;

public class BST<E extends Comparable> {

    ...
        // 寻找二分搜索树的最小元素
    public E minimum() {
        if (size == 0) {
            throw new IllegalArgumentException("BST is empty!");
        }

        return minimum(root).e;
    }

    // 返回以node为根的二分搜索树的最小值所在的节点
    private Node minimum(Node node) {
        if (node.left == null) {
            return node;
        }
        return minimum(node.left);
    }


    // 寻找二分搜索树的最大元素
    public E maximum() {
        if (size == 0) {
            throw new IllegalArgumentException("BST is empty!");
        }

        return maximum(root).e;
    }

    // 返回以node为根的二分搜索树的最大值所在的节点
    private Node maximum(Node node) {
        if (node.right == null) {
            return node;
        }
        return maximum(node.right);
    }

    // 删除最小值所在的节点, 返回最小值
    public E removeMin() {
        E ret = minimum();
        root = removeMin(root);
        return ret;
    }

    // 删除以node为根的最小节点, 返回删除节点后的根
    private Node removeMin(Node node) {
        if (node.left == null) {
            Node rightNode = node.right;
            node.right = null;
            size--;
            return rightNode;
        }

        node.left = removeMin(node.left);
        return node;
    }

    // 删除最大值所在的节点, 返回最大值
    public E removeMax() {
        E ret = maximum();
        root = removeMax(root);
        return ret;
    }

    // 删除以node为根的最大节点, 返回删除节点后的根
    private Node removeMax(Node node) {
        if (node.right == null) {
            Node leftNode = node.left;
            node.left = null;
            size--;
            return leftNode;
        }

        node.right = removeMax(node.right);
        return node;
    }
}

测试Main.java

import java.util.ArrayList;
import java.util.Random;

public class Main {

    public static void main(String[] args) {
        BST<Integer> bst = new BST<>();
        Random random = new Random();

        int n = 1000;

        for (int i = 0; i < n; i++) {
            bst.add(random.nextInt(1000));   // 生成1000个随机数,放入二分搜索树
        }

        ArrayList<Integer> minnums = new ArrayList<>();
        while (!bst.isEmpty())
            minnums.add(bst.removeMin());   // 按从小到大的顺序从二分搜索树中删除, 装入minnums

        System.out.println(minnums);

        for (int i = 1; i < minnums.size(); i++) {  // 如果不是从小到大排序,就报错
            if (minnums.get(i - 1) > minnums.get(i))
                throw new IllegalArgumentException("Error");
        }
        System.out.println("removeMin  ok!");

        for (int i = 0; i < n; i++) {
            bst.add(random.nextInt(1000));
        }

        ArrayList<Integer> maxnums = new ArrayList<>();
        while (!bst.isEmpty())
            maxnums.add(bst.removeMax());

        System.out.println(maxnums);

        for (int i = 1; i < maxnums.size(); i++) {  // 如果不是从大到小排序,就报错
            if (maxnums.get(i - 1) < maxnums.get(i))
                throw new IllegalArgumentException("Error");
        }
        System.out.println("removeMax  ok!");
    }
}


运行结果:

[0, 2, 3, 4, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 20, 21, 25, 26, 28, 29, 31...
removeMin  ok!

[999, 997, 996, 994, 993, 992, 989, 988, 983, 980, 978, 975, 974, 972, 971...
removeMax  ok!


十二. 删除二分搜索树的任意元素

  • 方法概述

  • 对于只有单孩子的节点, 删除很简单, 只需将该节点的父节点与该节点的子节点连接即可。
  • 对于左右都有孩子的节点, 相对复杂些, 我们以图的形式讲解

image

image

上面的图中流程中
是将 59放在被删节点d的位置上(后驱的方式)
我们也可以将53放在被删除元素d的位置上(前驱的方式)
  • 代码实现

BST.java

import java.util.Stack;
import java.util.Queue;
import java.util.LinkedList;

public class BST<E extends Comparable> {
    ...
    
  // 从二分搜索树中删除元素为e的节点
    public void remove(E e) {
        root = remove(root, e);
    }

    // 删除以node为根的二分搜索树中 值为e的节点, 递归算饭
    // 返回删除节点后新的二分搜索树的根
    private Node remove(Node node, E e) {
        if (node == null) {
            return null;
        }
        if (e.compareTo(node.e) < 0) {
            node.left = remove(node.left, e);
        } else if (e.compareTo(node.e) > 0) {
            node.right = remove(node.right, e);
        } else {   // e node.e相等的情况, 即删除node
            if (node.left == null) { // 待删除的节点 左子树为空
                Node rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }

            if(node.right == null){  // 待删除的节点 右子树为空
                Node leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }

            // 待删除的节点 左右子树均不为空的情况
            // 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
            // 用这个节点顶替删除节点的位置
            Node successor = minimum(node.right);
            successor.right = removeMin(node.right);   // removeMin中进行过size--
            successor.left = node.left;

            node.left = node.right = null;
            return successor;
        }
    }
}


十三. 更多的二分搜索树话题

1. floor和ceil

给一个元素(比一定要在二分搜索树中), 找出树中刚好比它小(floor)和刚好比它大(ceil)的元素

imaege

2. rank和select

  • rank: 给出一个树中的元素, 得出它的排名

image

  • select:给出一个排名, 找出该排名对应的元素

image

实现rankselect比较好的方式 就是: 构建 维护size的二分搜索树

3. 维护size的二分搜索树

即每个节点 都增加一个元素, 用来记录 以该节点为根的二分搜索树共有多少节点。

image

4. 维护depth的二分搜索树

image

5. 支持重复元素的二分搜索树

image

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/80882233
今日推荐