尝试一下用Java实现二叉搜索树/二叉查找树,记录自己的学习历程。
1. 首先先来设计实现一下节点Node。
💡一个二叉树的节点需要以下几个元素:
key 关键字
value 节点的值(key也可以代替value)
parent 父节点
leftChildren 左儿子节点
rightChildren 右儿子节点
那就开始吧!
/*** 节点*/
classNode{private intkey;privateString value;privateNode parent;privateNode leftChildren;privateNode rightChildren;publicNode(){}public Node(intkey, String value){this.key =key;this.value =value;
}public intgetKey() {returnkey;
}public void setKey(intkey) {this.key =key;
}publicNode getParent() {returnparent;
}public voidsetParent(Node parent) {this.parent =parent;
}publicNode getLeftChildren() {returnleftChildren;
}public voidsetLeftChildren(Node leftChildren) {this.leftChildren =leftChildren;
}publicNode getRightChildren() {returnrightChildren;
}public voidsetRightChildren(Node rightChildren) {this.rightChildren =rightChildren;
}publicString getValue() {returnvalue;
}public voidsetValue(String value) {this.value =value;
}
}
2. 接下来就是树的实现了。
💡一个二叉树会有什么操作呢?
find(int key) 根据key查找对应的节点
insert(int key, String value) 插入节点
preOrder(Node root) 前序遍历
midOrder(Node root) 中序遍历
backOrder(Node root) 后序遍历
delete(int key) 根据key删除节点
好了,需要的东西都知道了,开始实现吧!先新建一个BinarySearchTree.java文件,把其他属性和方法写上。
/*** 二叉查找树/二叉搜索树*/
public classBinarySearchTree {privateNode root;/*** 遍历结果集合*/List orderResult = new ArrayList<>();publicBinarySearchTree(Node root){this.root =root;
}publicBinarySearchTree(){}publicNode getRoot(){return this.root;
}
}
一步步往里面加入上面列出的方法并且一一实现吧!
1⃣️find(int key)
/*** 查找key
*@paramkey
*@return
*/
public Node find(intkey){/*** 从根节点开始找*/Node currentNode=root;/*** 判断根节点是否符合要求*/
while (currentNode != null && key !=currentNode.getKey()){if (key
currentNode =currentNode.getLeftChildren();
}else{//to right
currentNode =currentNode.getRightChildren();
}
}returncurrentNode;
}
首先把root节点设置为当前节点,当当前节点不为null而且当前节点的key和传入的key不相等时,我们继续循环,判断这个key的node节点是在当前节点的左子树还是右子树,并且继续把左子树或者右子树设为当前节点继续执行,直到当前节点是null或者当前节点的key值等于传入的key值,返回当前节点,查找结束。
2⃣️insert(int key, String value)
/*** 插入*/
public void insert(intkey, String value){
Node newNode= newNode(key, value);if (null ==root){this.root =newNode;return;
}/*** 当前搜索到的树*/Node currentNode=root;
Node parentNode=root;/*** 默认左子树*/
boolean isLeftChild = true;while (null !=currentNode){
parentNode=currentNode;if (key
currentNode =parentNode.getLeftChildren();
isLeftChild= true;
}else{//父节点右侧
currentNode =parentNode.getRightChildren();
isLeftChild= false;
}
}/*** 循环结束之后的parentNode就是最终需要插入子节点的节点,根据isLeftChild判断插入左儿子还是右儿子*/Node childNode= newNode(key, value);if(isLeftChild){
parentNode.setLeftChildren(childNode);
childNode.setParent(parentNode);
}else{
parentNode.setRightChildren(childNode);
childNode.setParent(parentNode);
}return;
}
二叉查找树的插入是将比父节点的值插入到左子树里面,大的放右子树去找到位置插入。
首先,我们new一个节点,看看root节点存不存在,不存在就设置new的节点为root节点,插入结束。
第二种情况,有root节点,继续往下执行,将root节点赋给currentNode变量和parentNode变量,后面采用cn和pn缩写;
我们就默许先从左儿子节点开始比较,key小于pn的key,把cn的左儿子节点赋给cn,继续循环,每次循环开始都把cn保存为pn起来,这样的话,直到cn为null,我们可以轻松获取到上一次操作的cn,也就是当前的pn。
循环结束后,开始插入新节点:
根据isLeftChild判断插入左边还是右边,插入的时候记得给父节点设置儿子,给儿子设置父节点,方便我们后续进行删除操作。
3⃣️遍历,有前中后三个遍历方法,记住前中后是对父节点来说的就很好办了!
* 前序遍历就是对每个树遍历的时候把父节点拎到最前面;
* 中序遍历就是对每个树遍历的时候把父节点拎到最中间;
* 后序遍历就是对每个树遍历的时候把父节点拎到最后面。
代码很简单,递归思想解决。不明白的话可以自己执行debug一遍就深刻理解了!100遍的解释不如自己debug一次!
/*** 前序遍历
*@paramroot
*@return
*/
public ListpreOrder(Node root){if (null ==root){return null;
}
orderResult.add(root);
preOrder(root.getLeftChildren());
preOrder(root.getRightChildren());returnorderResult;
}/*** 中序遍历
*@paramroot
*@return
*/
public ListmidOrder(Node root){if (null ==root){return null;
}
midOrder(root.getLeftChildren());
orderResult.add(root);
midOrder(root.getRightChildren());returnorderResult;
}/*** 后序遍历
*@paramroot
*@return
*/
public ListbackOrder(Node root){if (null ==root){return null;
}
backOrder(root.getLeftChildren());
backOrder(root.getRightChildren());
orderResult.add(root);returnorderResult;
}
4⃣️删除,最难的部分来了,这部分是最麻烦的,不过一步一步来,很多麻烦事都可以解决的,不用害怕。
先贴删除方法和涉及到的方法代码:
/*** 删除节点(有三种情况)
* 1.待删除节点没有子节点
* 2.待删除节点只有一个子节点
* 3.待删除节点有两个子节点
*@paramkey
*@return
*/
public boolean delete(intkey){/*** 被查找到的节点*/Node findNode=find(key);if (null ==findNode){return false;
}/*** 待删除节点没有儿子节点*/
if (null == findNode.getLeftChildren() && null ==findNode.getRightChildren()){if (null ==findNode.getParent()){
root= null;return true;
}else{/*** 是否和父节点的左儿子相同*/
if(isParentLeftChildren(findNode, key)){
findNode.getParent().setLeftChildren(null);return true;
}else{/*** 右儿子*/findNode.getParent().setRightChildren(null);return true;
}
}
}/*** 待删除节点左儿子存在,右儿子为空*/
if (null == findNode.getRightChildren() && null !=findNode.getLeftChildren()){if(isParentLeftChildren(findNode, key)){
findNode.getParent().setLeftChildren(findNode.getLeftChildren());
findNode.getLeftChildren().setParent(findNode.getParent());
findNode= null;return true;
}else{
findNode.getParent().setRightChildren(findNode.getLeftChildren());
findNode.getLeftChildren().setParent(findNode.getParent());
findNode= null;return true;
}
}/*** 待删除节点右儿子存在,左儿子为空*/
else if (null == findNode.getLeftChildren() && null !=findNode.getRightChildren()){if(isParentLeftChildren(findNode, key)){
findNode.getParent().setLeftChildren(findNode.getRightChildren());
findNode.getRightChildren().setParent(findNode.getParent());
findNode= null;return true;
}else{
findNode.getParent().setRightChildren(findNode.getRightChildren());
findNode.getRightChildren().setParent(findNode.getParent());
findNode= null;return true;
}
}/*** 待删除节点左右儿子都存在*/
else if (null != findNode.getLeftChildren() && null !=findNode.getRightChildren()){if(isParentLeftChildren(findNode, key)){/*** 设置一系列的引用*/Node succesor=findSuccessorNode(findNode);/*** 判断后继者是否就是当前节点的左儿子,如果是,不需要再重复设置自己引用自己,会引发内存泄漏。*/
if (succesor !=findNode.getLeftChildren()){
findNode.getLeftChildren().setParent(succesor);
succesor.setLeftChildren(findNode.getLeftChildren());
findNode.getRightChildren().setParent(succesor);
succesor.setRightChildren(findNode.getRightChildren());
findNode.getParent().setLeftChildren(succesor);
succesor.setParent(findNode.getParent());
findNode= null;return true;
}else{
findNode.getRightChildren().setParent(succesor);
succesor.setRightChildren(findNode.getRightChildren());
findNode.getParent().setLeftChildren(succesor);
succesor.setParent(findNode.getParent());
findNode= null;return true;
}
}else{
Node succesor=findSuccessorNode(findNode);/*** 判断后继者是否就是当前节点的左儿子,如果是,不需要再重复设置自己引用自己,会引发内存泄漏。*/
if (succesor !=findNode.getLeftChildren()){
findNode.getLeftChildren().setParent(succesor);
succesor.setLeftChildren(findNode.getLeftChildren());
findNode.getRightChildren().setParent(succesor);
succesor.setRightChildren(findNode.getRightChildren());
findNode.getParent().setRightChildren(succesor);
succesor.setParent(findNode.getParent());
findNode= null;return true;
}else{
findNode.getRightChildren().setParent(succesor);
succesor.setRightChildren(findNode.getRightChildren());
findNode.getParent().setRightChildren(succesor);
succesor.setParent(findNode.getParent());
findNode= null;return true;
}
}
}return false;
}/*** 查询该节点是否父节点的左节点
*@paramfindNode
*@paramkey*/
private boolean isParentLeftChildren(Node findNode,intkey){/*** 是否和父节点的左儿子相同*/
if (null != findNode.getParent().getLeftChildren() && findNode.getParent().getLeftChildren().getKey() ==key){return true;
}else{/*** 是否和父节点的右儿子相同*/
return false;
}
}/*** 查找待删除节点的后继节点,一般是右儿子的左子树的最小值
*@paramnode
*@return
*/
privateNode findSuccessorNode(Node node){/*** 保存父节点*/Node parentNode=node;/*** 当前节点*/Node currentNode=node;while (null !=currentNode){
parentNode=currentNode;
currentNode=currentNode.getLeftChildren();
}/*** 判断父节点的右儿子是否为空,不为空的话需要把右儿子提到该父节点的父节点(没有重复打,就是两个父节点)的左儿子*/
if (null ==parentNode.getRightChildren()){returnparentNode;
}else{
parentNode.getParent().setLeftChildren(parentNode.getRightChildren());
parentNode.getRightChildren().setParent(parentNode.getParent());returnparentNode;
}
}
至此!代码全部贴完!分析完删除部分,再把我的main测试方法代码贴出来,删除方法可能不是很完整,思想大致如此,第一次不是很缜密,下次改进!
*删除分三种情况:
1. 待删除节点没有儿子节点
2.待删除节点只有一个儿子节点
3.待删除节点有两个儿子节点
需要分别针对这三种情况处理!
虽然代码都写了注释,不过还是自己再重新理一遍吧!开干!(看自己写的代码真是觉得要多烂有多烂,头疼)
情况1 :待删除节点没有儿子节点
首先看看待删除节点有没有父节点,没有父节点那就是root节点,设置root为null就行了。
然后看看待删除节点是不是和父节点的左儿子相同,是的话将父节点的左儿子设为null,不是的话将右儿子设为null就行了。
情况1结束。
情况2 :待删除节点只有一个儿子节点,但是还不知道是左儿子还是右儿子。
1) 左儿子节点存在
先isParentLeftChildren()看看待删除节点是它父节点的左儿子还是右儿子,如果是左儿子,将待删除节点的左儿子节点设置为待删除节点父节点的左儿子,反之亦然。
2)右儿子节点存在
先isParentLeftChildren()看看待删除节点是它父节点的左儿子还是右儿子,如果是左儿子,将待删除节点的左儿子节点设置为待删除节点父节点的右儿子,反之亦然。
情况3 :待删除节点右两个儿子(这种情况略微复杂)
先isParentLeftChildren()看看待删除节点是它父节点的左儿子还是右儿子,如果是左儿子,继续
因为待删除节点的两个儿子都存在,说明要从两个子树中挑一个节点来做待删除节点的继承者!
查找继承者用findSuccessorNode()方法;首先查找到左子树的最左侧的最小的儿子,判断这个最小的儿子是不是有右儿子,没有右儿子的情况下直接返回这个最小的儿子,
如果这个最小的儿子有右儿子,需要把这个最小的儿子的右儿子设置为这个最小的儿子的父节点的左儿子,再返回这个最小的儿子作为继承者。
继承者找到了。这里再判断一下继承者是不是就是当前节点的左儿子,是的话就不必重复设置继承者的父节点为待删除节点的左儿子也就是继承者,不是的话,设置引用。
因为这里前面isParentLeftChildren()找到的是左儿子,所以设置待删除节点的父节点的左儿子为继承者,待删除节点的右儿子设置为继承者的右儿子。
反之,亦然。
下面的一大段分析看不明白就结合代码来看,代码里面的注释也有很多的,文章写到这里就结束了!
结束🔚