最近在学习数据结构和算法,所以用js来实现一下二叉搜索树,检验学习效果。
首先,我们来简单说一下什么是二叉搜索树,它有什么特性:
二叉搜索树(二叉查找树)不同于普通二叉树,是它的左子树所有的节点都小于根节点,右子树所有的节点都大于根节点,并且其左右子树也是二叉搜索树。
![12ffc2a684b935e14c13597073f288cf.png](https://i-blog.csdnimg.cn/blog_migrate/7748f1aa0f2c95b89ea73f1a39f835fc.png)
二叉搜索树的概念不多说,具体来看看js代码:
![0abc2c9dcb883bb3209477be96d4823e.png](https://i-blog.csdnimg.cn/blog_migrate/d29a3ca35e25ed835fc16bceb1a178e9.png)
上图,写了一个二叉搜索树的类,用来生成二叉搜索树的实例;可以看到,类里面只有一个root属性来保存根节点,然后还有一些操作方法;下面我们来看看每个方法怎么实现。
插入方法:
![0ddb461defd9c4419f49ef627d6c1a0b.png](https://i-blog.csdnimg.cn/blog_migrate/29865eab00deaf86b44dc8795d036e79.png)
![3a2684c23f20c8c0ff9643a4154146a0.png](https://i-blog.csdnimg.cn/blog_migrate/f22b7389e13223b64b252723dbc3dd76.png)
因为插入需要创建新节点,所以写了一个简单的节点构造函数,函数内有三个属性,key:表示节点的关键字也是值,left:指向左子树,right:指向右子树。
插入新节点的流程也比较简单,就是先创建一个节点,然后根据插入节点的大小,遍历找到插入的位置,然后赋值插入就OK;遍历之前需要先判断根节点是否有值,没有则直接赋值,然后返回。
遍历方法:
遍历二叉树的方法主要有:先序遍历、中序遍历、后续遍历三种;
先序遍历:先访问根节点,再遍历左子树,最后遍历右子树,在遍历左、右子树的过程中依然采用先序遍历。(先根再左后右)
中序遍历:先遍历左子树,再访问根节点,最后遍历右子树,在遍历左、右子树的过程中依然采用中序遍历。(先左后根再右)
后序遍历:先遍历左子树,再遍历右子树,最后访问根节点,在遍历左、右子树的过程中依然采用后序遍历。(先左后右再根)
![df2017269bc0224edb19f9222af8f753.png](https://i-blog.csdnimg.cn/blog_migrate/24f33663432e6b35d35d79d860d7d10a.png)
如上代码,是采用递归来实现遍历,可以看到,先序遍历是将处理节点的回调函数放在了最前面,这也意味着函数入栈时就处理节点,所以处理的顺序是:9-5-4-3-7-6-8-13-11-10-12-14
![eacc213867d4af07fa9b7fa79ecec24e.png](https://i-blog.csdnimg.cn/blog_migrate/4ad489958f2c17771cbd47ab0306429c.png)
可以看到,中序遍历是将处理节点的回调放到了遍历左子树的后面,这样就意味着在遍历左子树的函数出栈后再处理节点(这里需要理解函数调用栈,栈结构只能在栈顶进出,所以函数执行的顺序是先进后出),所以处理节点的顺序是:3-4-5-6-7-8-9-10-11-12-13-14
可以发现,中序遍历将整个二叉搜索树进行了从小到大的排序。
![9de04a128926b2343e4f4ac68f2151cd.png](https://i-blog.csdnimg.cn/blog_migrate/7e9791fc3fa7dd6ece7ef9c5016a72a3.png)
看到这里,后序遍历也很容易理解了,就是将处理的回调函数放在最后,等遍历右子树的函数出栈后再处理节点;所以处理节点的顺序是:3-4-6-8-7-5-10-12-11-14-13-9
获取最大值、最小值:
![6e4a24c2ddbc1adb04a5568eb6ac6e58.png](https://i-blog.csdnimg.cn/blog_migrate/fef9acc3c89720bc401677629e66346c.png)
搜索节点:
![4a001cf9233170a4753f65a6ff156c3c.png](https://i-blog.csdnimg.cn/blog_migrate/63848b0bf81f2a931cc7bd79eb2aaa75.png)
删除节点:
删除节点的操作比较复杂,这里分几个阶段来说:
1.首先,找到要删除的节点
![be63230898c22fed957fb416654f6735.png](https://i-blog.csdnimg.cn/blog_migrate/9423e7d5b1db14c29dc4c2f87f30efc0.png)
这里说明一下函数开头声明的几个变量,current用来存储要删除的节点,parentNode存储删除节点的父节点,isLefChi用来区分删除节点是父节点的左子节点还是右子节点。
2.根据要删除节点的不同情况进行删除
2.1 删除节点为叶节点
![8d6fd0861632ea5024d76cca4b0a876d.png](https://i-blog.csdnimg.cn/blog_migrate/ef8f489fc7b6f3923beb55820020383a.png)
2.2 删除节点只有一个子节点
![2484d998f65827e6590211ac4ddb1f4f.png](https://i-blog.csdnimg.cn/blog_migrate/d3b804c489475303cbd3adc91d504684.png)
只有一个节点的情况,需要再次判断是没有左子节点,还是没有右子节点;
2.3 删除节点有两个子节点
![606a35f3817f10fbf500d88e92fe6d48.png](https://i-blog.csdnimg.cn/blog_migrate/1dbb9f57451d5567de8b443b9675419a.jpeg)
说一下前驱和后继:
1)删除节点有两个子节点,这两个子节点下面也许还有子节点(子节点树);
2)我们需要从子节点树中找一个节点来替换删除节点;
3)根据二叉搜索树的特性:左子树小于根节点,右子树大于根节点,左右子树亦如此,可以发现,最适合用来替换(也可以说大小最接近)删除节点的有两个,一个是删除节点左子树最大节点(前驱),还有一个是右子树最小节点(后继),用其他的节点都不满足二叉搜索树的特性;
4)遍历找到前驱/后继节点(哪一个都行);
5)这里也有个情况要注意,前驱节点是否是删除节点的直接子节点;
好了,到这里就完成了js版的二叉搜索树。