数据结构之——二叉搜索树

一、基本概念

二叉查找树(Binary Search Tree),它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树

 

二、结构

如基本概念所述,二叉查找树有一个左子树,一个右子树,大于该节点的值则处于右边,小于该节点的值则处于左边。

如图:

二叉查找树只有两个节点,一个左一个右,且二叉查找树的的平均运行时间都是:O(lg n)。

 

三、代码(java实现)

3.1 结构定义

/**
 * 二叉搜索树定义
 */
public class BinarySearchTree {

    //根节点
    private Node root;


    /**
     * 树节点定义,静态内部类,每个节点都是一颗子树
     */
    private static class Node {
        //数据
        private int data;
        //右节点
        private Node right;
        //左节点
        private Node left;
        //父节点,用于辅助作用
        private Node parent;


        public Node(int data, Node right, Node left, Node parent) {
            this.data = data;
            this.right = right;
            this.left = left;
            this.parent = parent;
        }
    }
}

节点也就是每一棵树,它有左节点和右节点,以及数据域,和一个父节点,父节点也就是用于辅助,因为增删改查都需要用到父亲。

二叉搜索树只有一个根节点,类似单链表,只需要一个头结点即可,因为头结点连接着所有的节点。

 

3.1 插入操作

insert方法:

    public void insert(int data) {
        //1.构造节点
        Node node = new Node(data,null,null,null);
        //2.判断根节点是否为空
        if(root == null) {
            //为空直接设置为根节点
            root = node;
        }else {
            //设置当前节点为根节点
            Node currentNode = root;
            while (true) {
                //如果值为当前节点值,则结束,不进行操作,因为已经存在该值。
                if(data == currentNode.data) {
                    return;
                }else if(data < currentNode.data) { //如果值小于当前节点值
                        //左节点是否为空
                    if(currentNode.left == null) {
                        //设置为左节点
                        currentNode.left = node;
                        break;
                    }else {
                        //当前节点设置为左节点,继续循环
                        currentNode = currentNode.left;
                    }
                }else{ //如果值大于当前节点值
                    //右节点是否为空
                    if(currentNode.right == null) {
                        //设置为右节点
                        currentNode.right = node;
                        break;
                    }else {
                        //当前节点设置为右子节点,继续循环
                        currentNode = currentNode.right;
                    }
                }
            }
            //设置父节点
            node.parent = currentNode;
        }
    }

插入操作的逻辑也很简单,如图,当需要插入一个35时,从根节点开始判断,如果35大于根节点,即大于25,则判断25的右节点是否为空,如果为空,则设25的右节点为35,而25的右节点不为空,又从30开始判断,35大于30,则应该在右边,而30的右节点为空,所以35的位置则为30的右节点。

插入前:

插入后:

3.2 删除操作

二叉查找树的删除操作是比较复杂的,共分为三种情况,每一种情况的处理方式都不一样。

第一种:当删除的节点没有任何子节点(即叶子结点)。

如:要删除35时,35没有任何的左右节点,直接把其删除即可,把30的右节点设为null即可。

删除前:

删除后:

第二种:当删除的节点有一个子节点。

如:要删除30时,30存在一个右节点35,用节点35替代节点30既可。

删除前:

删除后:

第三种:当删除节点有两个子节点。

如:当要删除30时,30存在一个左节点28和一个右节点35,我们应该寻找节点30的左节点中最大的或者右节点中最小的来替代它,该节点也叫后继节点。则应该用29来替代30或用32来替代30,因为对于30节点的这棵树,30其实就是一个中间值,而左子树中最大的节点,它肯定比30的左节点28大,肯定比30的右节点35小;右子树中最小的节点,肯定也比30的右节点35小,30的左节点的28大,所以可以用来替换30。

我们转换思路,删除有2个子节点的节点时,我们的步骤应该是这样的,先找出后继节点存储值,然后删除后继节点,把后继节点的值设为原来要删除节点的值,即先找出32(本文使用右子树中的最小节点)存储起来,然后删除节点32,最后把节点30的值修改为32即可。

删除前:

删除后:

remove方法:

    public boolean remove(int data) {
        //1.找到要删除的节点
        Node node = find(data);
        //2.如果当前节点是null,则返回false,表示删除失败
        if(node == null) {
            return false;
        }
        //子节点数量
        int nodeNum = hasNodeNum(node);
        //3.判断子节点数量进行对应的删除
        if(0 == nodeNum) {
            deleteNoneNode(node);
        }else if(1 == nodeNum) {
            deleteHasOneNode(node);
        }else {
            deleteTwoNode(node);
        }
        return true;
    }

find方法:

    public Node find(int data) {
        //从根节点开始找
        Node currentNode = root;
        while (currentNode != null) {
            //1.如果值为当前节点,则表示找到
            if(data == currentNode.data) {
                return currentNode;
            }else if(data < currentNode.data) { //2.如果值小于当前节点,则把左节点设为当前节点
                currentNode = currentNode.left;
            }else {//3.如果值大于当前节点,则把右节点设为当前节点
                currentNode = currentNode.right;
            }
        }
        return currentNode;
    }

find方法则为从头开始向下找,找到要删除的节点则返回。

 

hasNodeNum方法:

    private int hasNodeNum(Node node) {
        if(node.left == null && node.right == null) {
            return 0;
        }else if(node.left == null || node.right == null) {
            return 1;
        }else {
            return 2;
        }
    }

hasNodeNum方法判断该节点存在几个子节点。

 

deleteNoneNode方法:

    private void deleteNoneNode(Node node) {
        //如果是根节点,这把根节点设为null即可。
        if(node == root) {
            root = null;
        }else { //不为根节点
            //找到父节点
            Node parent = node.parent;
            //判断要删除的节点是左节点还是右节点
            if(isRight(node)) {
                parent.right = null;
            }else {
                parent.left = null;
            }
        }
    }

deleteNoneNode方法即删除的节点不存在子节点的删除操作,逻辑如上,很简单。

 

isRight方法:

    private boolean isRight(Node node) {
        if (node == null || node.parent == null) {
            return false;
        }
        return node == node.parent.right ? true : false;
    }

isRight方法为判断一个节点是否是右节点。

 

deleteHasOneNode方法:

    private void deleteHasOneNode(Node node) {
        //如果为根节点
        if(node == root) {
            //使用不为null的那个节点替代根节点
            root = (root.right == null) ? root.left : root.right;
        }else { //不是根节点
            Node parent = node.parent;
            //如果是右节点
            if(isRight(node)) {
                //判断要删除的节点存在的是左节点还是右节点,用不为null的那个节点替代要删除的节点
                parent.right = (node.right == null) ? node.left : node.right;
                parent.right.parent = parent;
            }else {
                parent.left = (node.right == null) ? node.left : node.right;
                parent.left.parent = parent;
            }
        }
    }

deleteHasOneNode方法即删除的节点存在一个子节点的删除操作,逻辑如上,直接使用要删除节点下存在的那个节点替代要删除的节点即可。

 

deleteTwoNode方法:

    private void deleteTwoNode(Node node) {
        //找到右子树中最小的
        Node min = findMin(node.right);
        if(1 == hasNodeNum(min)) {
            deleteHasOneNode(min);
        }else {
            deleteNoneNode(min);
        }
        //把替代节点的值设为原来要被删除节点的值
        node.data = min.data;
    }

deleteTwoNode方法即删除的节点存在两个子节点的删除操作,逻辑如上,找到后继节点(本文使用右节点中的最小节点),然后删除后继节点,最后再使用后继节点的值替代要删除节点的值即可。

注意:因为右节点中最小节点只保证找到的该节点没有左节点,并不保证不存在右节点,所以需要判断该后继节点存在几个子节点,然后进行对应的删除操作。

 

findMin方法:

    private Node findMin(Node node) {
        Node current = node;
        while (current.left != null) {
            current = current.left;
        }
        return current;
    }

findMin方法寻找某个节点中最小的值,即找到左节点,如果左节点不存在,则当前节点值为最小。

到此为止,删除操作代码已经写完。

 

3.3 遍历操作

遍历主要分为三种,前序遍历、中序遍历、后序遍历。

第一种:前序遍历

前序遍历是对于每一棵树来说,先打印根节点,再打印左节点,最后打印右节点。

如图:

这是一个前序遍历的打印顺序,代码如下。

    public void preOrder() {
        preOrder(root);
    }

    private void preOrder(Node node) {
        if(node != null) {
            System.out.println(node.data);
            preOrder(node.left);
            preOrder(node.right);
        }
    }

第二种:中序遍历

中序遍历是对于每一棵树来说,先打印左节点,再打印根节点,最后打印右节点。

如图:

这是一个中序遍历的打印顺序,代码如下。

    public void midOrder() {
        midOrder(root);
    }

    private void midOrder(Node node) {
        if(node != null) {
            midOrder(node.left);
            System.out.println(node.data);
            midOrder(node.right);
        }
    }

第三种:后续遍历

后序遍历是对于每一棵树来说,先打印左节点,再打印右节点,最后打印根节点。

如图:

这是一个后序遍历的打印顺序,代码如下。

    public void proOrder() {
        proOrder(root);
    }

    private void proOrder(Node node) {
        if(node != null) {
            proOrder(node.left);
            proOrder(node.right);
            System.out.println(node.data);
        }
    }

对于以上三种遍历方式,通过看代码只需要记忆前序遍历即可,中序遍历是把根节点的打印放到中间,后续遍历是把根节点的打印放到最后。

 

总结:

以上内容就是整个二叉查找树的内容,经过测试代码都是OK的,如果发现问题,请留言,下一篇内容为二叉平衡树(也是java实现,其中包括插入、删除内容的详解讲解)

发布了35 篇原创文章 · 获赞 61 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/m0_37914588/article/details/103752481