JAVA搜索二叉树的重构,比较器的设计,添加方法,删除方法,判断元素是否存在于树中

前言

~~接着实现搜索二叉树,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);
	}
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值