1、二叉搜索树概念:
二叉搜索树是一种节点值之间具有一定数量级次序的二叉树,对于树中每个节点:
- 若其左子树存在,则其左子树中每个节点的值都不大于该节点值;
- 若其右子树存在,则其右子树中每个节点的值都不小于该节点值。
性能分析
查询复杂度、构造复杂度和删除复杂度三种操作的时间复杂度皆为。
的存在是应为二叉树有可能出现线性结构:
下面分析线性结构的三种操作复杂度,以二分法为例:
- 查询复杂度,时间复杂度为 ,优于二叉搜索树;
- 元素的插入操作包括两个步骤,查询和插入。查询的复杂度已知,插入后调整元素位置的复杂度为 ,即单个元素的构造复杂度为:
- 删除操作也包括两个步骤,查询和删除,查询的复杂度已知,删除后调整元素位置的复杂度为 ,即单个元素的删除复杂度为:
由此可知,二叉搜索树相对于线性结构,在构造复杂度和删除复杂度方面占优;在查询复杂度方面,二叉搜索树可能存在类似于斜树,每层上只有一个节点的情况,该情况下查询复杂度不占优势。
2、二叉搜索树实现
2.1 二叉树节点构造
package com.suirui.binarytree;
/**
* Created by zongx on 2020/1/10.
*/
public class TreeNode {
public int data;
public TreeNode right;
public TreeNode left;
public TreeNode() {
}
public TreeNode(int data) {
this.data = data;
}
public TreeNode(int data, TreeNode left, TreeNode right) {
this.data = data;
this.right = right;
this.left = left;
}
@Override
public String toString() {
return "当前节点值: " + this.data;
}
}
2.2 创建搜索二叉树
/**
* @Description: 往二叉树中插入数值
* @Author: zongx
* @Date: 2020/1/10
* @Param: value
* @return void
*/
public void add(int value) {
//构建节点
TreeNode node = new TreeNode(value);
//判断是否有根节点,没有将当前值节点定义为根节点
if (root == null) {
root = node;
return;
}
//需要比较的节点
TreeNode current = new TreeNode();
//从根节点开始比较
current = root;
while (true) {
//要插入节点大于或等于当前节点,则判断右子树
if (value >= current.data) {
if (current.right == null) {
current.right = node;
return;
}
current = current.right;
}
//要插入节点小于当前节点,则判断左子树
if (value < current.data) {
if (current.left == null) {
current.left = node;
return;
}
current = current.left;
}
}
}
2.3 查找节点
/**
* @Description: 通过key值查找节点
* @Author: zongx
* @Date: 2020/1/13
* @Param: key
* @return com.suirui.binarytree.TreeNode
*/
public TreeNode find(int key) {
if (root == null) {
return null;
}
TreeNode current = root;
while (current.data != key) {
if(key < current.data ) {
current = current.left;
}else{
current = current.right;
}
if (current == null) {
return null;
}
}
return current;
}
2.4 前中后三种遍历方式
- 先序遍历:访问根节点、先序遍历左子树、先序遍历右子树。
- 中序遍历:中序遍历左子树、访问根节点、中序遍历右子树。
- 后序遍历:后续遍历左子树、后续遍历右子树、访问根节点。(代码实现方式使用了倒推的方式,值得注意)
/**
* @Description: 递归实现前序遍历--中左右
* @Author: zongx
* @Date: 2020/1/10
* @Param: root
* @return void
*/
public void preOrderRecursion(TreeNode root) {
if (root != null) {
System.out.print(root.data + " ,");
preOrderRecursion(root.left);
preOrderRecursion(root.right);
}
}
/**
* @Description: 前序遍历--中左右
* @Author: zongx
* @Date: 2020/1/10
* @Param: root
* @return void
*/
public void preOrder(TreeNode root) {
if (root == null) {
return;
}
LinkedList<TreeNode> stack = new LinkedList<>();
while (root != null || !stack.isEmpty()) {
if(root != null) {
//只要root不为null,就代表是在前序遍历的路径上(都是根节点)
System.out.print(root.data + " ,");
stack.push(root);
root = root.left;
}else {
//此时不需要找最左侧节点,所以直接抛出(压栈的时候已经输出信息了),然后前序遍历右子树
root = stack.pop();
root =root.right;
}
}
}
/**
* @Description: 递归实现中序遍历--左中右
* @Author: zongx
* @Date: 2020/1/10
* @Param: root
* @return void
*/
public void inOrderRecursion(TreeNode root) {
if (root != null) {
inOrderRecursion(root.left);
System.out.print(root.data + " ,");
inOrderRecursion(root.right);
}
}
/**
* @Description: 中序遍历--左中右
* @Author: zongx
* @Date: 2020/1/10
* @Param: root
* @return void
*/
public void inOrder(TreeNode root) {
//使用栈的特性,新进后出
LinkedList<TreeNode> stack = new LinkedList<>();
if (root == null){
return;
}
//使用栈的特性,新进后出,进行遍历
while (root != null || !stack.isEmpty()) {
if(root != null) {
//如果节点不为null,则存入栈中
stack.push(root);
root = root.left;
} else {
//如果节点为null,则代表当前已没有更左侧的节点
//此时栈顶节点,便是应该输出的节点
//输出完栈顶节点,应该再找该节点的右子树的最左侧节点
root = stack.pop();
System.out.print(root.data + " ,");
root = root.right;
}
}
}
/**
* @Description: 递归实现后序遍历--左右中
* @Author: zongx
* @Date: 2020/1/10
* @Param: root
* @return void
*/
public void postOrderRecursion(TreeNode root) {
if (root != null) {
postOrderRecursion(root.left);
postOrderRecursion(root.right);
System.out.print(root.data + " ,");
}
}
/**
* @Description: 实现后序遍历--左右中
* @Author: zongx
* @Date: 2020/1/10
* @Param: root
* @return void
*/
public void postOrder(TreeNode root) {
if(root == null) {
return;
}
//利用栈进行树遍历
LinkedList<TreeNode> stack = new LinkedList<TreeNode>();
//获取后序遍历的逆顺序
LinkedList<TreeNode> resultInverseStack = new LinkedList<TreeNode>();
//精髓在逆序找后序遍历路径
while (root != null || !stack.isEmpty()) {
if(root != null) {
stack.push(root);
resultInverseStack.push(root);
root = root.right;
} else {
root = stack.pop();
root = root.left;
}
}
for (TreeNode node : resultInverseStack) {
System.out.print(node.data + " ,");
}
}
/**
* @Description: 层次遍历,核心思想使用队列保持层的顺序
* @Author: zongx
* @Date: 2020/1/10
* @Param: root
* @return void
*/
public void levelOrder(TreeNode root) {
if (root == null) {
return;
}
LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
root = queue.pop();
System.out.print(root.data + " ,");
if (root.left != null) {
queue.add(root.left);
}
if (root.right != null) {
queue.add(root.right);
}
}
}
2.5 删除节点
分为三种情况:
- 要删除节点有零个孩子,即叶子节点,直接删除
- 要删除节点有一个孩子,只需要将parent.left(或者是parent.right)设置为curren.right(或者是current.left)即可
- 要删除节点有两个孩子,此时需要注意后继节点的问题,代码中详细介绍
- 1.后继节点为待删除节点的右子
- 2.后继节点为待删除结点的右孩子的左子树
public boolean del(int key) {
if (root == null) {
return false;
}
//首先找到要删除节点和其父节点,以及要删除节点是其父节点的右节点还是左节点
TreeNode current = root;
TreeNode parent = null;
Boolean isRightChild = null;
while (current.data != key) {
parent = current;
if(key < current.data ) {
current = current.left;
isRightChild = false;
} else {
current = current.right;
isRightChild = true;
}
if (current == null) {
return false;
}
}
// 此时current就是要删除的结点,parent为其父结点
//判断三种情况
//1、要删除节点,没有子节点
if(current.right == null && current.left == null) {
if (current == root) {
//为根节点,清空树
root = null;
}else {
if (isRightChild) {
//删除节点是其父节点的右节点,则置空父节点的右节点
parent.right = null;
}else {
//删除节点是其父节点的左节点,则置空父节点的左节点
parent.left = null;
}
}
return true;
}
//2、删除的节点,有一个子节点
if (current.left == null) {
//左子树为空,则肯定有一个右节点
if (current == root) {
root = current.right;
}else {
if (isRightChild) {
//删除节点是其父节点的右节点,则将其父节点的右节点设为删除节点的右节点
parent.right = current.right;
}else {
//删除节点是其父节点的左节点,则将其父节点的左节点设为删除节点的右节点
parent.left = current.right;
}
}
return true;
}
if (current.right == null) {
//右子树为空,则肯定有一个左节点
if (current == root) {
root = current.left;
}else {
if (isRightChild) {
//删除节点是其父节点的右节点,则将其父节点的右节点设为删除节点的左节点
parent.right = current.left;
}else {
//删除节点是其父节点的左节点,则将其父节点的左节点设为删除节点的左节点
parent.left = current.left;
}
}
return true;
}
//3、有两个节点,此时需要确认:后继节点
//后继节点的概念,如果将一棵二叉树按照中序周游的方式输出,则任一节点的下一个节点就是该节点的后继节点。
TreeNode successor=delHelperGetSuccessor(current);
//右子树为空,则肯定有一个左节点
if (current == root) {
root = successor;
}else {
if (isRightChild) {
//删除节点是其父节点的右节点,则将其父节点的右节点设为删除节点的左节点
parent.right = successor;
}else {
//删除节点是其父节点的左节点,则将其父节点的左节点设为删除节点的左节点
parent.left = successor;
}
}
return true;
}
/**
* @Description: 确认删除节点的后继节点(中序遍历的下一个),并处理完成后继节点的结构
* @Author: zongx
* @Date: 2020/1/13
* @Param: current
* @return com.suirui.binarytree.TreeNode
*/
private TreeNode delHelperGetSuccessor(TreeNode delNode) {
//记录后继者的父节点,初始是要删除节点
TreeNode successorParent = delNode;
//记录后继节点,初始是父节点的右子树的根节点
TreeNode successor = delNode.right;
//因为是中序遍历,其实就是找右子树的最左端元素
TreeNode current = delNode.right;
while (current != null) {
successorParent = successor;
successor = current;
current = current.left;
}
//此时的successor就是后继节点
//下面根据情况构建后继节点的结构
//判断要删除节点的删除节点右子树是否有左子树---这个依据是中序遍历的顺序:先遍历左子树,输出根节点,再遍历右子树。
// 构建后继节点的右子树
if (successor != delNode.right) {
//如果后继节点直接不是右子树根节点,则后继节点肯定在右子树的左子树上,则需要优化树结构
//1、需要将后继节点的右孩子(后继节点只可能有右孩子),放到后继节点父节点的左孩子上
successorParent.left = successor.right;
//2、处理后继节点升级到删除节点位置时,仅当前情况(后继节点在删除节点右子树的左子树上)需要处理
//将要删除节点的右子树给后继节点的右子树
successor.right = delNode.right;
}
//构建后继节点的左子树
successor.left = delNode.left;
return successor;
}