一篇文章彻底理解二分搜索树(附代码实现)

本文使用JAVA语言进行描述。其实本质上什么语言描述数据结构都是可以的。

二叉树基础

在这里插入图片描述

二叉树的根节点

在这里插入图片描述
二叉树递归结构:
在这里插入图片描述
上面是一个满二叉树。但是实际中也有二叉树不是满的。
在这里插入图片描述
在这里插入图片描述

二分搜索树

在这里插入图片描述
二分搜索树也不一定是满的。
在这里插入图片描述
所以使用二分搜索树需要具备的条件:

存储的元素必须有可比较性。

树结构的定义代码实现:

/**
 * Created by hongyonghan on 2020/4/19.
 */
//为了让这个二分搜索树具有可比较性。那么继承Comparable的接口。
public class BST<E extends Comparable<E>> {
    private class Node{
        private E e;
        private Node left,right;

        public Node(E e) {
            this.e = e;
            left=null;
            right=null;
        }
    }
    private Node root;
    private int size;

    public BST() {
        root=null;
    }

    public int size()
    {
        return size;
    }

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

添加新元素

如果树之前是空的话,那么添加一个新元素之后,这个元素就是树的根。比如这里添加元素41.如果新加一个元素22,那么会根据二分搜索树的性质,将22作为二分搜索树的左子树的根节点。
在这里插入图片描述
在这里插入图片描述
那么该将60添加到哪个位置呢?逻辑如下:

60大于41,在右子树,60大于58,在右子树。所以添加如下:
在这里插入图片描述
这里的二分搜索树不包含重复元素。

如果想包含重复元素的话,只需要改变定义:左子树小于等于节点,或者右子树大于等于节点。

增加代码和查询代码:

  public void add(E e)
    {
        root=add(root,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)
    {
        //整体看以整颗二分搜索树为根的二分搜索树中是否包含元素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)
        {
            //如果不在node上面,那就去node的左子树去找,否则去node的右子树去找
            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 inOrder()
    {
        inOrder(root);
    }
    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);
    }
    private void postOrder(Node node)
    {
        if (node == null)
            return;
        postOrder(node.left);
        postOrder(node.right);
        System.out.println(node.e);
    }

对每一个节点使用递归遍历的方式,会有三次遍历的机会。分别是:在遍历左子树之前会访问这个根节点。在遍历左子树完成之后会回到根节点,会访问这个根节点,在遍历右子树之后会回到这个根节点,会再遍历一次。
在这里插入图片描述
那么对一棵树来说,前序遍历的过程:
在这里插入图片描述
这是前序遍历一棵树的部分过程,从28->16->13,下面的点表示的是这个访问了几次。

访问左子树完成之后:
在这里插入图片描述
前序遍历的完整过程:
在这里插入图片描述
这里面的蓝色的点就是第一次访问这个节点时候。然后对这个节点进行输出操作。

中序遍历:

中序遍历是当第二次访问到这个节点时候进行操作,第一次访问到该节点不进行操作。
在这里插入图片描述
例如这里的16,第一次访问16时,不输出16,只是第二次访问16时才输出16.

下面是左子树完成遍历的过程。
在这里插入图片描述
图中蓝色的点是对节点进行操作的时候:
在这里插入图片描述
后序遍历:
在这里插入图片描述
只有第三次访问节点的时候,才对节点进行操作。

部分树的操作如下:
在这里插入图片描述

前序遍历的非递归的写法

前序遍历的递归写法:
在这里插入图片描述
这里借助栈的特性记录遍历的位置:

首先,访问28节点,将节点压入栈,也就是接下来要访问28这个根节点了。
在这里插入图片描述
然后访问28这个节点,节点访问完成之后,28这个节点出栈
在这里插入图片描述
然后准备访问两个子节点,压栈30,16两个元素。因为先访问16,所以要先压栈30.
在这里插入图片描述
然后16出栈,压栈22和13.
在这里插入图片描述
然后出栈13,压入13的子节点,但是子节点为空,什么都不用压入了。然后出栈22.访问22节点。
在这里插入图片描述
然后出栈30,访问节点30;
在这里插入图片描述
然后将42和29入栈,然后访1问29节点。
在这里插入图片描述
然后取栈顶元素42.
在这里插入图片描述
遍历完成!


    //前序遍历的非递归的遍历。
    public void preOrderNR()
    {
        Stack<Node> stack=new Stack<>();
        //先入栈根节点
        stack.push(root);
        while (!stack.isEmpty())
        {
            //cur节点就是当前要访问的节点
            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);
        }
    }

广度优先遍历

按照层数来进行遍历。
在这里插入图片描述
这个是使用非递归的方式进行遍历的。使用队列的结构。

先将根节点入队,然后再将左右孩子入队。

具体展示如图:

初始化的时候将队首入队:
在这里插入图片描述
然后遍历的时候先看队首是谁,访问队首之后,将队首出队。这里是指28出队。
在这里插入图片描述
然后将第二层16和30入队。因为队列里面是先进先出,所以将树的左子树先入队,然后右子树再入队,这里是16先入队,然后30再入队。
在这里插入图片描述
然后取出队首元素16,对16进行访问。然后将16的孩子13和22入队。
在这里插入图片描述
然后30出队,对30进行访问。并对30的两个孩子29和42进行入队。
在这里插入图片描述
然后将13进行出队,并进行访问。13s是叶子节点,没有元素入队。
在这里插入图片描述
然后直到队首为空:遍历结束:
在这里插入图片描述
编程实现:

 //层序遍历(广度优先遍历)
    public void levelOrder()
    {
        Queue<Node> q=new LinkedList<>();
        //将根节点入队
        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);
        }

    }

中序遍历递归实现

//中序遍历递归实现
    public void inOrder()
    {
        inOrder(root);
    }
    private void inOrder(Node node)
    {
        if (node == null)
        {
            return;
        }
        inOrder(node.left);
        System.out.println(node.e);
        inOrder(node.right);
    }

广度优先遍历可以更快的找到你想要查找的元素。主要用于搜索上面。

删除节点

找到树中的最小值和最大值
在这里插入图片描述
在这里插入图片描述
不同树的结构是不同的,所以最小值可能不都在最左边的叶子节点,这里16就是最小值。

所以最小值是向左走再也走不动的位置。最大值是向右走再也走不动了。这里最大值是30。

代码实现:

 //找到树中元素的最小值
    public E minimum()
    {
        if (size==0)
            throw new IllegalArgumentException("BST is empty");
        return minimum(root).e;
    }
    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;
    }

    private Node maximum(Node node)
    {
        if (node.right == null)
             return node;
        return maximum(node.right);
    }
删除二分搜索树的最小值

如果最小值是这种情况:
在这里插入图片描述
这个是个叶子节点,直接将这个节点删除就行了。

但是如果最小值不是叶子节点呢?
在这里插入图片描述
那么就可以将22删除后,然后将后面的右子树做成41的左子树。
在这里插入图片描述

删除二分搜索树的最大值

如果是叶子节点,直接将叶子节点删除就好。

在这里插入图片描述
删除后:
在这里插入图片描述
如果不是叶子节点,例如这里的58这个节点,将这个节点删除之后,直接将这个节点的左子树变成41的右子树就可以了。
在这里插入图片描述

 //从二分搜索树中删除最小值所在节点,返回最小值
    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=removeMax();
        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;
    }
删除任意元素

1.删除的节点只有左孩子(或者右孩子)的话。例如下面删除的节点是58.
在这里插入图片描述
就是将这个节点删除后,然后将这个50当作根节点的右孩子。

2.删除左右都有孩子的节点d
在这里插入图片描述
删除58这个节点之后,要找一个新的节点来替代他,那么这里找的是比58这个节点还大的那个节点。也就是找到58的右子树中最小的那个节点。也就是59那个节点。
在这里插入图片描述
删除之后:
在这里插入图片描述
然后删除d,s成为新的子树的根。
在这里插入图片描述
代码如下:

//从二分搜索树中删除元素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==node.e
            //待删除节点左子树为空的情况
            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;



        }
    }

二分搜索树的顺序性

1.使用中序遍历的方法得到的就是有序的。

2.使用删除最大值和最小值的方法得到的也是有序的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章