前言
~~接着实现搜索二叉树,BST继承了BinaryTree(写在另一篇中了),由于是继承二叉树,搜索二叉树中只写了其特有的方法。本文章仅供学习参考使用,转载附明出处。
__方法包括:添加方法,删除方法,判断结点是否存在的方法,还有为了AVL树准备的方法。代码中BST的一个构造方法中需要加入比较器。为了防止是对引用数据类型进行比较多次比较。
完整代码在最后
1. 一些为AVL准备的方法
可以发现BST重构代码中有许多的方法中什么都没写,那些方法并不是为了BST准备的而是为了AVL准备的。可以看一下重构继承的关系。
AVL(二叉平衡树) 继承 BST(二叉搜索树)
BST(二叉搜索树) 继承 BinaryTree(二叉树)
AVL 树继承 BST 就可以使用BST中的add 方法 和 remove 方法。
AVL树的特点就在于,在添加和删除后可以恢复树的平衡,不至于导致树退化(降低效率)。afterAdd, afterRemove 方法就是用来操作之后恢复树的平衡的。在BST树种,我们在add,和remove 之后都相应的添加afterAdd, afterRemove 方法(空方法什么都不做)。我们在AVL树中只用重写afterAdd, afterRemove 方法就可以完成代码逻辑,而不用重写add和remove方法了。这样做的好处就是既然是重构就要泾渭分明啊,如果AVL再重写add,remove方法的话,冗余度就上升了。
还有一个 createNode 函数意义何在呢?我们知道BST树种的节点是没有 高度 这个属性的,但是AVL树中有,所以AVL树种的节点类(AVLNode)是要继承BST树中的节点类(Node)的而且还要多加一个 height 属性。所以在add 操作中 加入的节点也是不一样的,所以用一个方法来创建节点。AVL树中要重写此方法,把此方法的返回值改成AVLNode即可。
这些都是为了AVL树而准备的,AVL树的重构讲解在下一篇文章中。
2.比较功能的设计
如果给你一个需求,每个树的比较逻辑不一样应该怎么办呢?
设计大概是这样的,如果你在树的初始化中添加比较器,那么我们就按照比较器的比较逻辑来对比。如果你没加入比较器,我们就用引用类型自带(继承Comparable后)的比较逻辑来对比。这样的好处是可以灵活的改变比较的逻辑。比如一棵树你想比较年龄,另一棵树你想比较价格。此代码中你只要在传入的比较器中改变比较逻辑即可。想一想如果别人没有这个需求(需要不同的比较逻辑),那我们就不用传比较器。直接使用引用类型自带的比较逻辑即可。
1.有比较器的构造方法
//引用类型的比较器
private Comparator<E> comparator;
//有比较器的构造方法
public BST(Comparator<E> comparator) {
this.comparator = comparator;
}
2.比较函数
解释:如果BST树在构造时没有传比较器,就按照类的比较逻辑。如果构造的时候传了比较器,那么久按照比较器的逻辑来
/*
比较函数
*/
private int compare(E e1,E e2) {
if (comparator != null) {
return comparator.compare(e1, e2);
}
return ((Comparable<E>)e1).compareTo(e2);
}
且看示例:构造时未添加比较器
Person实现了Comparable接口,有三个属性
自带的比较逻辑是年龄比较
我们插入树中
再打印出来结果,树是按照年龄来构造的
且看示例:构造添加比较器(使用匿名内部类)
添加的比较器逻辑是按照钱的多少。
最终结果如下,是正确的
3. Add方法
思路如下:
1.找到父节点 parent
2.创建新的节点 node
3.加入父节点的左子节点或者右子节点
代码和注释如下:
elementNotNullCheck(element); 是判断元素是否为null的方法,
compare,createNode,afterAdd,afterRemove方法我们上边都说过了。
/*
添加方法
*/
public void add(E element) {
//元素不能为空的判断方法
elementNotNullCheck(element);
//添加第一个节点
if (root == null) {
root = createNode(element, null);
size ++;
afterAdd(root);
return;
}
//添加的不是第一个节点
//用来记录 父节点位置
Node<E> parent = root;
//比较是从父节点开始的
Node<E> node = root;
//我们还要记下方向(左节点 or 右节点)
int cmp = 0;
while(node != null) {
//记录父节点和方向
cmp = compare(element, node.element);
parent = node;
if (cmp > 0) {
node = node.right;
}else if (cmp < 0) {
node = node.left;
}else {
//相等覆盖
//防止是引用类型 除了年龄还有别的属性
node.element = element;
return;
}
}
//创建新节点
Node<E> newNode = createNode(element, parent);
//插入父节点左右
if (cmp > 0) {
parent.right = newNode;
}else {
parent.left = newNode;
}
size ++;
afterAdd(newNode);
}
4. Remove方法
我们给用户提供的方法就是 传入节点的元素也就是element
我们自己去找此元素对应的节点
然后进行删除
思路如下:
删除方法我们要分三种情况讨论
(1)删除的node是叶子节点:直接删除
node = = node.parent.left (是左叶子节点)
node.parent.letf = null (直接删除)
node = = node.parent.right (是右叶子节点)
node.parent.right = null (直接删除)
node.parent = = null (只有一个根节点)
root = null
(2)删除的节点度为1:用叶子节点代替原节点位置
child 是 node.left 或者是 node.right
用child 代替node位置
child.parent = node.parent;
node.parent.left = child; (如果是左子节点)
node是根节点
root = child,child.parent = null
(3)删除的节点是度为2的节点:
找此节点的前驱或者后继节点,用前驱或者后继节点覆盖原节点,
然后删除前驱与后继节点(递归调用来删除)
找前驱和后继方法的讲解在上篇文章二叉树的重构中。因为时树的重构,找前驱和后继要写在二叉树中。
代码如下
/*
删除元素方法
*/
public void remove(E element) {
//删除元素对应的结点
remove(node(element));
}
/*
分三大情况讨论
我们线讨论度为2的结点 有原因的 可以简化代码
*/
private void remove(Node<E> node) {
//结点为空
if(node == null) return;
size --;
//删除结点是度为2的结点
if (node.hasTwoChildren()) {
//找后继结点
Node<E> s = successor(node);
//后继节点的值覆盖原来结点
node.element = s.element;
//下面还要删除后继结点
node = s;
}
//删除node 结点(现在node的度必然为0 或者1)
Node<E> replacement = node.left != null ? node.left : node.right;
if (replacement != null) {//node 是度为1的结点
//更改parent
replacement.parent = node.parent;
//若果node为度为1的根节点
if (node.parent == null) {
root = replacement;
}else if (node == node.parent.left) {//node是左子节点
node.parent.left = replacement;
}else if(node == node.parent.right){//node是右子节点
node.parent.right = replacement;
}
afterRemove(node,replacement);
}else if (node.parent == null) {//node是叶子结点并且是根结点
root = null;
afterRemove(node,null);
}else {//是叶子结点 且不是根结点 直接删除
if (node == node.parent.left) {
node.parent.left = null;
}else {
node.parent.right = null;
}
afterRemove(node,null);
}
}
5. 查找一个结点是否存在
这个代码逻辑就相对简单了
/*
查找一个节点是否存在
*/
private Node<E> node(E element){
//查找从根节点开始
Node<E> node = root;
while(node != null) {
int cmp = compare(element, node.element);
if (cmp == 0) {
//找到了
return node;
}else if (cmp > 0) {
node = node.right;
}else {
node = node.left;
}
}
//循环出来就是没找到啊
return null;
}
完整代码如下
public class BST<E> extends BinaryTree<E> {
//引用类型的比较器
private Comparator<E> comparator;
//无参构造方法
public BST() {
}
//有比较器的构造方法
public BST(Comparator<E> comparator) {
this.comparator = comparator;
}
/*
添加方法
*/
public void add(E element) {
elementNotNullCheck(element);
//添加第一个节点
if (root == null) {
root = createNode(element, null);
size ++;
afterAdd(root);
return;
}
//添加的不是第一个节点
//用来记录 父节点位置
Node<E> parent = root;
//比较是从父节点开始的
Node<E> node = root;
//我们还要记下方向(左节点 or 右节点)
int cmp = 0;
while(node != null) {
//记录父节点和方向
cmp = compare(element, node.element);
parent = node;
if (cmp > 0) {
node = node.right;
}else if (cmp < 0) {
node = node.left;
}else {
//相等覆盖
//防止是引用类型 除了年龄还有别的属性
node.element = element;
return;
}
}
//创建新节点
Node<E> newNode = createNode(element, parent);
//插入父节点左右
if (cmp > 0) {
parent.right = newNode;
}else {
parent.left = newNode;
}
size ++;
afterAdd(newNode);
}
/*
创建节点的接口
*/
protected Node<E> createNode(E element, Node<E> parent){
return new Node<>(element, parent);
}
/*
添加node之后调整的接口
*/
protected void afterAdd(Node<E> node) {}
/*
删除后恢复平衡的方法
*/
protected void afterRemove(Node<E> node,Node<E> replacement) {}
/*
删除元素方法
*/
public void remove(E element) {
//删除元素对应的结点
remove(node(element));
}
/*
分三大情况讨论
我们线讨论度为2的结点 有原因的 可以简化代码
*/
private void remove(Node<E> node) {
//结点为空
if(node == null) return;
size --;
//删除结点是度为2的结点
if (node.hasTwoChildren()) {
//找后继结点
Node<E> s = successor(node);
//后继节点的值覆盖原来结点
node.element = s.element;
//下面还要删除后继结点
node = s;
}
//删除node 结点(现在node的度必然为0 或者1)
Node<E> replacement = node.left != null ? node.left : node.right;
if (replacement != null) {//node 是度为1的结点
//更改parent
replacement.parent = node.parent;
//若果node为度为1的根节点
if (node.parent == null) {
root = replacement;
}else if (node == node.parent.left) {//node是左子节点
node.parent.left = replacement;
}else if(node == node.parent.right){//node是右子节点
node.parent.right = replacement;
}
afterRemove(node,replacement);
}else if (node.parent == null) {//node是叶子结点并且是根结点
root = null;
afterRemove(node,null);
}else {//是叶子结点 且不是根结点 直接删除
if (node == node.parent.left) {
node.parent.left = null;
}else {
node.parent.right = null;
}
afterRemove(node,null);
}
}
/*
查找一个节点是否存在
*/
private Node<E> node(E element){
//查找从根节点开始
Node<E> node = root;
while(node != null) {
int cmp = compare(element, node.element);
if (cmp == 0) {
//找到了
return node;
}else if (cmp > 0) {
node = node.right;
}else {
node = node.left;
}
}
//循环出来就是没找到啊
return null;
}
/*
是否包含元素方法
*/
public boolean contains(E element) {
return node(element) != null;
}
/*
搜索树 节点不能为空的判断方法
*/
public void elementNotNullCheck(E element) {
if (element == null) {
throw new IllegalArgumentException("element must not be null");
}
}
/*
比较函数
*/
private int compare(E e1,E e2) {
if (comparator != null) {
return comparator.compare(e1, e2);
}
return ((Comparable<E>)e1).compareTo(e2);
}
}