数据结构-二分搜索树

182 阅读9分钟

ba2e5cd85f4a56eeed1fd0feeecbb90f939bc41ecc76-6Ja1d3_fw658.jfif

树这个结构我们总在各种场合看见他,无论是文件夹系统还是mysql种索引的结构。本篇文章以二分搜索树为例,详细介绍其概念,树的操作,应用场景。

二分搜索树是什么

  • 树结构本身是一种天然的组织结构
  • 二分搜索树也叫做二叉查找树、有序二叉树等,是指一棵空树或者具有下列性质的二叉树:
    • 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
    • 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
    • 任意节点的左、右子树也分别为二叉查找树
  • 相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为O(logn)O(logn)
  • 是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等

image.png

  • 如图所示,是一个满的二分搜索树,因为每个节点都有两个子节点,如果图中最左侧的30节点没有,那么就不是满的二分搜索树
  • 存储的元素必须有可比较性,因为存储元素的时候,需要依次比对大小,来找到对应的存储位置
  • 其中每一个节点都可以是一个二分搜索树:
    • 以46为根节点组成的树是一个二分搜索树
    • 同样,以35为根节点组成的树也是一个二分搜索树,只不过他又被叫做以46为根节点组成的树的左子树
    • 同理,52叫做以46为根节点组成的树的右子树
    • 以30为根节点组成的树也是一个二分搜索树,只不过只有根节点,没有子节点,但是同样他被叫做以35为根节点组成的树的左子树
    • 以59为根节点组成的树也是一个二分搜索树,和30节点一样只有根节点,没有子节点,但是同样他被叫做以52为根节点组成的树的右子树
    • 小伙伴们体会一下其中涉及的几个术语,后面我们会用到
  • 像30、48 这样的最下面的节点 叫做叶子节点

实操

添加新元素

向开始图中的二分搜索树中添加28元素如何做呢?

根据前面的结构特性介绍我们知道左子树的值要小于根节点的值,右子树的值要大于根节点的值,所以如果新加入一个元素需要从根节点依次比较,找到合适的位置

  1. 28与46比较发现28小于46,所以他会出现在46的左子树一侧
  2. 28与35比较,发现28小于35,同理他会出现在35的左子树一侧
  3. 28与30比较,发现28小于30,同理他会出现在30的左子树一侧
  4. 这个时候,30的左子树是空,所以28作为30的左子树,到此添加元素结束
  5. 变成如图所示的结构

image.png

  • 这里注意,这里的二分搜索树不包含相同的元素,如果加入相同的元素,例如加入52,那么依次比对后,到达52的位置将会什么都不做

查找元素

  • 有了上面添加元素的经历,想必查找元素信手拈来了吧
  • 只需要依次比对即可,例如查找39元素
  • 39与46比较,小于,则在其左子树中查找。与35比较,大于,则在其右子树中查找,与39比较,发现相等,查找结束

遍历元素

  • 在线性结构中遍历是个容易的操作,只需要从头到尾的访问一遍即可,
  • 但是在树这种结构中要如何操作呢?其实也没有那么难,
  • 前人给我们指出了三种遍历的方式,小伙伴们可以仔细的体会
前序遍历
  • 说人话就是 根节点-左子树-右子树(根-左-右) 这样的遍历方式,请看详细解析
  1. 这里有递归的思想,小伙伴们可以借助递归思想来帮助理解
  2. 先找到整棵树的根,然后找到其左子树,前面我们说到左子树也是一个二分搜索树,所以其左子树的遍历同样是需要找到根,然后找到其左子树,看到这里,小伙伴们想必体会到了递归的感觉,找到这个左子树还是一样的道理,遍历其左子树中的根,然后在是左子树,循环往复,直到其左子树没有值。
  3. 遍历其右子树,与遍历左子树一样的思路,其右子树中也是遍历其根节点,左子树... 循环往复。
  4. 思路就说到这里,下面以开始的二分搜索树为例子遍历。小伙伴们看完实际例子,想必再看这个思路会相辅相成,彻底理解

  1. 整棵树的根即为46 ,遍历输出46
  2. 找到其左子树,(也就是(35,30,39) 组成的树)。根节点是35,遍历输出35
  3. 接着找以35为根的左子树((30,28)这个树)。根节点是30,遍历输出30
  4. 接着找以30为根的左子树,即28,遍历输出28
  5. 接着找以28为根的左子树,为空。这个时候以28为根的左子树就遍历完了。
  6. 接着找以28为根的右子树,为空。这个时候以28为根的右子树就遍历完了。
  7. 接着找以30为根的右子树,为空。这个时候以30为根的右子树就遍历完了。
  8. 接着找以35为根的右子树(39这个树),根节点是39 遍历输出39
  9. 接着找以39为根的左子树,为空。这个时候以39为根的左子树就遍历完了。
  10. 接着找以39为根的右子树,为空。这个时候以39为根的右子树就遍历完了。
  11. 到此,以46为根的左子树就遍历完了。
  12. 小伙伴们可以自己体会一下以46为根的右子树的遍历
  13. 遍历结果是 46 35 30 28 39  52 48 59
中序遍历
  • 左子树-根节点-右子树(左-根-右) 这样的遍历方式
  • 有了上面的前序遍历方式,这个中序遍历想必就容易了吧
  • 即先遍历其左子树,在其左子树中还是遍历其左子树...循环往复
  • 遍历结果是:28 30 35 39 46 48 52 59
  • 是从小到大的排序哦,不知道小伙伴们发现没
后序遍历
  • 左子树-右子树-根节点(左-右-根) 这样的遍历方式
  • 这个不用多说了吧,遍历结果是:28 30 39 35 48 59 52 46

删除元素

  • 删除节点,小伙伴们可以先体会一下要如何删除
  • 先从简单的 删除最大,最小的元素开始
删除最小的元素
  • 最小的元素就是整个树最左边的节点
  • 如果该节点没有子节点,直接删除即可
  • 如果该节点有子节点,那么肯定是右子树,将右子树连接到最小节点的根节点即可。
  • 如下图,删除30节点,将32节点补上去

image.png

image.png

删除最大的元素
  • 最大的元素就是整个树最右边的节点
  • 与删除最小的节点同理,如果最大的节点有左子树那么将其连接到最大节点的根节点即可

删除任意位置的元素

image.png

  • 有了上面两个删除动作,删除任意位置的元素变得稍微简单
  • 如果带删除得节点没有右子树,那么和删除最小元素一样
  • 如果带删除得节点没有左子树,那么和删除最大元素一样
  • 比较困难的是待删除的元素有左右子树例如删除52节点,这种情况其子节点要如何处理?这里举出一个思路
    • 找到以52节点为根节点的右子树中最小的节点,即57
    • 将57移除,把57添加到原来的52的位置
    • 如下图所示

image.png

代码实操

//添加的元素必须要支持可比较性
public class BinTree<E extends Comparable<E>> {

    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 BinTree(){
        root = null;
        size = 0;
    }

    public int size(){
        return size;
    }

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

    // 向二分搜索树中添加新的元素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
    public boolean contains(E e){
        return contains(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);
        }
    }

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

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

        if(node == null){
            return;
        }

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

    // 二分搜索树的非递归前序遍历
    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);
            }
        }
    }

    // 二分搜索树的中序遍历
    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);
    }

    // 二分搜索树的后序遍历
    public void postOrder(){
        postOrder(root);
    }

    // 后序遍历以node为根的二分搜索树, 递归算法
    private void postOrder(Node node){

        if(node == null){
            return;
        }

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

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

        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("树是空");
        }

        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;
    }

    // 从二分搜索树中删除元素为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);
            return node;
        }
        else if(e.compareTo(node.e) > 0 ){
            node.right = remove(node.right, e);
            return node;
        }
        else{   // e.compareTo(node.e) == 0

            // 待删除节点左子树为空的情况
            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);
            successor.left = node.left;

            node.left = node.right = null;

            return successor;
        }
    }

    @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();
    }
}

到此 二分搜索树就介绍完毕了,小伙伴们可以结合理论和代码来体会树的添加元素和删除元素,递归的思想也会有更深的体会