来来来!热乎的二叉排序树(搜索树)查找、增加、删除操作


前言

这是这个系列上的第二篇文章,如果你还没有了解二叉树的话,可以先看我的文章中阅读量最高的二叉树(从建树、遍历到存储)Java.

一、定义

满是学究气息的文字定义我们先不看,还是沿用看图说话的惯例。
在这里插入图片描述
数无形时少直觉,形少数时难入微”,所以文字上的定义也是不能少的,其实我们通过上图可以自己总结出来。

对于二叉树上的任意节点,若它的左子树不为空,那么左子树上所有节点的值均小于它的值,若它的右子树不为空,那么右子树上所有节点的值均大于它的值。

构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找、增加和删除数据的速度。因为在一个有序的集合中查找数据的速度总是快于在无序集合中,而且二叉树排序树这种非线性的结构也有利于增加和删除节点(类似链表)。

二、查找

在一本很畅销的数据结构书上这一部分的算法实现用的递归,我不是很赞同,虽然递归写起来代码很简洁,但是对于初学者而言比较难理解,随着一层一层的递归调用下去,跟着跟着就满脑浆糊了。

所以这里我们还是用循环来写,方便理解。

代码

public boolean contains(int e) {
		//用current记录根节点
		Node current = root;
		while (current != null) {
			//相等,说明找到了,返回true
			if (e == current.data) {
				return true;
				//要查找的数比根节点大,指向根节点的右儿子
			} else if (e > current.data) {
				current = current.right;
				//要查找的数比根节点下,指向根节点的左儿子
			} else {
				current = current.left;
			}
		}
		//找不到,返回false
		return false;
	}

这只是一个代码片段,最后提供完整代码,更具有逻辑性。

图解

图一:初始状态,若查找80。
在这里插入图片描述
图二:
(1)node为根节点,不为空,进入while循环;
(2)要查找的是80,node.data的值是60,80大于60,node等于node.right。
在这里插入图片描述
图三:
(1)此时node为下图中的红色节点,不为空,进入while循环;
(2)要查找的是80,node.data是75,80大于75,node等于node.right。
在这里插入图片描述
图四:
(1)此时node为下图中的红色节点,不为空,进入while循环;
(2)要查找的80,node.data是80,80等于80,返回true。
在这里插入图片描述

三、增加节点

当要插入的数据在二叉排序树中不存在时,插入成功并返回true,否则返回false。

代码

	public boolean add(int e) {
		if (root == null) {
			root = new Node(e);
			return true;
		}
		//记录current的父节点
		Node father = null;
		Node current = root;
		while (current != null) {
			if (e > current.data) {
				father = current;
				current = current.right;
			} else if (e < current.data) {
				father = current;
				current = current.left;
			} else {
				return false;
			}
		}
		if (e > father.data) {
			father.right = new Node(e);
		} else {
			father.left = new Node(e);
		}
		return true;
	}

对比查找代码:

public boolean contains(int e) {
		Node current = root;
		while (current != null) {
			if (e == current.data) {
				return true;
			} else if (e > current.data) {
				current = current.right;
			} else {
				current = current.left;
			}
		}
		return false;
	}

所以如果已经理解查找的代码,对于增加的代码也不难理解。

四、删除节点

“请佛容易送佛难”,从上面的代码我们知道增加节点的操作其实并不麻烦,一半代码还和“查找”操作的代码相同,但是要删除节点的话就没那么轻松了,因为要考虑到删除该节点后整个二叉树还满住二叉排序树的要求。

1.单身狗

即如下图中的深色节点,这个要删除的节点是一个可怜的单身狗。。。
在这里插入图片描述
那么就把它直接删除!我们可以看到直接删除后对整个二叉树排序树并没有什么影响。

2.独生子

这个节点的后代虽然只有一棵独苗,但也是有的。
在这里插入图片描述
当他驾崩后,这皇位要传给谁呢?当然是他的独生子69。而且我们把69放到62的位置发现二叉排序树的性质并没有被破坏,如下图。
在这里插入图片描述
当然,若这颗独苗也有后代,情况是一样的。
在这里插入图片描述

3.多子多孙

“哈哈哈哈!我可是多福多寿,多子多孙之人!”又来了一个节点。这个节点可是儿孙满堂,那么他驾崩后,“夺嫡”自然是不可避免的。
在这里插入图片描述
30他驾崩了,留下了大好河山,宗室诸王们蠢蠢欲动,谁都想过一把皇帝瘾,经过一番比拼,发现25或者35都可以继承皇位,那就二者选其一吧。

“等等!怎么就25或者35了?”一个外国人不明所以地问道。原来在二叉排序树内部皇位的继承不是看谁最强而是看谁最像的。在30的众多子孙中只有25和35是最像他的(和他的差距最小)。

但是在实际操作中我们不一定要真的把30删掉,可以稍稍变通一下,先找到继承人将他的值赋给要删除的节点,再将继承人删除。

看到这里有些人不禁会想这个方法听上去简单,实际中要怎么用算法找到继承人呢?之后会与大家分享两句口诀,保证让大家按图索骥,顺利找到这两个节点。

(1)25继承

在这里插入图片描述

口诀一:左转,向右到尽头。

我们再来看一个例子,如下图:
在这里插入图片描述
注意:对于继承人来说他只有单身狗和左独生子这两种情况,不存在右独生子和多子多孙的情况,不明的话,再看看口诀就明白了,如果他存在右子,那么皇位还能轮到他继承吗?口诀可是向右到尽头!

(2)35继承

在这里插入图片描述

口诀二:右转,向左到尽头。

同样我们再来看一个例子,如下图:
在这里插入图片描述
注意:同理,对于第二种情况的继承人来说他只有单身狗和右独生子这两种情况,不存在左独生子和多子多孙的情况,如果他存在左子,就不是他继承皇位了。

五、完整代码

这是我在学习过程中模仿Java集合类写的一个二叉排序树类,欢迎大家指导。

BinarySortTree类:

import java.util.LinkedList;

public class BinarySortTree {
	//记录根节点
	Node root;

	public BinarySortTree() {
	}

	//增
	public boolean add(int e) {
		if (root == null) {
			root = new Node(e);
			return true;
		}
		//记录current的父节点
		Node father = null;
		Node current = root;
		while (current != null) {
			if (e > current.data) {
				father = current;
				current = current.right;
			} else if (e < current.data) {
				father = current;
				current = current.left;
			} else {
				return false;
			}
		}
		if (e > father.data) {
			father.right = new Node(e);
		} else {
			father.left = new Node(e);
		}
		return true;
	}

	/* 删
	 * 
	 * */
	public boolean remove(int e) {
		Node father = null;
		Node current = root;
		while (current != null) {
			if (current.data == e)
				return removeNode(current, father, e);
			else if (e > current.data) {
				father = current;
				current = current.right;
			} else {
				father = current;
				current = current.left;
			}
		}
		return false;
	}

	final boolean removeNode(Node current, Node father, int e) {
		//若左儿子为空,右儿子继承
		if (current.left == null) {
			if (current.data > father.data) {
				father.right = current.right;
			} else {
				father.left = current.right;
			}
			//方便垃圾回收器回收
			current = null;
		}
		//若右儿子为空,左儿子继承
		else if (current.right == null) {
			if (current.data > father.data) {
				father.right = current.left;
			} else {
				father.left = current.left;
			}
			current = null;
		}
		//多子多孙
		else {
			//用于记录heir的父节点
			Node heirFather = null;
			/* heir(继承人)
			 * heir = current.left即口诀一中的左转
			 * */
			Node heir = current.left;
			//即口诀一中的向右到尽头
			while (heir.right != null) {
				heirFather = heir;
				heir = heir.right;
			}
			//继承人的值赋给要被删除的节点
			current.data = heir.data;
			if (heir.data > heirFather.data) {
				heirFather.right = heir.left;
			} else {
				heirFather.left = heir.left;
			}
			heir = null;
		}
		return true;
	}

	//查找
	public boolean contains(int e) {
		Node current = root;
		while (current != null) {
			if (e == current.data) {
				return true;
			} else if (e > current.data) {
				current = current.right;
			} else {
				current = current.left;
			}
		}
		return false;
	}

	//层次遍历
	public void levelOrder() {
		if (root == null) {
			return;
		}
		LinkedList<Node> q = new LinkedList<Node>();
		q.addFirst(root);
		Node current;
		while (!q.isEmpty()) {
			current = q.removeLast();
			System.out.print(current.data + " -> ");
			if (current.left != null)
				q.addFirst(current.left);
			if (current.right != null)
				q.addFirst(current.right);
		}
	}

	private static class Node {
		int data;
		Node right;
		Node left;

		Node(int data) {
			this.data = data;
		}
	}
}

测试类:

public class Demo {
	public static void main(String[] args) {
	
		BinarySortTree tree = new BinarySortTree();

		tree.add(60);
		tree.add(30);
		tree.add(75);
		tree.add(15);
		tree.add(50);
		tree.add(80);
		tree.add(10);
		tree.add(25);
		tree.add(40);
		tree.add(55);
		tree.add(19);
		tree.add(35);

		System.out.print("层次遍历(删除前):");
		tree.levelOrder();
		System.out.println();

		//删除
		tree.remove(30);

		System.out.print("层次遍历(删除后):");
		tree.levelOrder();
		System.out.println();

		System.out.print("是否已存在:");
		System.out.print(tree.contains(80));
	}
}

测试类中构造出的二叉树排序树长这样:
在这里插入图片描述
结果:

层次遍历(删除前)60 -> 30 -> 75 -> 15 -> 50 -> 80 -> 10 -> 25 -> 40 -> 55 -> 19 -> 35 -> 
层次遍历(删除后)60 -> 25 -> 75 -> 15 -> 50 -> 80 -> 10 -> 19 -> 40 -> 55 -> 35 -> 
是否已存在:true

这里我只测试了一种情况,有兴趣的可以将代码粘贴复制到编译器上多测试几种情况。

写在最后

分享这篇文章的目的不仅仅是与大家一起讨论二叉排序树的使用,更大的目的是为了对付JDK1.8中的HashMap,其中用到了红黑树这种数据结构。

所以接下来还会分享其他类型二叉树的使用,总的路线为:二叉树——>二叉排序树(二叉搜索树)——>红黑树,再加一个哈希表(hash table)。

这是这条路线上的第二篇文章,之前还有一篇二叉树的二叉树(从建树、遍历到存储)Java.有兴趣的话可以看看,如果喜欢这个系列的话可以持续关注哦。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值