手写二叉查找树(java版本)


BST


一、什么是二叉查找树?

二叉查找树(BST)是一种数据结构同时具有二分查找和插入元素时不需要扩容的功能

BST的定义
在这里插入图片描述
BST的特点

1.有序性:BST的中序遍历是有序的
在这里插入图片描述

2.具有高效的查找和插入性能:

在查找和插入时类似二分查找高效
在这里插入图片描述
平均时间复杂度:O(logn)

最坏情况下时间复杂度是O(n):

下图表示BST被退化成链表,这也是为什么我们要对二叉查找树进行平衡操作的原因

在这里插入图片描述

二、手写二叉查找树

本篇文章使用java语言手写实现BST数据结构的增删改查操作。如果代码有误望大家指正。

1.BST类和TreeNode结点

定义BST类和TreeNode类:后面的功能实现基于BST类


public class BST<E extends  Comparable<E>> {//支持泛型并且结点类型要具有可比较性所以extends  Comparable类
	//结点类型
    private class TreeNode{
        E data;
        TreeNode left;
        TreeNode right;
        public TreeNode(E data) {
            this.data = data;
        }
    }
	//树的根节点
    private TreeNode root;
    
    //size表示节点个数
    private int size;

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

    public int getSize() {
        return size;
    }
	//判空
    public boolean isEmpty(){
        return size == 0;
    }

}

2.插入

实现步骤:

1.如果root==null  , root 就是新结点
2. 如果root != null , 定义辅助指针curr帮助我们查找新结点应该插入到哪个位置

2.1插入结点(版本1)

//版本1:容易理解但是if-else循环有点多,看着不舒服
public void add(E e){
        if(root == null){
            root = new TreeNode(e);
        }else{
            TreeNode curr = root;
            //curr一直找要插入的位置,curr == null 时找到了,跳出循环
            while(curr != null){
                
                if(e.compareTo(curr.data) == 0){
                    
                    return;//直接返回,不做任何事情
                    
                }else if (e.compareTo(curr.data) < 0){//新节点要插入到curr的左边部分
                    
                    if(curr.left == null){//curr左子树为null
                        curr.left = new TreeNode(e);
                        size++;
                        return;
                    }else{//curr左子树不为null
                        curr = curr.left;//curr指向curr.left,接着进行查找
                    }
                    
                }else{//e.compareTo(curr.data) > 0
                    //新节点要插入到curr的右边部分
                    
                    if(curr.right == null){//curr右子树为null
                        curr.right = new TreeNode(e);
                        size++;
                        return;
                    }else{//curr右子树不为null
                        curr = curr.right;//curr指向curr.right,接着进行查找
                    }
                    
                }
            }
        }
    }

2.2插入结点(版本2)

//版本2:代码稍微优雅一点,少了一层if-else循环
 public void add(E e){
        if(root == null){
            root = new TreeNode(e);
        }else{
            //curr用于查找插入到哪个位置
            TreeNode curr = root;
            while(curr != null){

                if(e.compareTo(curr.data) == 0){
                    return;
                }else if(e.compareTo(curr.data) < 0 && curr.left == null){
                        curr.left = new TreeNode(e);
                        size++;
                        return;
                }else if(e.compareTo(curr.data) > 0 && curr.right == null){
                        curr.right = new TreeNode(e);
                        size++;
                        return;
                }

                if(e.compareTo(curr.data) < 0)  curr = curr.left;//e.compareTo(curr.data) < 0 && curr.left != null
                else    curr = curr.right;//e.compareTo(curr.data) > 0 && curr.right != null

            }
        }
    }

选择代码1或者代码2都可以。

3.查找

类似二分查找,比较简单

3.1查找任意结点


public boolean contains(E target){
        return find(target) == null ? false : true;
    }

public TreeNode find(E target){
            //空树直接返回null
            if(root == null)    return null;
            //辅助指针curr用于查找结点
            TreeNode curr = root;
            while (curr != null){
                if(target.compareTo(curr.data) == 0){
                    return curr;//找到了
                }else if(target.compareTo(curr.data) < 0){
                    curr = curr.left;
                }else{
                    curr = curr.right;
                }
            }
            //不存在该结点
            return null;
    }

//修改结点值    
public void set(E src , E target){
        if(contains(target))    return;
        TreeNode srcNode = find(src);
        srcNode.data = target;
    }

3.2查找最小值结点

//查找最小结点
	public E minValue(){
        if(root == null)    throw  new RuntimeException("tree is null");
        TreeNode min = root;
        while(min.left != null){
            min = min.left;
        }
        return min.data;
    }

3.2查找最大值结点

	//查找最大结点
    public E maxValue(){
        if(root == null)    throw new RuntimeException("tree is null");
        TreeNode max = root;
        while(max.right != null){
            max = max.right;
        }
        return max.data;
    }

4.遍历

遍历操作直接套用二叉树的遍历方式
下面给出非递归版本(递归读者可以自己实现)
注:二叉树的遍历非递归写法是面试的重点,大家一定要理解和记住代码

4.1先序遍历

//前序遍历二叉树
    public List<E> preOrder() {
        List<E> res = new ArrayList<>();
        if (root == null)   return res;
        // 1. 使用一个栈
        Stack<TreeNode> stack = new Stack<>();
        // 2. 将根节点压入栈中
        stack.push(root);
        // 3. 当栈不为空的时候,while循环
        while (!stack.isEmpty()) {
            // 3.1 取出栈顶结点
            TreeNode curr = stack.pop();
            // 3.2 处理弹出的结点
            res.add(curr.data);
            // 3.3 先将栈顶节点的右子节点压入栈中,再将左子节点压入栈中
            // 压入栈的目的是为了下一次循环的处理
            // 先压入右子节点的目的是先处理左子节点(栈有后进先出的特点)
            if (curr.right != null) stack.push(curr.right);
            if (curr.left != null) stack.push(curr.left);
        }
        return res;
    }

4.2中序遍历

//中序遍历二叉树
    public List<E> inOrder() {
        ArrayList<E> res = new ArrayList<>();
        if (root ==null) return res;
        Stack<TreeNode> stack = new Stack<>();
        TreeNode curr = root;
        while (curr != null || !stack.isEmpty()) {
            while (curr != null) {
                stack.push(curr);
                curr = curr.left;
            }
            TreeNode node = stack.pop();
            res.add(node.data);
            curr = node.right;
        }
        return res;
    }

4.3后序遍历

  //后序遍历
	public List<E> postOrder() {
        LinkedList res = new LinkedList<>();
        if (root == null)   return res;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode curr = stack.pop();
            res.addFirst(curr.data);
            if (curr.left != null) stack.push(curr.left);
            if (curr.right != null) stack.push(curr.right);
        }
        return res;
    }

4.4层序遍历

	//层序遍历
	public List<List<E>> levelOrder() {
        List<List<E>> res = new ArrayList<>();
        if (root == null) return res;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            // 每轮循环遍历处理一层的节点
            int size = queue.size();
            List<E> levelNodes = new ArrayList<>();
            for (int i = 0; i < size; i++) {
                TreeNode curr = queue.poll();
                levelNodes.add(curr.data);
                // 将遍历后的节点的左右子节点入队,等到下一轮 while 循环的时候遍历处理
                if (curr.left != null) queue.add(curr.left);
                if (curr.right != null) queue.add(curr.right);
            }
            res.add(levelNodes);
        }
        return res;
    }


5.删除

删除节点比较复杂:

要定义两个指针:

	min指针用于找到最小值结点
	parent指针用于找到最小值结点的父亲结点

5.1删除最小值结点

我们可能会认为只要找到最小的结点(min.left == null)
然后执行代码1即可

//代码1(错误!)
parent.left = null;

情况1:在这里插入图片描述
情况2:不可以
在这里插入图片描述
如果执行parent.left = null,会删掉整个左子树

我们应该执行代码2

//代码2
parent.left = min.right;
min.right = null;

成功删除
在这里插入图片描述
注意:如果最小结点是根节点
我们执行 root = root.right 删除根节点。

在这里插入图片描述

public E removeMin(){
        //空树没有最小值抛出异常
        if(root == null){
            throw  new RuntimeException("tree is null");
        }
        //min指针用于指向找到最小值
        TreeNode min = root;
        //parent记录min指针的父结点
        TreeNode parent = null;
        //通过min.left 是否为null去找最小值节点
        // min.left != null表示还没找到,接着找
        while(min.left != null){
            parent = min;
            min = min.left;
        }
        //min指向的是最小值节点
        if(parent == null){//表示min同时也是根节点,直接root=root.right
            root = root.right;
        }else{
            //表示min不是根节点进行代码2操作
            parent.left = min.right;
            min.right = null;
        }
        size--;
        return min.data;
    }

5.2删除最大值结点

类似删除最小的结点,代码也是对称的

//类似
parent.right = max.left;
max.left = null;

在这里插入图片描述
在这里插入图片描述
同时如果要删除的最大结点是根节点
执行root = root.left
在这里插入图片描述

//代码和删除最小值结点思路一样,对称写就好了
public E removeMax(){
        if(root == null){
            throw  new RuntimeException("tree is null");
        }
        TreeNode max = root;
        TreeNode parent = null;
        while(max.right != null){
            parent = max;
            max = max.right;
        }
        if(parent == null){
            root = root.left;
        }else{
            parent.right = max.left;
            max.left = null;
        }
        size--;
        return max.data;
    }

5.3删除任意结点

删除任意结点的前提是找到结点本身和结点的父亲结点

我们定义两个指针:curr指针和parent指针
curr用于找到该结点,parent为该结点的父亲结点

例如我们删除值为35的结点的前提如下图:

在这里插入图片描述

//代码1:找到要删除的结点
    public void remove(E e){
        if(root == null)    return;
        TreeNode curr = root;
        TreeNode parent = null;

        //找到要删除的结点
        while(curr != null && e.compareTo(curr.data) != 0){
            parent = curr;
            if(e.compareTo(curr.data) < 0){
                curr = curr.left;
            }else{
                curr = curr.right;
            }
        }
        //跳出while循环表示:curr==null 或者找到了该结点

        //如果没有找到要删除的结点,直接返回
        if(curr == null)    return;

    }

此时我们找到了要删除的结点
但是待删除的结点有四种情况:

5.3.1要删除的结点是叶子结点

叶子结点(没有左右子树):
例如下图要删除值为15的结点(parent.left = curr)
我们执行:

parent.left = null;

图1
在这里插入图片描述

再例如我们要删除值为25的结点(parent.right = curr)

图2
在这里插入图片描述

parent.right = null;

代码补充:

 //代码1:找到要删除的结点
    public void remove(E e){
        if(root == null)    return;
        TreeNode curr = root;
        TreeNode parent = null;

        //找到要删除的结点
        while(curr != null && e.compareTo(curr.data) != 0){
            parent = curr;
            if(e.compareTo(curr.data) < 0){
                curr = curr.left;
            }else{
                curr = curr.right;
            }
        }
        //跳出while循环表示:curr==null 或者找到了该结点

        //如果没有找到要删除的结点,直接返回
        if(curr == null)    return;
        
		//下面是四种情况讨论:
        if(curr.left == null && curr.right == null){//叶子结点
        	//同时要注意如果删除的是根节点,根节点也是叶子结点,我们直接让root 指向 null。否则下面else部分的代码会出现空指针异常。
            if(parent == null){//删除根节点
                root = null;
            }else{
                if(curr == parent.left){//图1
                    parent.left = null;
                }else if(curr == parent.right){//图2
                    parent.right = null;
                }
            }

        }else if(curr.left != null && curr.right == null){//只有左子树

        }else if(curr.left == null && curr.right != null){//只有右子树

        }else{//左右子树都有

        }

    }
5.3.2要删除的结点只有一个左子树

图1:删除值为22的结点(parent.left = curr)
在这里插入图片描述
我们执行

parent.left = curr.left;
curr.left = null;

图2:删除结点22的结点成功!
在这里插入图片描述
图3:删除值为66的结点(parent.right = curr)
在这里插入图片描述
我们执行

parent.right = curr.left;
curr.left = null;

图4:成功删除值为66的结点!
在这里插入图片描述
图5:要删除的是根节点(parent == null)
在这里插入图片描述
执行

root = root.left;

就好了!

所以删除只有左子树的结点代码如下

//只有左子树的else-if部分
else if(curr.left != null && curr.right == null){

            if(parent == null){//删除根节点
                root = root.left;
            }else{
                if(curr == parent.left){
                    parent.left = curr.left;
                    curr.left = null;
                }else if(curr == parent.right){
                    parent.right = curr.left;
                    curr.left = null;
                }
            }

        }
5.3.3要删除的结点只有一个右子树

图1:删除值为22的结点(parent.left = curr)
在这里插入图片描述
执行代码

parent.left = curr.right;
curr.right = null;

删除成功!
在这里插入图片描述

图2:如果要删除值为66的结点(parent.right = curr)
在这里插入图片描述
执行

parent.right = curr.right;
curr.right = null;

删除成功!
在这里插入图片描述
如果要删除的是根节点
在这里插入图片描述
执行

root = root.right;

代码汇总:

else if(curr.left == null && curr.right != null){//只有右子树
            if(parent == null){
                root = root.right;
            }else if(curr == parent.left){
                parent.left = curr.right;
                curr.right = null;
            }else if(curr == parent.right){
                parent.right = curr.right;
                curr.right = null;
            }  
        }
5.3.4要删除的结点同时有左子树和右子树

例如要删除结点66,但是要保证删除后保持BST的性质
所以很麻烦!
在这里插入图片描述
我们要怎么做呢?

应该先寻找结点66的右子树上的最小结点(68)
因为68存在一个性质:68大于 结点66的左子树 的任意一个结点值,68同时小于等于结点66的右子树 的任意一个结点值。

我们也可以这么理解:我们使用中序遍历6。8结点是不是应该在66结点的后面一个。

即以中序遍历的方式,要删除的结点的右子树部分上的最小结点值在待删除结点的后一个!

下面给出图示:

步骤1:找到要删除的结点的右子树部分上的最小结点
在这里插入图片描述
同时要使用指针minParent指向min指针的父亲结点
在这里插入图片描述
步骤2:覆盖操作(66 变 68)
在这里插入图片描述
步骤3:删除多余的(68)
在这里插入图片描述
代码如下:

else if(curr.left != null && curr.right != null){//左右子树都有
            //1.找到curr右子树的最小值结点
            TreeNode min = curr.right;//min用于寻找右子树的最小值
            TreeNode minParent = curr;//min的父亲结点
            while(min.left != null){
                minParent = min;
                min = min.left;
            }
            //2.覆盖操作
            curr.data = min.data;
            //3.删除多余结点
            minParent.left = null;
        }
5.3.5删除结点代码汇总

是不是看着还挺头疼的!

 //优化前:
 public void remove(E e){
        if(root == null)    return;
        TreeNode curr = root;
        TreeNode parent = null;

        //找到要删除的结点
        while(curr != null && e.compareTo(curr.data) != 0){
            parent = curr;
            if(e.compareTo(curr.data) < 0){
                curr = curr.left;
            }else{
                curr = curr.right;
            }
        }
        //跳出while循环表示:curr==null 或者找到了该结点

        //如果没有找到要删除的结点,直接返回
        if(curr == null)    return;

        if(curr.left == null && curr.right == null){//叶子结点

            if(parent == null){//删除根节点
                root = null;
            }else{
                if(curr == parent.left){
                    parent.left = null;
                }else if(curr == parent.right){
                    parent.right = null;
                }
            }

        }else if(curr.left != null && curr.right == null){//只有左子树

            if(parent == null){//删除根节点
                root = root.left;
            }else{
                if(curr == parent.left){
                    parent.left = curr.left;
                    curr.left = null;
                }else if(curr == parent.right){
                    parent.right = curr.left;
                    curr.left = null;
                }
            }

        }else if(curr.left == null && curr.right != null){//只有右子树
            if(parent == null){
                root = root.right;
            }else if(curr == parent.left){
                parent.left = curr.right;
                curr.right = null;
            }else if(curr == parent.right){
                parent.right = curr.right;
                curr.right = null;
            }

        }else if(curr.left != null && curr.right != null){//左右子树都有
            //1.找到curr右子树的最小值结点
            TreeNode min = curr.right;//min用于寻找右子树的最小值
            TreeNode minParent = curr;//min的父亲结点
            while(min.left != null){
                minParent = min;
                min = min.left;
            }
            //2.覆盖操作
            curr.data = min.data;
            //3.删除多余结点
            minParent.left = null;
        }
    }

 //版本2:不好理解
 public void remove(E e){
        if(root == null)    return;
        TreeNode curr = root;
        TreeNode parent = null;

        //找到要删除的结点
        while(curr != null && e.compareTo(curr.data) != 0){
            parent = curr;
            if(e.compareTo(curr.data) < 0){
                curr = curr.left;
            }else{
                curr = curr.right;
            }
        }
        //跳出while循环表示:curr==null 或者找到了该结点

        //如果没有找到要删除的结点,直接返回
        if(curr == null)    return;
        
        //我们把情况4(删除既有左子树又有左子树的结点)这个问题转化成情况1(删除叶子结点)
        if(curr.left != null && curr.right != null){
            //1.找到curr右子树的最小值结点
            TreeNode min = curr.right;//min用于寻找右子树的最小值
            TreeNode minParent = curr;//min的父亲结点
            while(min.left != null){
                minParent = min;
                min = min.left;
            }
            //2.覆盖操作
            curr.data = min.data;
            //3.curr此时指向叶子结点,待删除中。。
            curr = min;
            parent = minParent;
        }
        
        TreeNode child;//用于存储要删除结点的子节点
        if (curr.left != null){
            child = curr.left;
            if(parent != null)  curr.left = null;
        }else if(curr.right != null){
            child = curr.right;
            if(parent != null)  curr.right = null;
        }else{
            child = null;
        } 
        
        //现在我们的问题转化成了:
        // 要删除的结点是 叶子结点 或者 只有一个子树
        if(parent == null){
            root = child;
        }else if(curr == parent.left){
            parent.left = child;
        }else if(curr == parent.right){
            parent.right = child;
        }
        
    }

6.修改(禁用)

我们直接调用查找方法:


//修改结点值    
public void set(E src , E target){
        if(contains(target))    return;
        TreeNode srcNode = find(src);
        srcNode.data = target;
    }
    
public boolean contains(E target){
        return find(target) == null ? false : true;
    }

public TreeNode find(E target){
            //空树直接返回null
            if(root == null)    return null;
            //辅助指针curr用于查找结点
            TreeNode curr = root;
            while (curr != null){
                if(target.compareTo(curr.data) == 0){
                    return curr;//找到了
                }else if(target.compareTo(curr.data) < 0){
                    curr = curr.left;
                }else{
                    curr = curr.right;
                }
            }
            //不存在该结点
            return null;
    }


但是这个方法其实是有问题:它会破坏BST的结构
所以我们一般对BST数据结构不提供修改操作。
要修改可以把原来的删了然后添加新的结点。

7.递归写法

7.1插入元素

  //递归版本
 public void add(E e){
       root = add(root , e);
    }

    //递归写法
    //将结点e插入到以node为根节点的子树中
    //要求插入结点后返回二叉查找树的根节点
    private TreeNode add(TreeNode node , E e){
        if(node == null){
            size++;
            return new TreeNode(e);
        }
        if(e.compareTo(node.data) < 0){
            TreeNode leftRoot = add(node.left, e);
            node.left = leftRoot;
        }else{
            TreeNode rightRoot = add(node.right , e);
            node.right = rightRoot;
        }
        return node;
    }

7.2查找元素

//递归版本
private  TreeNode find(TreeNode node , E target){
        if(node == null)    return null;
        if(target.compareTo(node.data) == 0)    return node;
        else if(target.compareTo(node.data) < 0) return find(node.left , target);
        else    return find(node.right, target);
    }
    

7.3查找元素

//找任意值
    private  TreeNode find(TreeNode node , E target){
        if(node == null)    return null;
        if(target.compareTo(node.data) == 0)    return node;
        else if(target.compareTo(node.data) < 0) return find(node.left , target);
        else    return find(node.right, target);
    }
    
    //找最小值
    public E minValue(){
        if(root == null)    throw  new RuntimeException("tree is null");
        return minValue(root).data;
    }

    private TreeNode minValue(TreeNode node){
        if(node.left == null)   return node;
        return minValue(node.left);
    }

    //找最大值
    public E maxValue(){
        if(root == null)    throw  new RuntimeException("tree is null");
        return maxValue(root).data;
    }

    private TreeNode maxValue(TreeNode node){
        if(node.left == null)   return node;
        return maxValue(node.right);
    }
    

7.4删除元素

//删除最小值结点
 public E removeMin(){
        E res = minValue();
        root = removeMin(root);
        return res;
    }
    
    //删除以node为根节点的子树的最小结点
    //返回删除完最小结点的子树的根节点
    private TreeNode removeMin(TreeNode node){
        if(node.left == null){
            TreeNode rightNode = node.right;
            node.right = null;
            size--;
            return rightNode;
        }
        TreeNode leftRoot = removeMin(node.left);
        node.left = leftRoot;
        return node;
    }
    
    //删除最大值结点
    public E removeMax(){
        E res = maxValue();
        root = removeMax(root);
        return res;
    }
    public TreeNode removeMax(TreeNode node){
        if(node.right == null){
            TreeNode leftNode = node.left;
            node.left = null;
            size--;
            return leftNode;
        }
        TreeNode rightRoot = removeMax(node.right);
        node.right = rightRoot;
        return node;
    }

删除任意节点递归版本

  // 时间复杂度:O(logn)
    public void remove(E e) {
        root = remove(root, e);
    }
    // 在以 node 为根节点的子树中删除节点 e
    // 并且返回删除后的子树的根节点
    private TreeNode remove(TreeNode node, E e) {
        if (node == null) return null;

        if (e.compareTo(node.data) < 0) {
            node.left = remove(node.left, e);
            return node;
        } else if (e.compareTo(node.data) > 0) {
            node.right = remove(node.right, e);
            return node;
        } else {
            // 要删除的节点就是 node
            if (node.left == null) {
                TreeNode rightNode = node.right;
                node.right = null;
                size--;
                return rightNode;
            }

            if (node.right == null) {
                TreeNode leftNode = node.left;
                node.left = null;
                size--;
                return leftNode;
            }

            // node 的 left 和 right 都不为空
            //successor是node的后继结点
            TreeNode successor = minValue(node.right);
			//1,2不能调换位置
            successor.right = removeMin(node.right);//1
            successor.left = node.left;//2

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

总结

删除操作较为复杂,手写二叉树很锻炼我们的思维。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值