我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
二分搜索树
一些基本的定义
- 每个节点最多只能有两个孩子节点,也可以没有孩子节点(也就是叶子节点)
- 每个节点的左孩子都小于该节点
- 每个节点的右孩子都大于该节点
- 每个节点只有一个父亲节点,根节点没有父亲节点
二叉树示例
- 文中,左孩子,左子树都是一个意思,右孩子,右子树也是一样。
二叉树的基本操作
通常来说对于一个二叉树我们会对其进行增加、查询、遍历、删除四种操作。
增加节点
核心思想 如果二叉树为空,则根节点等于新增元素就结束了。如果二叉树不为空,从根节点开始开始与插入元素进行比较,插入元素大于根节点则向根节点的右孩子进行比较,插入元素小于根节点则向根节点的左孩子进行比较,以此类推,直到找到与该元素比较的节点的左孩子或者右孩子为空(null)的时候,就是插入元素正确的位置。
【讲人话】:只要找到自己的位置就行了,因此,既然知道二叉搜索树的规则是每个节点只能有两个儿子,左边的比自己小,右边的比自己大,那就从根(root)开始对比,对比的值比自己大我就往左子树找自己的位置,比自己小那我就往右子树找自己的位置,当你发现下一个跟你对比的是个空的(null),那这就是你的位置了,直接坐下。
代码实现
首先编写二叉树的基本属性
- E为存储在树中的元素,我们用范型表示
- 因为需要比较元素的大小,所以我们需要指定E必须是继承了Comparable接口的对象
- 内部类Node是我们的节点类,他有一个E表示存储的值,left,right表示左孩子、右孩子
public class BST<E extends Comparable<E>> {
private class Node {
public E e;
public Node left, right;
public Node(E e) {
this.e = e;
this.left = null;
this.right = null;
}
}
// 根节点
private Node root;
// 树的大小
private int size;
public BST() {
root = null;
size = 0;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
// 查询树是否存在接口
public boolean contains(E e) {
return contains(e);
}
}
添加方法代码实现
public void add(E e) {
// 一定要将root更新为新增节点之后返回的树
root = addNode(root, e);
}
/**
* 递归添加元素e到node中
*
* @param node
* @param e
* @return
*/
private Node addNode(Node node, E e) {
// 当节点的值为空的时候 表示递归到底了,也就找到了元素的位置
if (node == null) {
size++;
return new Node(e);
}
// 如果元素小于当前节点,则继续递归比较该节点的左子树
if (e.compareTo(node.e) < 0) {
node.left = addNode(node.left, e);
} else if (e.compareTo(node.e) > 0) { // 如果元素大于当前节点,则继续递归比较该节点的右子树
node.right = addNode(node.right, e);
}
return node;
}
查询节点
核心思想 从根节点(root)开始递归查询,如果查询的值比递归到的当前节点大,就向当前节点右子树查询,如果查询的值比递归到的当前节点小,就向当前节点左子树查询,直到递归到的节点值与查询的值相等,即返回true,如果递归完整颗树,都没有,则返回false
【讲人话】:跟新增找自己的位置如出一辙,从根节点开始对比,比自己大往左子树找,比自己小往右子树找,一直对比,无非两个节点,如果发现跟自己对比的节点是个null,说明没有我,直接然后false,如果跟自己一样大,嘿,你猜怎么着,返回true呗。
代码实现
// 查询节点
public boolean contains(E e) {
return contains(root, e);
}
// 递归函数
private boolean contains(Node node, E e) {
if (node == null) {
return false;
}
// 当前节点等于查询的值 return true
if (e.compareTo(node.e) == 0) return true;
// 当前节点的值大于查询的值 向当前节点的左子树继续递归查询
else if (e.compareTo(node.e) < 0) return contains(node.left, e);
// 当前节点的值小于查询的值 向当前节点的右子树继续递归查询
else return contains(node.right, e);
}
树的遍历
想想树的遍历应该怎么进行了,遍历无非就是把怎个树里面的值打印出来,保证每一个都打印,不丢失任何一个就完事儿了。
核心思想 从根节点开始首先打印根节点自己,然后打印根节点的左子树,然后再打印根节点右子树。
代码实现
// 递归打印整棵树
private void printTree() {
printTree(root);
}
// 递归函数肯定就长这样 传入一个节点,打印节点的所有值,打印整棵树就传根节点root就行了
private void printTree(Node node) {
// 递归终止条件: 如果传入节点为空就表示节点以及到底了,就结束递归
if (node == null) return;
// 既然不为空就打印节点的值
System.out.println(node.e);
// 继续打印这个节点的左子树
printTree(node.left);
// 左子树打印完了就打印右子树
printTree(node.right);
}
有一个二叉树长这样
那么打印结果就是
常见的遍历方式
树的常见遍历方式有四种,你可能听说过,就是前序遍历,中序遍历,后续遍历和层序遍历四种,是不是很耳熟,是不是很高大上,我上大学的时候,数据结构学的那叫一点不会,考试遇到给定一个二叉树,让我写出前序遍历我是直接懵逼,前序遍历是什么鬼,是不是需要计算? 感觉好难啊,考试之前都会找牛逼的人求情,待会给我抄抄,哈哈,但当我毕业了自己在回过来看,这啥玩意,这也能叫期末考试题?? OK, 屁话先不说了,开始.....
假如现在有一个树长下面这个样子,我们给每一个节点都标上1,2,3三个序号,分别在前,中,后三个位置。
图1
我们再来看看遍历的函数
// 我们可以看一下 遍历一个节点我们会遇到这个节点三次 分别是下面的1,2,3 就是对应【图1】的1,2,3
private void printTree(Node node) {
if (node == null) return;
// 1
System.out.println(node.e);
printTree(node.left);
// 2
printTree(node.right);
// 3
}
好了下面开始拿捏前序,中序,后续遍历...
前序遍历
所谓前序遍历就是在1的位置打印节点的值,简单吧,上面我们写的打印方法其实就是前序遍历。
中序遍历
所谓中序遍历就是在2的位置打印节点的值,那么中序遍历的代码就很简单了
private void printTree(Node node) {
if (node == null) return;
// 1
printTree(node.left);
// 2
System.out.println(node.e);
printTree(node.right);
// 3
}
中序遍历打印上面的值
可以看到中序遍历的值是从小到大进行排序的,所以二分搜索树其实也可以用来进行排序。哦哟,小技巧,拿本本记好!
后序遍历
那么后序遍历就自然是在3的位置打印节点的值了,这就是后序遍历。
private void printTree(Node node) {
if (node == null) return;
// 1
printTree(node.left);
// 2
printTree(node.right);
// 3
System.out.println(node.e);
}
是不是突然发现,前序,中序,后续遍历也就这个样子,我要是早点用心学,也不至于考试的时候乞求别人给我看看,害。
删除节点
在二分搜索树的新增,查询,遍历,删除的几个操作中,删除是相对比较麻烦的,为啥呢,因为删除一个节点你需要维护整棵树,需要保证二分搜索树的性质不会被破坏,那么删除节点就会遇到一下四种可能的情况,每种情况对应的处理方式也各不一样。
待删除的节点的左右孩子都为空的
这种情况只需要将待节点删除就行
待删除节点的左孩子不为空,右孩子为空
这种情况只需要将该节点的左孩子代替该节点即可
待删除节点的右孩子不为空,左孩子为空
处理方式跟前一种一样,将该节点的右孩子代替该节点即可
待删除节点的左右孩子均不为空
此种情况是四种情况中最麻烦的一种,因为需要删除后维护二分搜索树的性质,因此比较麻烦,通常的处理方式有两种
- 找到被删除节点的后继节点中最小值代替被删除节点,被删除节点的左子树成为最小值的左子树,被删除节点的右子树删除最小值后成为最小值的右子树
- 找到被删除节点的前驱节点中最大值代替被删除节点,被删除节点的右子树成为最大值的右子树,被删除节点的左子树删除最大值后成为最大值的左子树
代码实现
首先删除节点的第四种情况,我们需要找到一棵树或者是子树的的最大值,或者最小值,因为我们知道二分搜索树的性质,一棵树中最小值一定是在树的最右下侧,即一直递归一课树的左子树,直到某个节点的左孩子为null的时候,该节点就是最小值。最大值与最小值相反,一直递归一棵树的右子树,直到某个节点的右孩子为null的时候,该节点就是该树最大值,如图
代码如下:
// 递归查询最小值
private Node findMinNodeNR(Node e) {
if (e.left == null) {
return e;
} else {
return findMinNodeNR(e.left);
}
}
// 递归查询最大值
private Node findMaxNodeNR(Node e) {
if (e.right == null) {
return e;
} else {
return findMinNodeNR(e.right);
}
}
同时找到了子树的最小值最大值,还要有相应的删除方法,删除方法也很简单,直接将对应节点设置为null就行了, 代码如下...
// 递归删除最小值
private Node removeMin(Node e) {
if (e.left == null) {
size--;
return null;
}
e.left = removeMin(e.left);
return e;
}
private Node removeMax(Node e) {
if (e.right == null) {
size--;
return null;
}
e.right = removeMax(e.right);
return e;
}
因此最后删除节点的完整代码如下:
public void remove(E e) {
root = remove(root, e);
}
private Node remove(Node node, E e) {
if (node == null)
return null;
if (e.compareTo(node.e) < 0) {
node.left = remove(node.left, e);
return node;
} else if (e.compareTo(node.e) > 0) {
node.right = remove(node.right, e);
return node;
} else {
// 当被删除节点左孩子为空的时候,用被删除节点的右孩子代替被删除节点
if (node.left == null) {
node = node.right;
size--;
return node;
}
// 当被删除节点右孩子为空的时候,用被删除节点的左孩子代替被删除节点
if (node.right == null) {
node = node.left;
size--;
return node;
}
// 当被删除节点左右孩子都不为空的时候,有两种方法
// 1.找到被删除节点的后继节点中最小值代替被删除节点,被删除节点的左子树成为最小值的左子树,被删除节点的右子树删除最小值后成为最小值的右子树
// 2.找到被删除节点的前驱节点中最大值代替被删除节点,被删除节点的右子树成为最大值的右子树,被删除节点的左子树删除最大值后成为最大值的左子树
Node successor = findMinNodeNR(node.right);// 找到后继节点中的最小值
successor.right = removeMin(node.right);// 删除后继节点中的最小值
successor.left = node.left;// 被删除节点的左子树成为最小值的左子树 并成为最小值的右子树
node.left = node.right = null;
return successor;
}
}
好啦,该说的也都差不多说完了! 加油!
结束语
写文章是对自己学习成功的输出与记录,看到这二叉搜索树的基本概念跟基本操作就已经讲完了,纯手打,纯手画图,这是我在掘金的第一篇文章,如有不对的地方还请各位大佬指出来,最后,感谢你的浏览! bye-bye ! 下一篇原来如此-二叉堆再见!