前言
每个人都有自己的追求和梦想,想要成为更好的自己。而在软件开发领域,掌握数据结构是成为优秀程序员的必备技能之一。二叉搜索树是其中重要的一种,它的优缺点和实现原理不仅是程序员必须了解的内容,同时也是程序员个人成长的一部分。在Java语言中实现一个二叉搜索树需要的不仅仅是技术上的熟练掌握,更需要的是内心的坚定和自我超越的精神。掌握数据结构,科学编程,这是我们一路走来的坚定信念,也是我们通向成功的必经之路。
正文
一、概念
二叉搜索树(Binary Search Tree,BST)是一种基于二叉树而产生的一种数据结构。在二叉搜索树中,每个节点的值只能比它的左子节点大或者比它的右子节点小,这一特性使得数据的查找、插入和删除等操作变得高效而容易。今天我们将学习如何使用Java语言实现二叉搜索树,以及该数据结构的一些重要概念和操作。
二、相关操作的代码实现
二叉搜索树主要要实现的代码就是构建、查找、删除,当然,构建是第一位,下面将介绍二叉搜索树是如何构建出来的。
2.1、构建
构建二叉搜索树首先要搞清楚他有几个属性。首先,左右孩子节点肯定要有吧?然后就是要有个数字的属性吧?还要有个头,也就是根节点,有了这三个属性,再写个构造方法,我们就可以先写一部分了,代码实现如下:
public class BinarySearchTree {
static class TreeNode {
private int val;
private TreeNode left;
private TreeNode right;
public TreeNode( int val) {
this.val = val;
}
}
}
然后就是构造二叉搜索树的正式代码了,在写这个之前,我们要搞清楚二叉搜索树到底是怎么构建的。首先来看一个构建好的二叉搜索树,如下图就是一课标准的二叉搜索树:
如上图,大家先找找这颗树的特点,和普通的二叉树有什么不同呢?是不是这棵树所有的左右子树都满足左边子树的数据全部比根节点小,右边的子树所有的数据全都比根节点大呢?是这样的没错,所以我们思路可以这样:
首先,如果root == null
,那么一定是第一次插入,我们直接让头等于这个值就行了。如果是后面的插入,我们定义需要插入的数据为val
,然后我们要在这棵树里面找到val
合适的位置,此时我们可以定义一个代走的指针为cur
,如下图:
就比如我要插入的数据是 4 ,那cur
该怎么走呢?首先是不是得和根节点 8 进行比较呢?比较的比 8 小,就走左边,再跟 5 比较,还是比 5 小,继续走左边,再跟 3 比较,这回比较大了,就走右边,但是一走右边就为null
了呀,那还怎么插?所以,光有一个变量cur
是肯定不够的,还要定义一个在cur
之上的,也就是cur
的父亲,当cur == null
了,直接跳出去,跳出去之后,直接和这个cur
的父亲节点比较,如果大就放右边,如果小就左边,插入成功返回true
,否则返回false
,思路理顺,代码实现如下:
public boolean insert(int key) {
TreeNode node = new TreeNode(key);
if (root == null) {
root = node;
return true;
}
TreeNode cur = root;
TreeNode curParent = null;
while (cur != null) {
// 如果比它小就往左边走
if (key < cur.val) {
curParent = cur;
cur = cur.left;
}
// 如果比它大就往右边走
else if (key > cur.val) {
curParent = cur;
cur = cur.right;
}
// 如果相等就提醒用户已存在,然后返回 false
else {
System.out.println("该值得节点已存在!!!");
return false;
}
}
// 出来判断到底是大还是小
if (key < curParent.val) {
curParent.left = node;
}
else {
curParent.right = node;
}
return true;
}
如上,构造二叉搜索树的代码就实现了,可以先试着插入几个数据试试,运行结果如下:
这是我随便写的数据,按照这个数据,画出的图如下:
2.2、查找
构造完成之后,我们还要学习关于二叉搜索树的查找方法,只要建好了一颗二叉搜索树,查找起来那速度真的是没得说,一颗普通的二叉树查找效率的时间复杂度是 O(N*long2n) ,搜索二叉树查找起来的效率那就直接减少了N倍,时间复杂度直接变成了 O(long2n) 。
那这个查找方法到底该怎么写呢?首先我们可以定义一个代走的指针->Phead
, 让这个指针走,小走左,大往右,找到返回节点,找不到返回空,思路理顺,代码实现如下:
public TreeNode search(int val) {
TreeNode phead = root;
while (phead != null) {
if (val < phead.val) {
phead = phead.left;
}
else if (val > phead.val) {
phead = phead.right;
}
else {
break;
}
}
return phead;
}
这样子二叉搜索树查找的代码就写好了。因为如果打印返回值的话,打印的是一个地址,所以大家可以重写toString
方法或者在break
前面也写个打印,主要是打印找到该节点的地址,然后两个节点的地址相比较,如下:
2.3、删除
然后就是二叉搜索树比较绕的删除部分了,首先看图:
删除一般是分三种情况,一种是删除的该节点左边是空,一种是删除的该节点右边是空,一种是两边都不是空,每种又分三种情况,所以说比较绕,但是你先别急,我带你慢慢来,先来看删除节点左边是空的情况:
先假设,假设我要删除的节点左边是空,然后分三种:
- 如果要删除的节点是头。
- 如果不是头,但是是在父亲节点的左边。
- 如果不是头,但是是在父亲节点的右边。
主要就是这些情况,我们先写找节点的代码,如下:
public void removeNode(int key) {
// 先找到节点
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
if (key < cur.val) {
parent = cur;
cur = cur.left;
}
else if (key > cur.val) {
parent = cur;
cur = cur.right;
}
else {
remove(cur, parent);
break;
}
}
System.out.println("未找到该节点!!!");
}
上面三种情况,我们一个一个慢慢分析,先假设是头,那我们直接让头等于头的右边不就行了吗?那如果不是头呢?比如这样的:
假如我要删除的节点是 3 ,那该怎么办?哎,我们不是都找到了cur
的父亲节点吗?直接让父亲节点指向cur.right
不就好了?这个也分析完毕,我们再来看第三种情况:如果不是头,但是在父亲节点的右边,比如我要删除的是 6 ,如下:
还是老样子,看图说话,我们直接让parent.right = cur.right
不就好了?所以第一种大情况的代码如下:
private void remove(TreeNode cur, TreeNode parent) {
// 左边是空
if (cur.left == null) {
// 判断是不是头
if (cur == root) {
root = root.right;
}
// 如果不是头
else {
// 判断是左还是右
if (cur == parent.left) {
parent.left = cur.right;
}
else {
parent.right = cur.right;
}
}
}
// 右边是空
else if (cur.right == null) {
}
// 左右都不是空
else {
}
}
紧接着我们来分析待删除的节点如果右边是空的情况,老样子,又分为三种情况:
- 如果要删除的节点是头。
- 如果不是头,但是是在父亲节点的左边。
- 如果不是头,但是是在父亲节点的右边。
这三种和上面大差不差,先来看第一种:待删除的节点是头节点,如下:
就比如说我要删除 8 ,那我们直接让root = root.left
不就好了?
再来分析第二种情况:不是头,但是待删除节点在父亲节点的左边,比如像删除 3 这种情况,我们直接让 5 这个节点指向 3 的左就行了,然后分析第三种情况,要一个新的图,如下:
第三种情况的条件是:待删除的节点右边是空,但是这个节点在父亲节点的右边,比如说 20 这个节点就很是满足这种情况,还是看图说话,我们直接让 8 这个父亲节点跳过 20 ,指向 15 就行了,所以当待删除节点右边为空的情况代码实现如下:
else if (cur.right == null) {
// 判断是不是头
if (cur == root) {
root = root.left;
}
// 如果不是头
else {
// 判断是左还是右
if (cur == parent.left) {
parent.left = cur.left;
}
else {
parent.right = cur.left;
}
}
}
最后接着看我们最麻烦的一个:待删除节点左右都不是空的情况:
比如说我要删除 5 这个节点,我们用的是替罪羊的方法来实现这个要求的,可以找待删除节点左树的最大值,也可以找待删除节点右树的最小值,我们就拿找右树最小值举例,是不是可以定义一个代cur
走的节点pcur
呀,然后我们让pcur
走,一直走到pcur.left
是空,然后这个节点和cur
交换,完成之后删掉pcur
这个节点,但是因为pcur
的左边是空的,不就又变成我们第一种大情况(当待删除节点左树为空时)的情况了吗?这个思想使用的是数学里面的归元的思想。思路理顺之后,代码实现如下:
// 左右树都不是空
else {
// 先找到右边的最小值
TreeNode pcur = cur.right;
TreeNode pparent = cur;
while (pcur.left != null) {
pparent = pcur;
pcur = pcur.left;
}
cur.val = pcur.val;
// 然后删除这个替罪的节点
// 又到了待删除的节点左边是空的情况了
if (pcur == pparent.left) {
pparent.left = pcur.right;
}
else {
pparent.right = pcur.right;
}
}
然后所有关于删除的代码整合一下,就是这样的:
public void removeNode(int key) {
// 先找到节点
TreeNode cur = root;
TreeNode parent = null;
while (cur != null) {
if (key < cur.val) {
parent = cur;
cur = cur.left;
}
else if (key > cur.val) {
parent = cur;
cur = cur.right;
}
else {
remove(cur, parent);
break;
}
}
System.out.println("未找到该节点!!!");
}
private void remove(TreeNode cur, TreeNode parent) {
// 左边是空
if (cur.left == null) {
// 判断是不是头
if (cur == root) {
root = root.right;
}
// 如果不是头
else {
// 判断是左还是右
if (cur == parent.left) {
parent.left = cur.right;
}
else {
parent.right = cur.right;
}
}
}
// 右边是空
else if (cur.right == null) {
// 判断是不是头
if (cur == root) {
root = root.left;
}
// 如果不是头
else {
// 判断是左还是右
if (cur == parent.left) {
parent.left = cur.left;
}
else {
parent.right = cur.left;
}
}
}
// 左右都不是空
else {
// 先找到右边的最小值
TreeNode pcur = cur.right;
TreeNode pparent = cur;
while (pcur.left != null) {
pparent = pcur;
pcur = pcur.left;
}
cur.val = pcur.val;
// 然后删除这个替罪的节点
// 又到了待删除的节点左边是空的情况了
if (pcur == pparent.left) {
pparent.left = pcur.right;
}
else {
pparent.right = pcur.right;
}
}
}
这样一个普通二叉搜索树就建成了,大家可以试着插入、删除、查找数据了。
总结
在本文中,我们学习了二叉搜索树的基本概念、特征和操作,以及如何在Java语言中实现它。通过学习二叉搜索树,我们可以更加深刻地理解数据结构和算法,并且了解如何使用二叉搜索树处理各种问题。
与其他数据结构相比,二叉搜索树具有许多优点,例如进行插入、查找、删除等基本操作时速度非常快,而且它的内存使用效率也相对较高。因此,理解和掌握二叉搜索树是非常有必要的。
当然,二叉搜索树也存在缺陷,例如不同的插入和删除顺序会影响树的平衡,使得二叉搜索树的性能降低等。为了解决这些问题,我们需要使用更高级的数据结构,例如红黑树和AVL树等。
在学习过程中,我们需要时刻注意代码实现的复杂度和鲁棒性,尤其是处理边缘情况的能力。当然,能够熟练掌握二叉搜索树的基本操作远远不够,在实际应用中,我们还需要结合具体问题,在实际情况中加以运用。
希望本文能够对读者了解和掌握二叉搜索树提供一点参考和帮助,欢迎读者在实践中不断探索和发现!
结语
作为程序员,学习数据结构和算法是必不可少的。数据结构不仅在理解算法中扮演着重要的角色,而且在程序设计和软件开发中也扮演着至关重要的角色。因此,了解和掌握二叉搜索树对于我们的成长和进步是非常重要的。
学习编程的过程充满了挫折与艰辛,甚至可能会让我们失去信心和方向。但是,只要怀有激情和毅力,有着不断探索和学习的心态,一切困难都将迎刃而解。在二叉搜索树的学习中,我们不仅掌握了基础知识和技能,更重要的是在不断学习和探索中,我们转变了解决问题的思维方式,更深刻地认识了自己的不足,也拥有了更广阔的视野和更多的机遇。
因此,让我们一起坚持不懈地学习、实践和探索,在编程的旅途中越走越远,成为真正的技术巨匠!