赞
踩
如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里
二叉搜索树
Java代码实现
二叉搜索树
,又叫二叉查找树
,二叉排序树
,二分搜索|查找|排序树
。其实都是一样的东西,我们这里就统一一下用词“二叉搜索树”。
二叉树搜索树的定义:
- 首先二叉搜索树也是一棵二叉树
- 二叉搜索树的任意结点A, 其左子树的所有结点的值都小于结点A的值,其右子树的所有结点都大于结点A的值;前提是任意结点A的左右子树不为空
- 二叉搜索树的左右子树也是一棵二叉搜索树
- 二叉搜索树没有值相等的结点
所以总结起来,二叉树搜索树就是一棵,左孩子小,右孩子大,且没有相同大小值的有序树
完全二叉树
的话,查找值的最坏时间复杂度就是O(logn)
, 但是如果这棵二叉搜索树是一棵线性树
(斜树
),那就是O(n)
我们从上图可以看出,左边的树和右边的树,都能满足二叉树搜索树的定义。从代码实现角度来看,如果后续插入的元素都是一致的,想成为左边的树,还是右边的树,完全取决于整颗树的根结点的大小。如果整颗树的倾向是一颗右边的树,那么这棵树的结点就不太平衡,也不利于做二分搜索,当查看一个元素时,时间复杂度会退回成O(n)
所以一颗二叉搜索树的好坏,往往就取决了根结点开头的取值,但是我们又无法左右用户对根结点取什么值。这就是普通二叉搜索树的一个很大的缺陷,容易造成树的倾斜或层级过深。
为了解决二叉搜索树倾斜等因素造成查询效率降低的问题,就此引进了平衡二叉搜索树AVL
和红黑树
等概念
add()
| 递归contains()
| 递归minimum()
、maximum()
| 递归与迭代removeMin()
、removeMax()
| 递归remove()
| 递归如果你想了解二叉树的前中后序遍历以及层次遍历的实现方式,你可以去这里了解
add()
| 递归这里提供了两种实现方式,一种是常规版本
,代码清晰简单易懂,另一种是优化版
,代码简洁高效,但相对不直观易懂; 之后的其他方法实现都是按照优化版的逻辑去实现的
直观常规版本:
/** * 添加一个元素 * * @param data */ public void add(T data) { //如果根结点为空,那么新结点就是根结点 if (this.rootNode == null) { this.nodeCount++; this.rootNode = new TreeNode<>(data); } //不然就递归以rootNode为根结点的二叉搜索树 add(this.rootNode, data); } /** * 插入新元素到以root为根结点的二叉树搜索树 * * @param root * @param data */ private void add(TreeNode<T> root, T data) { //1. 二叉搜索树的结点元素不允许有重复 if (root.data.compareTo(data) == 0) { throw new RuntimeException("二叉搜索树所存储的元素不允许相等,已存在"); } //2. 如果添加的元素大小小于相对根结点的值 if (data.compareTo(root.data) < 0) { //且相对根结点的左孩子是null, 那我们直接将新结点赋予给相对根结点的左孩子,递归退出条件 if (root.lchild == null) { this.nodeCount++; root.lchild = new TreeNode<>(data); return; } //否则继续递归将左孩子作为根结点的子树 add(root.lchild, data); } //3. 如果新增元素的大小大于相对根结点的值 if (data.compareTo(root.data) > 0) { //且相对根结点的右孩子为null,那么我们就将新结点赋予给相对根结点的右孩子,递归退出条件 if (root.rchild == null) { this.nodeCount++; root.rchild = new TreeNode<>(data); return; } //否则继续递归将右孩子作为根结点的子树 add(root.rchild, data); } }
优化版本:
/** * 向二分搜索树添加一个元素 | 递归 * 1. 统一操作,不用对根结点进行额外的判断 * 2. 代码更简洁,只是相对不好理解 * * @param data */ public void add(T data) { this.rootNode = add(this.rootNode, data); } /** * 向根结点为root的二分搜索树添加元素 | 改进型,不需要对根结点做特殊处理 * 1. 递归推出条件是,当递归到空树时,那么新元素就是该空树的根结点 * 2. 每一次的递归返回的结点,都是用于给上层做关联的,即递归是找到新元素要存储的位置,然后将新元素结点返回给父结点去操作。parent.child = newNode * * @param root * @param data * @return */ private TreeNode<T> add(TreeNode<T> root, T data) { //递归退出条件,只要相对根结点为null,即代表目前我是一棵空树,新增元素就要作为我这棵空树的根结点,直接返回新结点给上层操作 if (root == null) { this.nodeCount++; return new TreeNode<>(data); } //不允许存在相同元素 if (data.compareTo(root.data) == 0) { throw new RuntimeException("二叉搜索树所存储的元素不允许相等,已存在"); //如果新增元素小于当前子树的根结点的值,左递归 } else if (data.compareTo(root.data) < 0) { root.lchild = add(root.lchild, data); //如果新增元素大于当前子树的根结点的值,右递归 } else if (data.compareTo(root.data) > 0) { root.rchild = add(root.rchild, data); } //将当前子树的根结点返回出去,让上层做关联 return root; }
区别:
contains()
| 递归这里的实现说白了就是一个二分查找
/** * 二叉树的查询 | 递归 * * @param data * @return */ public boolean contains(T data) { return contains(this.rootNode, data); } /** * 查询以root为根结点的子树 * * @param root * @param data * @return */ private boolean contains(TreeNode<T> root, T data) { //空树是肯定不含有目标元素,直接返回false , 递归退出条件 if (root == null) { return false; } //如果相等,说明找到了,直接返回true if (data.compareTo(root.data) == 0) { return true; //如果当前子树的根结点不是目标元素,判断是当前根结点的左边还是右边 } else if (data.compareTo(root.data) < 0) { return contains(root.lchild, data); } else { return contains(root.rchild, data); } }
minimum()
、maximum()
| 递归与迭代迭代的方式:
/** * 查询二叉搜索树中的最小值 | 迭代 * * @return */ public T minimumWithIterator() { if (this.rootNode == null) { return null; } //找到二叉搜索树最左结点,即最左没有左孩子的结点,不一定是叶子结点 TreeNode<T> node = this.rootNode; while (node.lchild != null) { node = node.lchild; } return node.data; } /** * 查询二叉搜索树中的最大值 | 迭代 * * @return */ public T maximumWithIterator() { if (this.rootNode == null) { return null; } //找到二叉搜索树最右结点,即最右没有右孩子的结点,不一定是叶子结点 TreeNode<T> node = this.rootNode; while (node.rchild != null) { node = node.rchild; } return node.data; }
递归的方式:
/** * 查找二叉搜索树的最小值 | 递归 * * @return */ public T minimumWithRecursion() { if (this.rootNode == null) { return null; } return minimumWithRecursion(this.rootNode).data; } /** * 查找二叉树搜索树的最小值 | 递归 * 1. 每一步就是查找左子树的最小值是什么,如果没有左子树,则最小值就是当前结点所代表的值 * * @param node * @return */ private TreeNode<T> minimumWithRecursion(TreeNode<T> node) { if (node.lchild == null) { return node; } return minimumWithRecursion(node.lchild); } /** * 查找二叉树搜索树的最大值 | 递归 * * @return */ public T maximumWithRecursion() { if (this.rootNode == null) { return null; } return maximumWithRecursion(this.rootNode).data; } /** * 查找二叉树搜索树的最大值 | 递归 * 1. 每一步就是查找右子树的最大值是什么,如果没有右子树,则最大值就是当前结点所代表的值 * * @param node * @return */ private TreeNode<T> maximumWithRecursion(TreeNode<T> node) { if (node.rchild == null) { return node; } return maximumWithRecursion(node.rchild); }
removeMin()
、removeMax()
| 递归要注意的问题:
删除最小值:
/** * 删除二叉树搜索树中的最小值 | 递归 * * @return */ public T removeMin() { if (this.rootNode == null) { return null; } //1. 先获得最小值 T minResult = minimumWithRecursion(); //2. 再删除最小值的结点 this.rootNode = removeMin(this.rootNode); return minResult; } /** * 删除以Node结点为根结点的子树的最小值 | 递归 * 1. 该代码属于优化代码,类似于add()操作,优化的好处就是简洁,但不好理解 * 2. 先判断当前子树的根结点是否有左孩子,如果有,继续递归 * 3. 如果没有左孩子,即找到最小值结点,判断最小值结点是否有右孩子 * 4. 如果没有,直接删除,如果有,让删除结点父结点的左孩子指向删除结点的右孩子 * * @param node * @return */ private TreeNode<T> removeMin(TreeNode<T> node) { //1. 如果当前结点的左孩子为null , 则当前结点就是最小值结点,就是要删除的结点 if (node.lchild == null) { /** * 两种情况: * 1. 无右孩子,直接让上层的结点.lchild = null即可 * 2. 有右孩子,让上层结点.lchild = 删除结点.rchild; * 这里在代码上可以将两种情况进行合并(优化后,可能不好理解),如果右结点为null,返回null给上层结点的左孩子 * 如果不为null,返回删除结点的右孩子给上层结点左孩子 */ TreeNode<T> rNode = node.rchild; node.rchild = null; nodeCount--; return rNode; } //2. 如果当前结点有左孩子,则递归左孩子 node.lchild = removeMin(node.lchild); //3. 返回当前结点 return node; }
删除最大值:
/** * 删除二叉搜索树的最大值 | 递归 * * @return */ public T removeMax() { //1. 获得最大值 T maxResult = maximumWithRecursion(); //2. 删除最大值结点 this.rootNode = removeMax(this.rootNode); return maxResult; } /** * 删除二叉搜书树的最大值结点 | 递归 * * @param node * @return */ private TreeNode<T> removeMax(TreeNode<T> node) { if (node.rchild == null) { TreeNode<T> lNode = node.lchild; node.lchild = null; nodeCount--; return lNode; } node.rchild = removeMax(node.rchild); return node; }
remove()
| 递归要注意的问题:
Hibbard Deletion
方法Hibbard Deletion
方法:
为了解决删除结点即有左孩子,又有右孩子的情况,我们就需要给删除结点找到一个替代结点
这个替代结点,可以是删除结点的 后继结点
, 也可以是前趋结点
因为删除结点的替代结点可以是前趋结点
,也可以是后继节点
,所以我们的代码中采用后继结点
去代替删除结点
代码:
/** * 二叉搜索树删除目标值结点(任意结点) * 1. 删除结点没有孩子的情况 * 2. 删除结点只有一个孩子的情况 * 3. 删除结点有两个孩子的情况 * 4. 删除结点的替代结点可以是前趋结点,也可以是后继节点,我们这里采用后继结点 * * @param data */ public void remove(T data) { this.rootNode = remove(data, this.rootNode); } /** * 二叉搜索删除目标结点 * * @param data * @param node * @return */ private TreeNode<T> remove(T data, TreeNode<T> node) { //如果结点为null, 直接返回null,代表没有找到目标值结点 if (node == null) { return null; } //二分查找递归,目标是找到目标值结点 if (data.compareTo(node.data) < 0) { node.lchild = remove(data, node.lchild); return node; } else if (data.compareTo(node.data) > 0) { node.rchild = remove(data, node.rchild); return node; } else { //data == node.data,找到删除结点 //(1) 如果删除结点没有孩子,直接删除,爽快,其实这个步骤可以省略,因为下面的做法已经囊括了,但是为了直观 if (node.lchild == null && node.rchild == null) { nodeCount--; return null; } else if (node.lchild == null) { //(2) 如果删除结点没有左孩子 | 等价删除结点只有右孩子 TreeNode<T> rNode = node.rchild; nodeCount--; node.rchild = null; return rNode; } else if (node.rchild == null) { //(3) 如果删除结点没有右孩子 | 等价删除结点只有左孩子 TreeNode<T> lNode = node.lchild; nodeCount--; node.lchild = null; return lNode; } else { /** * (4) 如果删除结点左右孩子都不为空,Hibbard Deletion * 1. 找到删除结点右子树的最小值结点(删除结点的后继结点) minimum(node.rchild) ,作为删除结点的替代结点 * 2. 删除结点的后继结点的左右指针替换成删除结点的左右指针的指向 * 3. 剔除删除结点 * 4. 返回替代结点给上层,重新关联 */ //获得后继结点,将后继结点作为删除结点的代替结点 TreeNode<T> successorNode = minimumWithRecursion(node.rchild); //后继结点的左指针指向删除结点的左指针 successorNode.lchild = node.lchild; //后继结点的右指针指向删除结点的右指针 //为什么removeMin, 因为后继结点要从删除结点的右子树最小结点位置移动到当前删除结点的位置,先删除,再移动 //这里因为涉及到nodeCount的问题,为什么这里不nodeCount--,是因为removeMin中已经nodeCount--了,曲线做了,因为不直观,容易误导 successorNode.rchild = removeMin(node.rchild); //释放删除结点的左右孩子 node.lchild = null; node.rchild = null; return successorNode; } }
TreeNode.java
/** * 二叉搜索树的结点构造 * 跟其他普通的二叉树一样 * * @param <E> */ public class TreeNode<E extends Comparable<T>> { public E data; public TreeNode<E> lchild, rchild; public TreeNode(E data) { this.data = data; this.lchild = null; this.rchild = null; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof TreeNode)) return false; TreeNode<?> treeNode = (TreeNode<?>) o; if (data != null ? !data.equals(treeNode.data) : treeNode.data != null) return false; if (lchild != null ? !lchild.equals(treeNode.lchild) : treeNode.lchild != null) return false; return rchild != null ? rchild.equals(treeNode.rchild) : treeNode.rchild == null; } @Override public int hashCode() { int result = data != null ? data.hashCode() : 0; result = 31 * result + (lchild != null ? lchild.hashCode() : 0); result = 31 * result + (rchild != null ? rchild.hashCode() : 0); return result; } }
BinarySearchTree.java
/** * 二叉搜索树 * * @author liwenjie */ public class BinarySearchTree<T extends Comparable<T>> /** * 二叉搜索树根结点 */ private TreeNode<T> rootNode; /** * 二叉搜索树的结点个数 */ private int nodeCount; public BinarySearchTree(T data) { this.rootNode = new TreeNode<>(data); this.nodeCount = 1; } /** * 向二分搜索树添加一个元素 | 递归 * 1. 统一操作,不用对根结点进行额外的判断 * 2. 代码更简洁,只是相对不好理解 * * @param data */ public void add(T data) { this.rootNode = add(this.rootNode, data); } /** * 向根结点为root的二分搜索树添加元素 | 改进型,不需要对根结点做特殊处理 * 1. 递归推出条件是,当递归到空树时,那么新元素就是该空树的根结点 * 2. 每一次的递归返回的结点,都是用于给上层做关联的,即递归是找到新元素要存储的位置,然后将新元素结点返回给父结点去操作。parent.child = newNode * * @param root * @param data * @return */ private TreeNode<T> add(TreeNode<T> root, T data) { //递归退出条件,只要相对根结点为null,即代表目前我是一棵空树,新增元素就要作为我这棵空树的根结点,直接返回新结点给上层操作 if (root == null) { this.nodeCount++; return new TreeNode<>(data); } //不允许存在相同元素 if (data.compareTo(root.data) == 0) { throw new RuntimeException("二叉搜索树所存储的元素不允许相等,已存在"); //如果新增元素小于当前子树的根结点的值,左递归 } else if (data.compareTo(root.data) < 0) { root.lchild = add(root.lchild, data); //如果新增元素大于当前子树的根结点的值,右递归 } else if (data.compareTo(root.data) > 0) { root.rchild = add(root.rchild, data); } //将当前子树的根结点返回出去,让上层做关联 return root; } /** * 二叉树的查询 | 递归 * * @param data * @return */ public boolean contains(T data) { return contains(this.rootNode, data); } /** * 查询以root为根结点的子树 * * @param root * @param data * @return */ private boolean contains(TreeNode<T> root, T data) { //空树是肯定不含有目标元素,直接返回false , 递归退出条件 if (root == null) { return false; } //如果相等,说明找到了,直接返回true if (data.compareTo(root.data) == 0) { return true; //如果当前子树的根结点不是目标元素,判断是当前根结点的左边还是右边 } else if (data.compareTo(root.data) < 0) { return contains(root.lchild, data); } else { return contains(root.rchild, data); } } /** * 查询二叉搜索树中的最小值 | 迭代 * * @return */ public T minimumWithIterator() { if (this.rootNode == null) { return null; } //找到二叉搜索树最左结点,即最左没有左孩子的结点,不一定是叶子结点 TreeNode<T> node = this.rootNode; while (node.lchild != null) { node = node.lchild; } return node.data; } /** * 查询二叉搜索树中的最大值 | 迭代 * * @return */ public T maximumWithIterator() { if (this.rootNode == null) { return null; } //找到二叉搜索树最右结点,即最右没有右孩子的结点,不一定是叶子结点 TreeNode<T> node = this.rootNode; while (node.rchild != null) { node = node.rchild; } return node.data; } /** * 查找二叉搜索树的最小值 | 递归 * * @return */ public T minimumWithRecursion() { if (this.rootNode == null) { return null; } return minimumWithRecursion(this.rootNode).data; } /** * 查找二叉树搜索树的最小值 | 递归 * 1. 每一步就是查找左子树的最小值是什么,如果没有左子树,则最小值就是当前结点所代表的值 * * @param node * @return */ private TreeNode<T> minimumWithRecursion(TreeNode<T> node) { if (node.lchild == null) { return node; } return minimumWithRecursion(node.lchild); } /** * 查找二叉树搜索树的最大值 | 递归 * * @return */ public T maximumWithRecursion() { if (this.rootNode == null) { return null; } return maximumWithRecursion(this.rootNode).data; } /** * 查找二叉树搜索树的最大值 | 递归 * 1. 每一步就是查找右子树的最大值是什么,如果没有右子树,则最大值就是当前结点所代表的值 * * @param node * @return */ private TreeNode<T> maximumWithRecursion(TreeNode<T> node) { if (node.rchild == null) { return node; } return maximumWithRecursion(node.rchild); } /** * 删除二叉树搜索树中的最小值 | 递归 * * @return */ public T removeMin() { if (this.rootNode == null) { return null; } //1. 先获得最小值 T minResult = minimumWithRecursion(); //2. 再删除最小值的结点 this.rootNode = removeMin(this.rootNode); return minResult; } /** * 删除以Node结点为根结点的子树的最小值 | 递归 * 1. 该代码属于优化代码,类似于add()操作,优化的好处就是简洁,但不好理解 * 2. 先判断当前子树的根结点是否有左孩子,如果有,继续递归 * 3. 如果没有左孩子,即找到最小值结点,判断最小值结点是否有右孩子 * 4. 如果没有,直接删除,如果有,让删除结点父结点的左孩子指向删除结点的右孩子 * * @param node * @return */ private TreeNode<T> removeMin(TreeNode<T> node) { //1. 如果当前结点的左孩子为null , 则当前结点就是最小值结点,就是要删除的结点 if (node.lchild == null) { /** * 两种情况: * 1. 无右孩子,直接让上层的结点.lchild = null即可 * 2. 有右孩子,让上层结点.lchild = 删除结点.rchild; * 这里在代码上可以将两种情况进行合并(优化后,可能不好理解),如果右结点为null,返回null给上层结点的左孩子 * 如果不为null,返回删除结点的右孩子给上层结点左孩子 */ TreeNode<T> rNode = node.rchild; node.rchild = null; nodeCount--; return rNode; } //2. 如果当前结点有左孩子,则递归左孩子 node.lchild = removeMin(node.lchild); //3. 返回当前结点 return node; } /** * 删除二叉搜索树的最大值 | 递归 * * @return */ public T removeMax() { //1. 获得最大值 T maxResult = maximumWithRecursion(); //2. 删除最大值结点 this.rootNode = removeMax(this.rootNode); return maxResult; } /** * 删除二叉搜书树的最大值结点 | 递归 * * @param node * @return */ private TreeNode<T> removeMax(TreeNode<T> node) { if (node.rchild == null) { TreeNode<T> lNode = node.lchild; node.lchild = null; nodeCount--; return lNode; } node.rchild = removeMax(node.rchild); return node; } /** * 二叉搜索树删除目标值结点(任意结点) * 1. 删除结点没有孩子的情况 * 2. 删除结点只有一个孩子的情况 * 3. 删除结点有两个孩子的情况 * 4. 删除结点的替代结点可以是前趋结点,也可以是后继节点,我们这里采用后继结点 * * @param data */ public void remove(T data) { this.rootNode = remove(data, this.rootNode); } /** * 二叉搜索删除目标结点 * * @param data * @param node * @return */ private TreeNode<T> remove(T data, TreeNode<T> node) { //如果结点为null, 直接返回null,代表没有找到目标值结点 if (node == null) { return null; } //二分查找递归,目标是找到目标值结点 if (data.compareTo(node.data) < 0) { node.lchild = remove(data, node.lchild); return node; } else if (data.compareTo(node.data) > 0) { node.rchild = remove(data, node.rchild); return node; } else { //data == node.data,找到删除结点 //(1) 如果删除结点没有孩子,直接删除,爽快,其实这个步骤可以省略,因为下面的做法已经囊括了,但是为了直观 if (node.lchild == null && node.rchild == null) { nodeCount--; return null; } else if (node.lchild == null) { //(2) 如果删除结点没有左孩子 | 等价删除结点只有右孩子 TreeNode<T> rNode = node.rchild; nodeCount--; node.rchild = null; return rNode; } else if (node.rchild == null) { //(3) 如果删除结点没有右孩子 | 等价删除结点只有左孩子 TreeNode<T> lNode = node.lchild; nodeCount--; node.lchild = null; return lNode; } else { /** * (4) 如果删除结点左右孩子都不为空,Hibbard Deletion * 1. 找到删除结点右子树的最小值结点(删除结点的后继结点) minimum(node.rchild) ,作为删除结点的替代结点 * 2. 删除结点的后继结点的左右指针替换成删除结点的左右指针的指向 * 3. 剔除删除结点 * 4. 返回替代结点给上层,重新关联 */ //获得后继结点,将后继结点作为删除结点的代替结点 TreeNode<T> successorNode = minimumWithRecursion(node.rchild); //后继结点的左指针指向删除结点的左指针 successorNode.lchild = node.lchild; //后继结点的右指针指向删除结点的右指针 //为什么removeMin, 因为后继结点要从删除结点的右子树最小结点位置移动到当前删除结点的位置,先删除,再移动 //这里因为涉及到nodeCount的问题,为什么这里不nodeCount--,是因为removeMin中已经nodeCount--了,曲线做了,因为不直观,容易误导 successorNode.rchild = removeMin(node.rchild); //释放删除结点的左右孩子 node.lchild = null; node.rchild = null; return successorNode; } } public static void main(String[] args) { BinarySearchTree<Integer> bsTree = new BinarySearchTree<>(5); bsTree.add(1); bsTree.add(4); bsTree.add(9); bsTree.add(7); System.out.println(bsTree.contains(9)); System.out.println(bsTree.minimumWithRecursion()); System.out.println(bsTree.maximumWithRecursion()); bsTree.removeMin(); bsTree.removeMax(); bsTree.add(10); }
要注意的地方就是, 二叉搜索树的结点的元素必须可以比较,所以无论是结点的泛型还是二叉树结构本身的泛型都必须是一个实现了Comparable接口的元素。不能比较的元素是无法构造成一棵搜索树的
RongleXie/Play-with-Data-Structures-Ronglexie - @作者:RongleXie
如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。