二叉搜索树定义
二叉搜索树(又:二叉查找树,二叉排序树)是以一颗二叉树来组织的,在代码实现上,二叉搜索树可以用链表来表示,将二叉树的节点看作链表的Node对象,每个Node对象包含left、right、parent属性,它们分别代表节点的左孩子、右孩子、父节点(双亲节点),如果某个孩子或父节点不存在,则用null值表示。
二叉搜索树是指具有下面定义的二叉树(下面1、2点必须背下来):
1、任何父节点的值均大于等于(>=)左孩子节点值以及左孩子下的所有孩子节点值
2、任何父节点的值均小于(<)右孩子节点值以及右孩子下的所有孩子节点值
3、树最根节点是唯一父节点值为null的节点
二叉搜索树本质上就是一颗二叉树,二叉树逻辑上一般有以下类型和形态:
1、空二叉树
2、只有一个根节点的二叉树
3、左斜树
4、右斜树
5、完全二叉树
6、满二叉树
二叉搜索树一般具有以下动态集合操作API:
1、查找(search)
2、插入(insert)
3、删除(delete)
4、最大值(maxVal)
5、最小值(minVal)
6、前驱(pre)
7、后继(post)
前驱:给定任意节点,如果该节点是整颗树的最小值(绝对不存在左孩子的),它的前驱是null;否则如果该节点存在左孩子,它的前驱是以这个左孩子为根下的左边整颗子树中最大值的那个节点;如果不存在左孩子且不是最小值,它的前驱是根节点。
后继:给定任意节点,如果该节点是整颗树的最大值(绝对不存在右孩子的),它的后继是null;否则如果该节点存在右孩子,它的后继是以这个右孩子为根下的右边整颗子树中最小值的那个节点;如果不存在右孩子且不是最大值,它的后继是根节点
二叉搜索树基本动态集合操作API的时间复杂度均为O(h),其中h为二叉树的高度。
二叉搜索树-插入(insert)
当调用插入接口时(insert)就开始构建了二叉搜索树了,所以,研究如何构建二叉树实际上就是研究如何实现插入方法。根据如下定义,我们来看下如何实现插入接口的代码:
二叉搜索树是指具有下面定义的二叉树(下面1、2点必须背下来):
1、任何父节点的值均大于等于(>=)左孩子节点值以及左孩子下的所有孩子节点值
2、任何父节点的值均小于(<)右孩子节点值以及右孩子下的所有孩子节点值
3、树最根节点是唯一父节点值为null的节点
根据定义,我们还快能够想到,插入元素一些基本逻辑如下:
1、每个元素值必须是能够进行大小比较的,至少有定义好比较规则
2、第一次插入元素可以直接作为根。这里注意,如果第一次插入的值是整棵树最大值或最小值,那么这种情况这个二叉树通过会成为斜树,就是只有(首次插入的是最大值)左孩子或(首次插入的是最小值)右孩子的,那么这棵树的高度会很高,我们知道,二叉搜索树的大部分操作API时间复杂度是0(h)高度h越高,时间复杂度越大,实际使用时可以使用随机方式避免这种情况的发生。
3、后面每次插入元素时,从根开始进行比对,如果小于等于根,循环判断左孩子下的所有孩子节点直到叶子节点(孩子节点为null值);如果大于根,循环判断右孩子下的所有孩子节点直到叶子节点(孩子节点为null值);
代码如下:
public void insert(T eleVal) {
Node newNode = new Node();
newNode.eleVal = eleVal;
this.insertNode(newNode);
}
private void insertNode(Node newNode) {
//检查参数
checkArg(newNode);
Node insertParent = null;
Node tempParent = this.rootNode;
//循环过程直到遇到叶子节点才停止
while (tempParent != null) {
insertParent = tempParent;
//newNode.eleVal <= tempParent.eleVal
if (newNode.eleVal.compareTo(tempParent.eleVal) <= 0) {
//如果插入的节点值小于等于根节点,那么将循环左孩子下的所有孩子节点
tempParent = tempParent.left;
} else {
//如果插入的节点值大于根节点,那么将循环右孩子下的所有孩子节点
tempParent = tempParent.right;
}
}
newNode.parent = insertParent;
//如果是第一次插入 rootNode == null
if (insertParent == null) {
newNode.left = null;
newNode.right = null;
this.rootNode = newNode;
} else {
//newNode.eleVal <= insertParent.eleVal
if (newNode.eleVal.compareTo(insertParent.eleVal) <= 0) {
//左孩子
insertParent.left = newNode;
} else {
//右孩子
insertParent.right = newNode;
}
}
}
//检查参数
private void checkArg(Object arg) {
if (arg == null) {
throw new RuntimeException("arg not can null");
}
}
//链表结构节点对象
private class Node {
private Node left;
private T eleVal;
private Node right;
private Node parent;
}
二叉搜索树-查找(get)
从根往下查找一个给定的值,我们只需要判断这个值是根节点的左孩子还是右孩子,如果是左孩子,那么将左孩子和这个值比较看是否相等,如果相等就返回,否则将这个左孩子作为子根按上面逻辑递归查找,代码如下:
public Node get(T eleVal){
return this.getNode(this.rootNode, eleVal);
}
/**
* 从某个根节点开始查找某个值
*
* @param subRoot 某个根或子根节点
* @param eleVal 某个值
* @return 返回对应值的Node对象
*/
@SuppressWarnings("unchecked")
private Node getNode(Node subRoot, T eleVal) {
//没有找到[subRoot == null]
//或 匹配到值[subRoot.eleVal.equals(eleVal)]
if (subRoot == null || subRoot.eleVal.equals(eleVal)) {
return subRoot;
}
//root.eleVal >= eleVal
if (subRoot.eleVal.compareTo(eleVal) >= 0) {
//如果查找的值小于等于根或孩子作为的子根,递归左孩子作为下一个根继续查找
return getNode(subRoot.left, eleVal);
} else {
//如果查找的值大于根或孩子作为的子根,递归右孩子作为下一个根继续查找
return getNode(subRoot.right, eleVal);
}
}
二叉搜索树-最大值(max)
给定任意节点的最大值节点:基于这个给定节点循环或递归往右查找,直到叶子节点为止,该叶子节点便是整棵树的最大值节点,代码如下:
/**
* 获取给定任意节点的最大值节点
*
* @param root 根节点
* @return 最大值节点
*/
public Node maxNode(Node root) {
checkArg(root);
Node maxNode = root.right;
if (maxNode == null){
return root;
}
//直到叶子节点为止条件
while (maxNode.right != null) {
maxNode = maxNode.right;
}
return maxNode;
}
二叉搜索树-最小值(min)
给定任意节点的最小值节点:基于这个给定节点循环或递归往左查找,直到叶子节点为止,该叶子节点便是整棵树的最小值节点,代码如下:
/**
* 获取给定任意节点的最小值节点
*
* @param root 根节点
* @return 最小值节点
*/
public Node minNode(Node root) {
checkArg(root);
Node minNode = root.left;
if (minNode == null){
return root;
}
//直到叶子节点为止条件
while (minNode.left != null) {
minNode = minNode.left;
}
return minNode;
}
二叉搜索树-前驱后继(pre、post)
前驱:给定任意节点,如果该节点是整颗树的最小值(绝对不存在左孩子的),它的前驱是null;否则如果该节点存在左孩子,它的前驱是以这个左孩子为根下的左边整颗子树中最大值的那个节点;如果不存在左孩子且不是最小值,它的前驱是根节点,代码如下:
public T pre(T eleVal){
checkArg(eleVal);
Node n = this.getNode(this.rootNode, eleVal);
if (n == null){
return null;
}
return this.preNode(n).eleVal;
}
/**
* 查找给定节点的前驱
*
* @param node 给定节点
* @return 前驱
*/
private Node preNode(Node node) {
checkArg(node);
//如果节点存在左节点
if (node.left != null) {
//返回左节点最大值节点作为前驱节点
return this.maxNode(node.left);
} else {
//如果节点不存在左节点,则需要向上查找,直到最根节点
Node parent = node.parent;
while (parent != null && parent.left == node) {
parent = parent.parent;
node = parent;
}
//如果node值为整棵树最小值,这里返回的node为null,
//这是由核心条件parent.left == node控制的效果
return wapNode(parent);
}
}
private Node wapNode(Node node){
return node == null ? new Node() : node;
}
后继:给定任意节点,如果该节点是整颗树的最大值(绝对不存在右孩子的),它的后继是null;否则如果该节点存在右孩子,它的后继是以这个右孩子为根下的右边整颗子树中最小值的那个节点;如果不存在右孩子且不是最大值,它的后继是根节点,代码如下:
public T post(T eleVal){
checkArg(eleVal);
Node n = this.getNode(this.rootNode, eleVal);
if (n == null){
return null;
}
return this.postNode(n).eleVal;
}
private Node postNode(Node node) {
checkArg(node);
//如果节点存在右节点
if (node.right != null) {
//返回右节点最小值节点作为后继节点
return this.minNode(node.right);
} else {
//如果节点不存在右节点,则需要向上查找,直到最根节点
Node parent = node.parent;
while (parent != null && parent.right == node) {
node = parent;
parent = parent.parent;
}
//如果node值为整棵树最大值,这里返回的node为null,
//这是由核心条件parent.right == node控制的效果
return wapNode(parent);
}
}
二叉搜索树-删除(max)
删除API是二叉搜索树所有API中最复杂之一,下面我们把它分解多种情况来讲解。
1、如果节点只有一个元素且删除的节点是根节点,那么直接把根节点置为null即可
2、删除给定任意一个节点,如果该节点只存在左节点,那么很简单,只需要把左节点替换到该节点上,给定的这个节点就被删除了
3、删除给定任意一个节点,如果该节点只存在右节点,那么很简单,只需要把右节点替换到该节点上,给定的这个节点就被删除了
4、删除给定任意一个节点,如果该节点存在左右节点,这种情况就比较复杂些,我们知道,删除某个节点一定是要改变二叉树的结构的,而改变结构可能会破坏二叉搜索树的定义。所以复杂之处就是要保证删除后的二叉搜索树依然满足定义,下面直接给出删除的处理逻辑。
4.1、找出被删除节点的后继节点
4.2、判断这个后继节点是否是被删除节点的直接孩子节点(后继节点不可能是左孩子节点)
4.3、如果后继节点是被删除节点的直接孩子节点,直接将后继节点替换到被删除节点的位置
4.4、如果后继节点不是被删除节点的直接孩子节点,需要先将后继节点的右孩子节点(根据定义这个后继节点不可能有左孩子节点的只有右孩子节点)替换到这个后继节点位置,最后再把后继节点替换到被删除节点位置
图解后继节点是被删除节点的直接孩子节点的删除前后情况:
图解后继节点不是被删除节点的直接孩子节点的删除前后情况:
我们发现,删除API需要替换节点位置能力,我们可以先把这个能力提取出来作为一个独立的方法,代码如下:
/**
* 替换节点
*
* @param replaceNode 用replaceNode节点
* @param delNode 替换delNode节点
*/
private void replaceNode(Node replaceNode, Node delNode) {
//如果被替换节点是根
if (delNode.parent == null) {
//直接赋值给根
this.rootNode = replaceNode;
//如果被替换节点是左节点
} else if (delNode.parent.left == delNode) {
//将节点替换到左节点位置
delNode.parent.left = replaceNode;
//如果被替换节点是右节点
} else if (delNode.parent.right == delNode) {
//将节点替换到右节点位置
delNode.parent.right = replaceNode;
} else {
throw new RuntimeException("数据结构错误");
}
if (replaceNode != null) {
replaceNode.parent = delNode.parent;
}
}
删除节点代码如下:
public void delete(T eleVal) {
Node delNode = this.getNode(this.rootNode, eleVal);
this.deleteNode(delNode);
}
/**
* 删除某个节点
*
* @param delNode 节点对象
*/
private void deleteNode(Node delNode) {
checkArg(delNode);
//被删除节点只有右孩子,没有左孩子,直接用右孩子替换当前删除节点
if (delNode.left == null) {
this.replaceNode(delNode.right, delNode);
} else if (delNode.right == null) {
//被删除节点只有左孩子,没有右孩子,直接用左孩子替换当前删除节点
this.replaceNode(delNode.left, delNode);
} else {
//被删除节点有左右孩子,先拿后继节点
Node postNode = this.postNode(delNode);
//如果后继不是被删除节点的直接孩子节点
if (postNode.parent != delNode) {
//将后继的右孩子替换到这个后继节点postNode
//(后继节点指右边最小值节点,这个节点没有左孩子节点,因为它就是最小的)
this.replaceNode(postNode.right, postNode);
postNode.right = delNode.right;
postNode.right.parent = postNode;
}
//将后继节点替换到被删除节点位置
this.replaceNode(postNode, delNode);
//将原来被删除节点的左孩子节点挂到后继节点左孩子上
postNode.left = delNode.left;
//修改原来被删除节点的左孩子节点父节点为继节点
postNode.left.parent = postNode;
}
}
完整二叉搜索树代码
/**
* <p>
* 二叉搜索树
* </p>
*
* @author laizhiyuan
* @since 2019/10/4.
*/
public class TwoForkSearchTree<T extends Comparable> {
private Node rootNode;
private AtomicInteger index = new AtomicInteger(0);
public static void main(String[] args) {
//插入
Integer[] array = {16, 8, 17, 19, 23, 12, 15};
TwoForkSearchTree<Integer> tree = new TwoForkSearchTree<>();
for (Integer integer : array) {
tree.insert(integer);
}
//构建完成后的二叉树
Integer[] result = new Integer[array.length];
tree.sortAsc(tree.rootNode, result);
System.out.println("构建完成后的二叉树 " + JSON.toJSONString(result));
System.out.println("根节点值 " + tree.rootNode.eleVal);
//查找
System.out.println("值为16的节点值 " + tree.get(16).eleVal);
//前驱
System.out.println("值为15的前驱值 " + tree.pre(15));
System.out.println("值为8的前驱值 " + tree.pre(8));
System.out.println("值为16的前驱值 " + tree.pre(16));
//后继
System.out.println("值为16的后继值 " + tree.post(16));
System.out.println("值为8的后继值 " + tree.post(8));
System.out.println("值为23的后继值 " + tree.post(23));
System.out.println("值为17的后继值 " + tree.post(17));
//删除根节点
System.out.println("删除根前根值是 " + tree.rootNode.eleVal);
tree.delete(tree.rootNode.eleVal);
System.out.println("删除根后根值是 " + tree.rootNode.eleVal);
System.out.println("删除根节点后二叉树正序输出");
tree.print(tree.rootNode);
System.out.println();
//查找
System.out.println("删除后值为16的节点值 " + tree.get(16).eleVal);
//前驱
System.out.println("删除后值为17的前驱值 " + tree.pre(17));
//后继
System.out.println("删除后值为15的后继值 " + tree.post(15));
}
public void sortAsc(Node subRoot, T[] array) {
if (subRoot != null) {
sortAsc(subRoot.left, array);
array[index.getAndIncrement()] = subRoot.eleVal;
sortAsc(subRoot.right, array);
}
}
public void delete(T eleVal) {
Node delNode = this.getNode(this.rootNode, eleVal);
this.deleteNode(delNode);
}
/**
* 删除某个节点
*
* @param delNode 节点对象
*/
private void deleteNode(Node delNode) {
checkArg(delNode);
//被删除节点只有右孩子,没有左孩子,直接用右孩子替换当前删除节点
if (delNode.left == null) {
this.replaceNode(delNode.right, delNode);
} else if (delNode.right == null) {
//被删除节点只有左孩子,没有右孩子,直接用左孩子替换当前删除节点
this.replaceNode(delNode.left, delNode);
} else {
//被删除节点有左右孩子,先拿后继节点
Node postNode = this.postNode(delNode);
//如果后继不是被删除节点的直接孩子节点
if (postNode.parent != delNode) {
//将后继的右孩子替换到这个后继节点postNode
//(后继节点指右边最小值节点,这个节点没有左孩子节点,因为它就是最小的)
this.replaceNode(postNode.right, postNode);
postNode.right = delNode.right;
postNode.right.parent = postNode;
}
//将后继节点替换到被删除节点位置
this.replaceNode(postNode, delNode);
//将原来被删除节点的左孩子节点挂到后继节点左孩子上
postNode.left = delNode.left;
//修改原来被删除节点的左孩子节点父节点为继节点
postNode.left.parent = postNode;
}
}
/**
* 替换节点
*
* @param replaceNode 用replaceNode节点
* @param delNode 替换delNode节点
*/
private void replaceNode(Node replaceNode, Node delNode) {
//如果被替换节点是根
if (delNode.parent == null) {
//直接赋值给根
this.rootNode = replaceNode;
//如果被替换节点是左节点
} else if (delNode.parent.left == delNode) {
//将节点替换到左节点位置
delNode.parent.left = replaceNode;
//如果被替换节点是右节点
} else if (delNode.parent.right == delNode) {
//将节点替换到右节点位置
delNode.parent.right = replaceNode;
} else {
throw new RuntimeException("数据结构错误");
}
if (replaceNode != null) {
replaceNode.parent = delNode.parent;
}
}
public void insert(T eleVal) {
Node newNode = new Node();
newNode.eleVal = eleVal;
this.insertNode(newNode);
}
@SuppressWarnings("unchecked")
public void insertNode(Node newNode) {
//检查参数
checkArg(newNode);
Node insertParent = null;
Node tempParent = this.rootNode;
//循环过程直到遇到叶子节点才停止
while (tempParent != null) {
insertParent = tempParent;
//newNode.eleVal <= tempParent.eleVal
if (newNode.eleVal.compareTo(tempParent.eleVal) <= 0) {
//如果插入的节点值小于等于根节点,那么将循环左孩子下的所有孩子节点
tempParent = tempParent.left;
} else {
//如果插入的节点值大于根节点,那么将循环右孩子下的所有孩子节点
tempParent = tempParent.right;
}
}
newNode.parent = insertParent;
//如果是第一次插入 rootNode == null
if (insertParent == null) {
newNode.left = null;
newNode.right = null;
this.rootNode = newNode;
} else {
//newNode.eleVal <= insertParent.eleVal
if (newNode.eleVal.compareTo(insertParent.eleVal) <= 0) {
//左孩子
insertParent.left = newNode;
} else {
//右孩子
insertParent.right = newNode;
}
}
}
public boolean isEmpty() {
return this.rootNode == null;
}
public T pre(T eleVal){
checkArg(eleVal);
Node n = this.getNode(this.rootNode, eleVal);
if (n == null){
return null;
}
return this.preNode(n).eleVal;
}
/**
* 查找给定节点的前驱
*
* @param node 给定节点
* @return 前驱
*/
private Node preNode(Node node) {
checkArg(node);
//如果节点存在左节点
if (node.left != null) {
//返回左节点最大值节点作为前驱节点
return this.maxNode(node.left);
} else {
//如果节点不存在左节点,则需要向上查找,直到最根节点
Node parent = node.parent;
while (parent != null && parent.left == node) {
parent = parent.parent;
node = parent;
}
//如果node值为整棵树最小值,这里返回的node为null,
//这是由核心条件parent.left == node控制的效果
return wapNode(parent);
}
}
private Node wapNode(Node node){
return node == null ? new Node() : node;
}
public T post(T eleVal){
checkArg(eleVal);
Node n = this.getNode(this.rootNode, eleVal);
if (n == null){
return null;
}
return this.postNode(n).eleVal;
}
private Node postNode(Node node) {
checkArg(node);
//如果节点存在右节点
if (node.right != null) {
//返回右节点最小值节点作为后继节点
return this.minNode(node.right);
} else {
//如果节点不存在右节点,则需要向上查找,直到最根节点
Node parent = node.parent;
while (parent != null && parent.right == node) {
node = parent;
parent = parent.parent;
}
//如果node值为整棵树最大值,这里返回的node为null,
//这是由核心条件parent.right == node控制的效果
return wapNode(parent);
}
}
private void checkArg(Object arg) {
if (arg == null) {
throw new RuntimeException("arg not can null");
}
}
/**
* 获取给定任意节点的最大值节点
*
* @param root 根节点
* @return 最大值节点
*/
public Node maxNode(Node root) {
checkArg(root);
Node maxNode = root.right;
if (maxNode == null){
return root;
}
//直到叶子节点为止条件
while (maxNode.right != null) {
maxNode = maxNode.right;
}
return maxNode;
}
/**
* 获取给定任意节点的最小值节点
*
* @param root 根节点
* @return 最小值节点
*/
public Node minNode(Node root) {
checkArg(root);
Node minNode = root.left;
if (minNode == null){
return root;
}
//直到叶子节点为止条件
while (minNode.left != null) {
minNode = minNode.left;
}
return minNode;
}
public Node get(T eleVal){
return this.wapNode(this.getNode(this.rootNode, eleVal));
}
public void print(Node subRoot){
if (subRoot != null) {
print(subRoot.left);
System.out.print(subRoot.eleVal + " ");
print(subRoot.right);
}
}
/**
* 从某个根节点开始查找某个值
*
* @param subRoot 某个根或子根节点
* @param eleVal 某个值
* @return 返回对应值的Node对象
*/
@SuppressWarnings("unchecked")
private Node getNode(Node subRoot, T eleVal) {
//没有找到[subRoot == null] 或 匹配到值[subRoot.eleVal.equals(eleVal)]
if (subRoot == null || subRoot.eleVal.equals(eleVal)) {
return subRoot;
}
//root.eleVal >= eleVal
if (subRoot.eleVal.compareTo(eleVal) >= 0) {
//如果查找的值小于等于根或孩子作为的子根,递归左孩子作为下一个根继续查找
return getNode(subRoot.left, eleVal);
} else {
//如果查找的值大于根或孩子作为的子根,递归右孩子作为下一个根继续查找
return getNode(subRoot.right, eleVal);
}
}
private class Node {
private Node left;
private T eleVal;
private Node right;
private Node parent;
}
}
测试二叉搜索树
public static void main(String[] args) {
//插入
Integer[] array = {16, 8, 17, 19, 23, 12, 15};
TwoForkSearchTree<Integer> tree = new TwoForkSearchTree<>();
for (Integer integer : array) {
tree.insert(integer);
}
//构建完成后的二叉树
Integer[] result = new Integer[array.length];
tree.sortAsc(tree.rootNode, result);
System.out.println("构建完成后的二叉树 " + JSON.toJSONString(result));
System.out.println("根节点值 " + tree.rootNode.eleVal);
//查找
System.out.println("值为16的节点值 " + tree.get(16).eleVal);
//前驱
System.out.println("值为15的前驱值 " + tree.pre(15));
System.out.println("值为8的前驱值 " + tree.pre(8));
System.out.println("值为16的前驱值 " + tree.pre(16));
//后继
System.out.println("值为16的后继值 " + tree.post(16));
System.out.println("值为8的后继值 " + tree.post(8));
System.out.println("值为23的后继值 " + tree.post(23));
System.out.println("值为17的后继值 " + tree.post(17));
//删除根节点
System.out.println("删除根前根值是 " + tree.rootNode.eleVal);
tree.delete(tree.rootNode.eleVal);
System.out.println("删除根后根值是 " + tree.rootNode.eleVal);
System.out.println("删除根节点后二叉树正序输出");
tree.print(tree.rootNode);
System.out.println();
//查找
System.out.println("删除后值为16的节点值 " + tree.get(16).eleVal);
//前驱
System.out.println("删除后值为17的前驱值 " + tree.pre(17));
//后继
System.out.println("删除后值为15的后继值 " + tree.post(15));
}
输出:
构建完成后的二叉树 [8,12,15,16,17,19,23]
根节点值 16
值为16的节点值 16
值为15的前驱值 12
值为8的前驱值 null
值为16的前驱值 15
值为16的后继值 17
值为8的后继值 12
值为23的后继值 null
值为17的后继值 19
删除根前根值是 16
删除根后根值是 17
删除根节点后二叉树正序输出
8 12 15 17 19 23
删除后值为16的节点值 null
删除后值为17的前驱值 15
删除后值为15的后继值 17
二叉搜索树适用场景
二叉搜索树是二叉树的一种,顾名思义,二叉搜索树一般就是做路径搜索用的,如何定义好父节点很重要,调优二叉搜索树,实际上就是如何找到集合的中间值作为父节点,因为只有父节点值处于较中间位置,那么二叉树的高度就比较低,查找速度就比较快。如果已知集合是可能有序的或者第一个插入值是最大或小值时,最好通过随机的方式从集合获取一个值进行插入,避免出现斜树的情况。