数据结构与算法:二叉查找树/排序树/搜索树

本文详细介绍了二叉查找树(又称排序树、搜索树)的特性,包括查找、插入和删除操作。二叉查找树在执行中序遍历时可输出有序数据。查找操作采用类似二分查找的方式,时间复杂度为O(logn)。插入操作的时间复杂度也是O(logn),而删除操作则需要考虑多种情况以保持树的特性。

二叉树的前序遍历,中序遍历,后续遍历的思想,对其应分别对应的介绍了快速排序、汉诺塔、归并排序。二叉树的增删查操作很普通,时间复杂度与链表并没有太多差别。但当二叉树具备一些特性的时候,则可以利用这些特性实现时间复杂度的降低。

二叉查找树/排序树/搜索树

二叉查找树(也称作二叉排序树,二叉搜索树)具备以下几个的特性:

  • 若左子树不为空,那么左子树上面的所有节点的关键字值都比根节点的关键字值小

  • 若右子树不为空,那么右子树上面的所有节点的关键字值都比根节点的关键字值大

  • 左右子树都为二叉树

  • 没有重复值(这一点在实际中可以忽略)

  • 对二叉查找树进行中序遍历,就可以输出一个从小到大的有序数据队列,如下图所示,中序遍历的结果就是 10、13、15、16、20、21、22、26。

在这里插入图片描述

二叉查找树的查找操作

在利用二叉查找树执行查找操作时,我们可以进行以下判断:

  • 首先判断根结点是否等于要查找的数据,如果是就返回。

  • 如果根结点大于要查找的数据,就在左子树中递归执行查找动作,直到叶子结点。

  • 如果根结点小于要查找的数据,就在右子树中递归执行查找动作,直到叶子结点。

这样的“二分查找”所消耗的时间复杂度就可以降低为 O(logn)。关于二分查找,我们会在后续的分治法一讲中详细讲述。

public class BinarySearchTree {
    /**
     * 根结点
     */
    public TreeNode root;

    public class TreeNode{
        int data;
        TreeNode leftChild;
        TreeNode rightChild;
        TreeNode parent;

        public TreeNode(int data) {
            this.data = data;
            this.leftChild = null;
            this.rightChild = null;
            this.parent = null;
        }
    }
    
    /**
     * 查找结点
     */
    public TreeNode searchNode(int data) {
        if (root == null) {
            return null;
        }
        TreeNode node = root;
        while (node != null) {
            if (data > node.data) {
                node = node.rightChild;
            } else if (data < node.data) {
                node = node.leftChild;
            } else {
                return node;
            }
        }
        return null;
    }
    
    /**
     * 中序遍历
     */
    public void midOrderTraverse(TreeNode root) {
        if (root == null) {
            return;
        }
        //LDR
        midOrderTraverse(root.leftChild);
        System.out.print(root.data+" ");
        midOrderTraverse(root.rightChild);
    }
}

测试代码如下:

public class Client {
    @Test
    public void test() {
        //实例化查找二叉树
        BinarySearchTree tree = new BinarySearchTree();
        int[] array = new int[]{15,13,16,10,21,20,22,26};
        for (int i : array) {
            //调用添加结点方法
            tree.put(i);
        }
        //中序遍历tree
        tree.midOrderTraverse(tree.root);
        //10 13 15 16 20 21 22 26 
    }
}
二叉查找树的插入操作

在二叉查找树执行插入操作也很简单。从根结点开始,如果要插入的数据比根结点的数据大,且根结点的右子结点不为空,则在根结点的右子树中继续尝试执行插入操作。直到找到为空的子结点执行插入动作。

如下图所示,如果要插入数据 X 的值为 14,则需要判断 X 与根结点的大小关系:

由于 14 小于 16,则聚焦在其左子树,继续判断 X 与 13 的关系。

由于 14 大于 13,则聚焦在其右子树,继续判断 X 与15 的关系。

由于 14 小于 15,则聚焦在其左子树。

因为此时左子树为空,则直接通过指针建立 15 结点的左指针指向结点 X 的关系,就完成了插入动作。

在这里插入图片描述

代码如下:

/**
 * 插入结点
 */
public TreeNode put(int data) {
    if (root == null) {
        TreeNode node = new TreeNode(data);
        root = node;
        return node;
    }
    TreeNode parent = null;
    TreeNode node = root;
    //找到要插入的位置,循环结束,node为null,parent记录位置,
    while (node != null) {
        parent = node;//记录父结点
        if (data < node.data) {
            node = node.leftChild;
        } else if (data > node.data) {
            node = node.rightChild;
        } else {//重复值就不用处理了(达到去重复值的效果)
            return node;
        }
    }
    //生成一个结点插入到parent后面
    TreeNode newNode = new TreeNode(data);
    if (data < parent.data) {
        parent.leftChild = newNode;
    } else {
        parent.rightChild = newNode;
    }
    newNode.parent = parent;
    return newNode;
}

二叉查找树插入数据的时间复杂度是 O(logn)。但这并不意味着它比普通二叉树要复杂。原因在于这里的时间复杂度更多是消耗在了遍历数据去找到查找位置上,真正执行插入动作的时间复杂度仍然是 O(1)。

二叉查找树的删除操作

二叉查找树的删除操作会比较复杂,这是因为删除完某个结点后的树,仍然要满足二叉查找树的性质。我们分为下面四种情况讨论。

  • 1.要删除的结点是叶子结点
    • 1.1如果要删除的是根结点
    • 1.2要删除的不是根结点

在这里插入图片描述

  • 2.要删除的结点只有左孩子

    • 2.1要删除的是根结点
    • 2.2要删除的不是根结点
      • 2.2.1要删除的结点是父结点的左孩子
      • 2.2.2要删除的结点是父结点的右孩子
  • 3.要删除的结点只有右孩子

    • 3.1要删除的是根结点
    • 3.2要删除的不是根结点
      • 3.2.1要删除的结点是父结点的左孩子
      • 3.2.2要删除的结点是父结点的右孩子
  • 4.要删除的结点有左右孩子

    • 4.1如果被删除结点的右子树的左子树为空,就直接补上右子树

在这里插入图片描述

  • 4.2否则就要补上右子树的左子树上最小的一个结点

在这里插入图片描述

先要找到右子树的左子树最小的结点

private TreeNode getMinLeftTreeNode(TreeNode node) {
    TreeNode curRoot = null;
    if (node == null) {
        return null;
    } else {
        curRoot = node;
        while (curRoot.leftChild != null) {
            curRoot = curRoot.leftChild;
        }
    }
    return curRoot;
}

然后执行步骤1,2,3,4,结果如下

在这里插入图片描述

代码如下:

/**
 * 删除结点
 * 正常的逻辑是在删除操作之前,会先进行查询操作,
 * 只有在树上查到该结点了,才会去执行删除操作
 * 这里为了便于理解,只是编码了单纯的删除逻辑
 */
public void deleteNode(TreeNode node) {
    if (node == null) {
        throw new NoSuchElementException();
    } else {
        //先得到父结点,方便后面的操作
        TreeNode parent = node.parent;
        //1.要删除的结点是叶子结点
        if (node.leftChild == null && node.rightChild == null) {
            //特别的情况:树上只有一个节点或是空树
            if (parent == null) {
                root = null;
            } else if (parent.rightChild == node) {//如果要删除的结点是父结点的右孩子
                parent.rightChild = null;//将父结点的右指针指向null
            } else if (parent.leftChild == node) {//如果要删除的结点是父结点的左孩子
                parent.leftChild = null;//将父结点的左指针指向null
            }
            node.parent = null;//java可以不用这一步,但是C语言需要写,否则内存不会释放
        } else if (node.leftChild != null && node.rightChild == null) {//2.要删除的结点只有左孩子
            if (parent == null) {//如果要删除的是根结点,将要删除结点的左孩子置为根结点
                node.parent = null;
                node.leftChild.parent = null;
                root = node.leftChild;
            } else {
                if (parent.leftChild == node) {//要删除的结点是父亲的左孩子
                    node.leftChild.parent = parent;//
                    parent.leftChild = node.leftChild;
                } else {//要删除的结点是父亲的右边
                    node.leftChild.parent = parent;
                    parent.rightChild = node.leftChild;
                }
                node.parent = null;
            }

        } else if (node.leftChild == null && node.rightChild != null) {//3.要删除的结点只有右孩子
            if (parent == null) {//如果要删除的是根结点,将要删除结点的左孩子置为根结点
                node.parent = null;
                node.rightChild.parent = null;
                root = node.rightChild;
            } else {
                if (parent.leftChild == node) {//要删除的结点是父亲的左孩子
                    node.rightChild.parent = parent;
                    parent.leftChild = node.rightChild;
                } else {//要删除的结点是父亲的右孩子
                    node.rightChild.parent = parent;
                    parent.rightChild = node.rightChild;
                }
                node.parent = null;
            }
        } else {//4.要删除的结点有左右两个孩子
            if (node.rightChild.leftChild == null) {//4.1如果被删除结点的右子树的左子树为空,就直接补上右子树
                node.rightChild.leftChild = node.leftChild;
                if (parent == null) {//如果删除的是根结点
                    root = node.rightChild;
                } else {
                    if (parent.leftChild == node) {//如果删除的结点是父结点的左孩子
                        parent.leftChild = node.rightChild;
                        //
                        node.rightChild.parent = parent;
                    } else {//删除的结点是父结点的右孩子
                        parent.rightChild = node.rightChild;
                        //
                        node.rightChild.parent = parent;
                    }
                }
                node.parent = null;
            } else {//4.2否则就要补上右子树的左子树上最小的一个结点
                TreeNode minLeftTreeNode = getMinLeftTreeNode(node.rightChild);
                //1
                minLeftTreeNode.leftChild = node.leftChild;
                //2
                TreeNode leftNodeP = minLeftTreeNode.parent;
                leftNodeP.leftChild = minLeftTreeNode.rightChild;
                //3
                minLeftTreeNode.rightChild = node.rightChild;
                //4
                if (parent == null) {
                    root = minLeftTreeNode;
                } else {
                    if (parent.leftChild == node) {
                        parent.leftChild = minLeftTreeNode;
                        //
                        minLeftTreeNode.parent = parent;
                    } else {
                        parent.rightChild = minLeftTreeNode;
                        //
                        minLeftTreeNode.parent = parent;
                    }
                }
            }
        }
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值