二叉搜索树
概念
二叉搜索树(BST)满足的性质:
每个节点中的值必须大于(或等于)存储在其左子树中的任何值。 每个节点中的值必须小于(或等于)存储在其右子树中的任何值。
一个二叉搜索树的例子如下所示:
二叉搜索树的操作—查找
核心思想
二叉搜索树的操作—插入
核心思想
1:此时我们想向如下的二叉树插入一个元素:
2:那么我们就需要定义一个cur变量指向我们二叉树的根节点,然后定义一个空的指针parent
3:每次插入的时候判断我们所要插入的元素与我们当前cur所指向的元素的大小,如果比我们cur所指向的元素大,那么先让parent指向我们的cur,然后让cur指向我们的cur.right,如果比我们cur所指向的元素小,那么先让parent指向我们的cur,然后让cur指向我们的cur.left
4:最终我们的cur一定会等于null,因为此时我们的cur一定会走到这一步,那么此时当cur等于null的时候,就要判断我们所想要插入的元素的位置了,如果所要插入的元素比我们此时parent指针所指向的元素大,就将这个元素插入到parent.right,否则插入到parent.left.
二叉搜索树的操作—删除(难点)
核心思想
设待删除结点为 cur, 待删除结点的双亲结点为 parent
二叉搜索树的删除分为以下几种大情况,每个大情况下又有几种小情况.
1. cur.left == null
(1) cur 是 root,则 root = cur.right
假设此时要删除的是cur所指向的5这个节点,因为cur的左子树为null,所以直接让根节点指向cur的右子树即可.即root =cur.right
(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
此时我们的cur是parent.left,并不是root,并且cur的左子树为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的3这个数字的话,可直接让parent.left = cur.right即可
(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
此时我们的cur是root.right,并不是root,并且cur的左子树为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的7这个数字的话,可直接让parent.right=cur.right即可
cur.right == null
(1) cur 是 root,则 root = cur.left
假设此时要删除的是cur所指向的5这个节点,因为cur的右子树为null,所以直接让根节点指向cur的左子树即可.即root=cur.left
(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
此时我们的cur是root.left,并不是root,并且cur的右子树为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的3这个数字的话,可直接让parent.left= cur.left即可
(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.left
此时我们的cur是parent.right,并不是root,并且cur的左子树不为空,parent此时也指向我们cur的父节点,此时若要删除我们的cur所指向的7这个数字的话,可直接让parent.right=cur.left即可
cur.left != null && cur.right != null
此时我们的cur指向所要删除的点,也就是我们的20,parent指向当前cur的父亲节点。那么问题来了:
当我们将20删除后,到底放哪个点到cur这个位置呢?
我们发现可以放的点有两个:一个是12,一个是25,这两个分别是20左子树中最大的值,25是20右子树中最小的值
所以我们总结出来就是当我们需要找将谁放上去的时候,在当前需要删除的节点的左边找最大的,在右边找最小的.
而左边最大的值一定在左子树的最右边,右边最小的值一定在右子树的最左边
此时还有一个问题:当我们像之前那样删除,那么一个节点就会出现两个父亲节点,例如我们将20删除掉后,此时假设12顶替了20,那么12既有一个父亲是9这个节点,顶替了20后,还会有一个新的父亲也就是我们parent所指向的8这个节点.此时该怎么办呢?
答:
此时我们采取替罪羊的删除方式
当我们在右子树找最左边的元素的时候,同样也有两种情况:
情况1:还是先来看刚才的图:
我们现在找到25就是我们想要替换到20处的点,所以此时我们就让25来当这个替罪羊:
首先将25替换到20处,注意此时如果我们的25是最终要替换到20的地方,就说明25这个点的左子树一定为null,因为我们25在20的右子树,需要替换20的元素一定是20的右子树中最左边的元素,很显然,假如25还有左子树,去替换20的一定是25的左子树了,哪还能轮得到我们的25自己呢,但是25这个节点的右子树就未必是空了,所以最终我们可以将25的右子树作为我们29的左子树
当然还要注意的是我们再将25的右子树作为我们29的左子树的时候,需要获取到25的父节点29的位置,我们将替罪羊25命名为target,将替罪羊的父亲命名为tp(targetParent).
情况2:
当然也有可能我们20的右子树中是没有左子树的,如下图所示:
此时需要去替代20的就变成了我们的替罪羊29,替换完成后再将右子树指向29原先的右子树.
所有操作代码综合
注意此处我们拿内部类来实现我们的代码:
class BinarySearchTree { static class BSNode { public int val; public BSNode left; public BSNode right; public BSNode(int val) { this.val = val; } } public BSNode root = null; public BSNode search(int val) { if (root == null) { return null; } BSNode cur = root; while (cur != null) { if (cur.val == val) { return cur; } else if (cur.val < val) { cur = cur.right; } else { cur = cur.left; } } return null; } public boolean insert(int val) { BSNode bsNode = new BSNode(val); if (root == null) { root = bsNode; return true; } BSNode cur = root; BSNode parent = null; while (cur != null) { //注意我们在二叉搜索树中插入的元素不能与已知二叉树的元素相同. if (cur.val == val) { return false; } else if (cur.val < val) { parent = cur; cur = cur.right; } else { parent = cur; cur = cur.left; } } //这块是当cur为null的时候,此时就要插入我们的元素了 if (parent.val < val) { parent.right = bsNode; } else { parent.left = bsNode; } return true; } public void remove(int val) { //假设整棵二叉搜索树都为空的话,直接结束 if (root == null) { return; } //使用cur来遍历寻找我们所要删除的节点 BSNode cur = root; //父亲节点parent BSNode parent = null; while (cur != null) { if (cur.val == val) { //此时我们使用removeNode方法来进行删除操作,这样的模块化设计代码看起来更加清晰,并且二叉搜索树的删除操作比较繁琐,需要一个单独的方法 removeNode(parent, cur, val); //删除完成直接return走人 return; } else if (cur.val < val) { //让parent去记录每个节点的父节点 parent = cur; //小于val就说明我们所要删除的值在我们当前节点的右边 cur = cur.right; } else { //让parent去记录每个节点的父节点 parent = cur; 大于val就说明我们所要删除的值在我们当前节点的左边 cur = cur.left; } } } /** * 这个方法中的参数第一个代表当前所有需要删除节点的父类,第二个是需要删除的节点,第三个是要删除的节点的值 * * @param parent * @param cur * @param val */ private void removeNode(BSNode parent, BSNode cur, int val) { //第一种大情况:cur.left = null if (cur.left == null) { if (cur == root) { root = cur.right; } else if (parent.left == cur) { parent.left = cur.right; } else { parent.right = cur.right; } //第二种大情况:cur.right = null } else if (cur.right == null) { if (cur == root) { root = cur.left; } else if (parent.left == cur) { parent.left = cur.left; } else { parent.right = cur.left; } } else { //这块的else就是第三种情况了:cur.left != null && cur.right != null //首先让我们替罪羊的父亲指向cur BSNode targetParent = cur; //因为我们想让即将被删除的节点的右子树中的最小值作为替罪羊,所以此处我们就让替罪羊先指向右子树 BSNode target = cur.right; //开始让下遍历寻找右子树中的最小值 while (target.left != null) { //每走一步之前想让父亲节点指向我们target所在的位置 targetParent = target; //然后让target一直往左下走 target = target.left; } //走到这里就代表target.left = null了,说明target此时指向的节点就是 右边的最小值 //然后将target所指向的值赋给我们的cur,也就是替换掉当前要删除掉的值 cur.val = target.val; //下面就是分的那两种情况 //第一种情况是我们要删除节点的右子树含有左子树 if (target == targetParent.left) { targetParent.left = target.right; //下面的是我们所删除的节点的右子树不含有左子树的时候 } else { targetParent.right = target.right; } } } } public class TestDemo { //注意这里的传参方式 public static void preOrder(BinarySearchTree.BSNode root) { if (root == null) { return; } System.out.print(root.val + " "); preOrder(root.left); preOrder(root.right); } public static void inOrder(BinarySearchTree.BSNode root) { if (root == null) { return; } inOrder(root.left); System.out.print(root.val + " "); inOrder(root.right); } public static void main(String[] args) { /* 以下代码是对我们二叉搜索树的插入,删除,搜索操作进行测试的 */ System.out.println("插入操作测试...................................."); BinarySearchTree binarySearchTree = new BinarySearchTree(); binarySearchTree.insert(4); binarySearchTree.insert(3); binarySearchTree.insert(1); binarySearchTree.insert(15); binarySearchTree.insert(11); //对我们的二叉搜索树进行前序遍历 System.out.println("前序遍历结果为:"); preOrder(binarySearchTree.root); System.out.println(); //对我们的二叉搜索树进行中序遍历 System.out.println("中序遍历结果为:"); inOrder(binarySearchTree.root); System.out.println(); System.out.println("查找操作测试......................................"); try { BinarySearchTree.BSNode ret = binarySearchTree.search(4); //如果搜索到了当前的4这个节点,就直接输出 System.out.println("找到"+ret.val+"这个节点啦"); } catch (NullPointerException e) { //如果没有找到这个节点,就输出没有找到的话 System.out.println("没有找到当前的节点............"); e.printStackTrace(); } System.out.println("删除操作测试...................................."); binarySearchTree.remove(15); //对我们的二叉搜索树进行前序遍历 System.out.println("前序遍历结果为:"); preOrder(binarySearchTree.root); System.out.println(); //对我们的二叉搜索树进行中序遍历 System.out.println("中序遍历结果为:"); inOrder(binarySearchTree.root); System.out.println(); } }
性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为: log2N
也就是二叉树的高度,因为在二叉搜索树中查找相当于是二分查找,大了去右子树,小了去左子树,查找的效率与二叉树的高度有关.例如查找8的时间复杂度为O(log2N)
最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N/2
例如查找8的时间复杂度最好为O(1),也就是一开始就找到了,最坏为O(N)
和 java 类集的关系
TreeMap 和 TreeSet 即 java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容后序再进行讲解。
练习题
1:二叉搜索树转换成排序双向链表 OJ链接
2:验证是否为二叉搜索树 OJ链接