1. 概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
- 不允许存在相同的结点
如:一个int a [] = {5,3,4,1,7,8,2,6,0,9}; 数组组成二叉排序树
定义二叉搜索树的结构:
static class TreeNode {public int val;public TreeNode left;public TreeNode right;public TreeNode(int val) {this.val = val;}}public TreeNode root;
2. 操作 - 查找
/*搜索搜索的时间复杂度最好:O(logN)搜索的时间复杂度最坏:O(N)*/public boolean search(int key){TreeNode cur = root;while(cur != null){//找到key,返回trueif(cur.val == key) {return true;}//到右树寻找else if(cur.val < key){cur = cur.right;}//到左树寻找else{cur = cur.left;}}//没有找到值为key的结点return false;}
3. 操作 - 插入
(1) 如果树为空树,即根 == null,直接插入
(2)如果树不是空树,按照查找逻辑确定插入位置,插入新结点
/*插入:都是插入到叶子结点插入的时间复杂度最好:O(logN)插入的时间复杂度最坏:O(N)*/public void insert(int val) throws Exception {TreeNode newNode = new TreeNode(val);if (root == null) {root = newNode;return;}TreeNode cur = root;TreeNode parent = null;while (cur != null) {parent = cur;//判断是否有重复元素进入if (cur.val == val) {throw new Exception("有重复元素进入!");}//如果要插入的结点的值,大于当前结点,就应该在cur的右子树if (cur.val < val) {cur = cur.right;}//如果要插入的结点的值,小于当前结点,就应该在cur的左子树else if (cur.val > val) {cur = cur.left;}}//如果要插入的结点小于父节点,就应该接在父节点的左子树if (parent.val > val) {parent.left = newNode;}//如果要插入的结点大于父节点,就应该接在父节点的右子树else {parent.right = newNode;}}
搜索和插入的运行结果:
4. 操作 - 删除 ⭐⭐⭐⭐⭐
分情况讨论,如下所示:
设待删除结点为 cur, 待删除结点的双亲结点为 parent
(1)cur.left == null
① cur 是 root,则 root = cur.right
② cur 不是 root,cur 是 parent.left,则 parent.left = cur.right
③ cur 不是 root,cur 是 parent.right,则 parent.right = cur.right
(2)cur.right == null
① cur 是 root,则 root = cur.left
② cur 不是 root,cur 是 parent.left,则 parent.left = cur.left
③ cur 不是 root,cur 是 parent.right,则 parent.right = cur.left
(3)cur.left != null && cur.right != null
需要使用替换法进行删除,即在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被 删除节点中,再来处理该结点的删除问题。
这部分的代码为(有bug):
//3.cur两边的子树都不为空else{TreeNode t = cur.left;TreeNode tp = null;while(t.right != null){tp = t;t = t.right;}cur.val = t.val;//这个就变成了删除t指向的结点,且t结点的右子树为空tp.right = t.left;}
原来的图:
第一步进行分析和改进:
第二步进行分析和改进:
树类的方法:
//删除结点的操作public void remove(int val) {TreeNode parent = null;TreeNode cur = root;while (cur != null) {//去右子树寻找if (cur.val < val) {parent = cur;cur = cur.right;} else if (cur.val > val) {parent = cur;cur = cur.left;}//找到了else {removeNode(cur, parent);return;}}}private void removeNode(TreeNode cur, TreeNode parent) {//1.cur的左子树为空if (cur.left == null) {//(1)cur 是 root,则 root = cur.rightif (cur == root) {root = cur.right;}//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.rightelse if (cur == parent.left) {parent.left = cur.right;}//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.rightelse {parent.right = cur.right;}}//2.cur的右子树为空else if (cur.right == null) {//(1)cur 是 root,则 root = cur.leftif (cur == root) {root = cur.left;}//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.leftelse if (cur == parent.left) {parent.left = cur.right;}//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.leftelse {parent.right = cur.right;}}//3.cur两边的子树都不为空else {TreeNode t = cur.left;TreeNode tp = cur;while (t.right != null) {tp = t;t = t.right;}cur.val = t.val;if (cur == tp) {tp.left = t.left;} else {//这个就变成了删除t指向的结点,且t结点的右子树为空tp.right = t.left;}}}
测试类:
public class Test {public static void main(String[] args) throws Exception {BinarySearchTree binarySearchTree = new BinarySearchTree();binarySearchTree.insert(5);binarySearchTree.insert(3);binarySearchTree.insert(7);binarySearchTree.insert(1);binarySearchTree.insert(4);binarySearchTree.insert(6);binarySearchTree.insert(8);binarySearchTree.insert(0);binarySearchTree.insert(9);binarySearchTree.remove(3);}
}
5. 二叉搜索树的完整代码
public class BinarySearchTree {static class TreeNode {public int val;public TreeNode left;public TreeNode right;public TreeNode(int val) {this.val = val;}}public TreeNode root;/*搜索搜索的时间复杂度最好:O(logN)搜索的时间复杂度最坏:O(N)*/public boolean search(int key) {TreeNode cur = root;while (cur != null) {//找到key,返回trueif (cur.val == key) {return true;}//到右树寻找else if (cur.val < key) {cur = cur.right;}//到左树寻找else {cur = cur.left;}}//没有找到值为key的结点return false;}/*插入:都是插入到叶子结点插入的时间复杂度最好:O(logN)插入的时间复杂度最坏:O(N)*/public void insert(int val) throws Exception {TreeNode newNode = new TreeNode(val);if (root == null) {root = newNode;return;}TreeNode cur = root;TreeNode parent = null;while (cur != null) {parent = cur;//判断是否有重复元素进入if (cur.val == val) {throw new Exception("有重复元素进入!");}//如果要插入的结点的值,大于当前结点,就应该在cur的右子树if (cur.val < val) {cur = cur.right;}//如果要插入的结点的值,小于当前结点,就应该在cur的左子树else if (cur.val > val) {cur = cur.left;}}//如果要插入的结点小于父节点,就应该接在父节点的左子树if (parent.val > val) {parent.left = newNode;}//如果要插入的结点大于父节点,就应该接在父节点的右子树else {parent.right = newNode;}}//删除结点的操作public void remove(int val) {TreeNode parent = null;TreeNode cur = root;while (cur != null) {//去右子树寻找if (cur.val < val) {parent = cur;cur = cur.right;} else if (cur.val > val) {parent = cur;cur = cur.left;}//找到了else {removeNode(cur, parent);return;}}}private void removeNode(TreeNode cur, TreeNode parent) {//1.cur的左子树为空if (cur.left == null) {//(1)cur 是 root,则 root = cur.rightif (cur == root) {root = cur.right;}//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.rightelse if (cur == parent.left) {parent.left = cur.right;}//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.rightelse {parent.right = cur.right;}}//2.cur的右子树为空else if (cur.right == null) {//(1)cur 是 root,则 root = cur.leftif (cur == root) {root = cur.left;}//(2)cur 不是 root,cur 是 parent.left,则 parent.left = cur.leftelse if (cur == parent.left) {parent.left = cur.right;}//(3)cur 不是 root,cur 是 parent.right,则 parent.right = cur.leftelse {parent.right = cur.right;}}//3.cur两边的子树都不为空else {TreeNode t = cur.left;TreeNode tp = cur;while (t.right != null) {tp = t;t = t.right;}cur.val = t.val;if (cur == tp) {tp.left = t.left;} else {//这个就变成了删除t指向的结点,且t结点的右子树为空tp.right = t.left;}}}}
6. 性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。 对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度 的函数,即结点越深,则比较次数越多。 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
- 最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:
- 最差情况下,二叉搜索树退化为单支树,其平均比较次数为:N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以,是二叉搜索树的性能最佳?
答:可以,这就涉及到后面的AVL树和红黑树了,后期的文章会继续讨论AVL树和红黑树。
7. 和Java类集的关系
TreeMap 和 TreeSet 即 Java 中利用搜索树实现的 Map 和 Set;实际上用的是红黑树,而红黑树是一棵近似平衡的二叉搜索树,即在二叉搜索树的基础之上 + 颜色以及红黑树性质验证,关于红黑树的内容后序再进行讲解。