一、二叉搜索树的特征
1. 二叉树与二叉搜索树
所谓二叉搜索树,其实本质上就是一个树状的数据结构,也是有一个根节点和若干个子树所组成的,其基于二叉树的结构,而在其数据的填充位置上所进行优化而得到的数据结构,并运用二叉搜索树数据的分布特点来实现在搜索功能。二叉树的结构特征如下图:
2.二分查找
基于二叉搜索树每个节点中的数值大小均满足:左子节点的值 < 根节点的值 < 右子节点的值的特点,二叉搜索树在搜索值时是依照二分来进行查找的,即先将待查找的值与每棵子树的根节点的值进行比较,当待查找的值小于每棵子树的根节点的值时,到该根节点的左子树递归查找;当待查找的值大于每棵子树的根节点的值时,到该根节点的右子树递归查找;待查找的值等于每棵子树的根节点的值时,即找到待查找的值所在的节点。除此之外,在二叉搜索树进行删除节点,添加节点,修改节点等操作时也会使用二分查找的思想。相比较于链表而言,二叉搜索树在搜索查找方面的效率要提高不少。
3.递归
二叉树的许多相关操作是和递归密不可分的。从上述二叉树的结构图中,我们不难看出,二叉树是由若干个由一个根节点和左右两个叶子节点构成的子树所组成的;而每棵子树的根节点可能是其上级子树的叶子节点,而叶子节点也可能是其下级子树的根节点,这也就表明了递归在二叉树结构中的使用。也正是由于二叉搜索树与二叉树相同的结构特点,使得二叉搜索树在许多相关操作和相关问题的解决上均会运用到递归这种实现。
二、二叉搜索树相关操作的实现
1.添加操作
二叉搜索树增加节点的操作和链表的增加操作相类似,均是通过增加节点中指针的指向,从而来添加新的节点元素。与链表不同的是,二叉搜索树在执行增加节点的操作时会遵守 左子节点的值 < 根节点的值 < 右子节点的值 的结构特征,将待添加的值同树根节点的值比较大小,并更加大小的比较情况,递归地将待添加值与左右子树的节点比较,以此来查找到添加的位置,这也是二分查找思想的运用。
代码实现
// 定义二叉树的节点类
class Node {
int val; // 节点存储的值
Node lchild; // 左子节点
Node rchild; // 右子节点
// 构造函数,初始化节点
public Node(int val) {
this.val = val;
this.lchild = null;
this.rchild = null;
}
}
// 定义二叉树类
public class BinaryTree {
private Node root; // 二叉树的根节点
// 构造函数,初始化根节点为null
public BinaryTree() {
this.root = null;
}
// 公共方法,用于添加节点
public void addNode(int val) {
// 如果根节点为空,直接创建根节点
if (root == null) {
root = new Node(val);
} else {
// 如果根节点不为空,递归地添加节点
this.addNode(root, val);
}
}
// 私有递归方法,用于在二叉树中添加节点
private void addNode(Node node, int val) {
// 如果待添加的值小于当前节点的值
if (val < node.val) {
// 如果当前节点没有左子节点,则将待添加的节点作为左子节点
if (node.lchild == null) {
node.lchild = new Node(val);
} else {
// 如果当前节点有左子节点,则递归地向左子树添加节点
this.addNode(node.lchild, val);
}
} else {
// 如果待添加的值大于或等于当前节点的值
if (node.rchild == null) {
// 如果当前节点没有右子节点,则将待添加的节点作为右子节点
node.rchild = new Node(val);
} else {
// 如果当前节点有右子节点,则递归地向右子树添加节点
this.addNode(node.rchild, val);
}
}
}
// 主方法,用于测试二叉树的添加节点功能
public static void main(String[] args) {
BinaryTree tree = new BinaryTree();
tree.addNode(10);
tree.addNode(5);
tree.addNode(15);
tree.addNode(3);
tree.addNode(7);
tree.addNode(12);
// 可以添加更多节点,或者实现遍历方法来验证树的结构
}
}
时间复杂度
最坏情况:当树上的节点全都集中在根节点的某一侧时,二叉树在上就会成为一个单链结构,即添加操作的时间复杂度将会为 O(n) ,其中 n 取决于二叉树上节点的个数。
一般情况:根据二叉树就结构的特征,其在添加操作上的时间复杂度和二分查找算法的时间复杂度相类似,即添加操作的平均时间复杂度为:O(log n)
4.查找操作
同上文所述,基于二叉搜索树 左子节点的值 < 根节点的值 < 右子节点的值 的结构特征,即可通过二分查找的思想来查找我们所要查找的元素节点,即先将待查找的值与每棵子树的根节点的值进行比较,当待查找的值小于每棵子树的根节点的值时,到该根节点的左子树递归查找;当待查找的值大于每棵子树的根节点的值时,到该根节点的右子树递归查找;待查找的值等于每棵子树的根节点的值时,即找到待查找的值所在的节点。
代码实现
// 定义二叉树节点的类
class TreeNode {
// 节点存储的值
int val;
// 左子节点的引用
TreeNode lchild;
// 右子节点的引用
TreeNode rchild;
// 构造函数,初始化节点的值和子节点
TreeNode(int val) {
this.val = val;
this.lchild = null;
this.rchild = null;
}
}
// 定义二叉搜索树的类
public class BinarySearchTree {
// 根节点的引用
TreeNode root;
// 构造函数,初始化根节点为null
public BinarySearchTree() {
this.root = null;
}
/**
* 在二叉搜索树中查找特定值的节点
* @param root 当前遍历到的节点
* @param val 要查找的值
* @return 如果找到,则返回对应的TreeNode节点;否则返回null
*/
public TreeNode BST_Search(TreeNode root, int val) {
// 如果当前节点为空,说明没有找到,返回null
if (root == null) {
return null;
}
// 如果当前节点的值等于要查找的值,则返回当前节点
if (val == root.val) {
return root;
} else if (val < root.val) {
// 如果要查找的值小于当前节点的值,则在左子树上递归查找
return BST_Search(root.lchild, val);
} else {
// 如果要查找的值大于当前节点的值,则在右子树上递归查找
return BST_Search(root.rchild, val);
}
}
}
时间复杂度
与上述的操作相类似。
最坏情况:当树上的节点均集中在其根节点的用一侧时,这棵树将会形成一个单链的结构,故此时删除操作的时间复杂度为:O(n),其中 n 取决于树中节点的个数。
一般情况:在二叉搜索树进行删除操作时,会优先查找要进行删除的节点,故删除操作的时间复杂度也为 O(log n)。
4.插入操作
二叉搜索树即是根据 左子节点的值 < 根节点的值 < 右子节点的值 的规则,将数据插入到树中符合条件的节点上。运用二分查找和递归的思想,当待插入的值小于当前节点的值时,在当前节点的左孩子递归插入;当待插入的节点大于当前节点的值时,在当前节点的右孩子递归插入。
代码实现
// 定义二叉树节点的类
class TreeNode {
int val;
TreeNode left; // 左子节点
TreeNode right; // 右子节点
// 构造函数,初始化节点的值和子节点
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// 定义二叉搜索树的类
public class BinarySearchTree {
// 根节点的引用
TreeNode root;
// 构造函数,初始化根节点为null
public BinarySearchTree() {
this.root = null;
}
/**
* 在二叉搜索树中插入新值
* @param root 当前遍历到的节点
* @param val 要插入的值
* @return 返回插入新节点后的根节点
*/
public TreeNode BST_Insert(TreeNode root, int val) {
// 如果当前节点为空,说明树为空,需要插入新节点
if (root == null) {
return new TreeNode(val);
}
// 如果待插入的值小于当前节点的值,则在左子树上递归插入
if (val < root.val) {
root.left = BST_Insert(root.left, val);
}
// 如果待插入的值大于当前节点的值,则在右子树上递归插入
else if (val > root.val) {
root.right = BST_Insert(root.right, val);
}
// 如果值已经存在,则不插入,直接返回当前节点
return root;
}
}
时间复杂度
最坏情况:当所有的节点全部都集中在树根节点的一侧时,这棵树就会形成单链的结构,此时插入操作的时间复杂度为:O(n),其中 n 取决于树上节点的个数。
一般情况:于上述的操作相类似,运用二分查找的思想,即二叉搜索树插入操作的一般时间复杂度为:O(log n)
5.创建二叉搜索树
创建二叉搜索树,即是将传入的一组数据按照 左子节点的值 < 根节点的值 < 右子节点的值 的规则包装成树状的结构。这个过程可以看成是将传入的数据先封装成节点,再将封装后的节点插入树中。
代码实现
// 定义二叉树节点的类
class TreeNode {
int val;
TreeNode left; // 左子节点
TreeNode right; // 右子节点
// 构造函数,初始化节点的值和子节点
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// 定义二叉搜索树的类
public class BinarySearchTree {
// 根节点的引用
TreeNode root;
// 构造函数,初始化根节点为null
public BinarySearchTree() {
this.root = null;
}
/**
* 在二叉搜索树中插入新值
* @param root 当前遍历到的节点
* @param val 要插入的值
* @return 返回插入新节点后的根节点
*/
public TreeNode BST_Insert(TreeNode root, int val) {
// 如果当前节点为空,说明树为空,需要插入新节点
if (root == null) {
return new TreeNode(val);
}
// 如果待插入的值小于当前节点的值,则在左子树上递归插入
if (val < root.val) {
root.left = BST_Insert(root.left, val);
}
// 如果待插入的值大于当前节点的值,则在右子树上递归插入
else if (val > root.val) {
root.right = BST_Insert(root.right, val);
}
// 如果值已经存在,则不插入,直接返回当前节点
return root;
}
/**
* 根据给定的值创建二叉搜索树
* @param nums 包含要插入的值的数组
* @return 返回创建的二叉搜索树的根节点
*/
public TreeNode BST_Build(int[] nums) {
if (nums == null || nums.length == 0) {
return null;
}
// 首先创建一个树的根节点
root = new TreeNode(nums[0]);
// 循环遍历数组,将剩余的值插入树中
for (int i = 1; i < nums.length; i++) {
BST_Insert(root, nums[i]);
}
return root;
}
}
三、二叉搜索树的完整Java代码
// 定义二叉树节点的类
class Node {
int val;
Node lchild; // 左子树
Node rchild; // 右子树
// 构造函数,初始化节点的值和子树
Node(int val) {
this.val = val;
this.lchild = null;
this.rchild = null;
}
}
// 定义二叉搜索树的类
public class BST {
private Node root; // 根节点
private int size; // 节点个数
// 构造函数,初始化根节点为null
public BST() {
this.root = null;
this.size = 0;
}
// 判断二叉搜索树是否为空
public boolean isEmpty() {
return root == null;
}
// 获取二叉搜索树的节点个数
public int size() {
return size;
}
// 清空二叉搜索树
public void clear() {
this.root = null;
}
// 添加节点的方法
public void addNode(int val) {
this.root = addNode(this.root, val);
this.size++;
}
// 递归插入节点的方法
private Node addNode(Node node, int val) {
if (node == null) {
return new Node(val);
}
if (val < node.val) {
node.lchild = addNode(node.lchild, val);
} else if (val > node.val) {
node.rchild = addNode(node.rchild, val);
}
return node;
}
// 二叉搜索树的查找方法
public Node BSTSearch(Node root, int val) {
if (root == null) {
return null;
}
if (val == root.val) {
return root;
} else if (val < root.val) {
return BSTSearch(root.lchild, val);
} else {
return BSTSearch(root.rchild, val);
}
}
// 二叉搜索树的插入方法
public Node BSTInsert(Node root, int val) {
if (root == null) {
return new Node(val);
}
if (val < root.val) {
root.lchild = BSTInsert(root.lchild, val);
} else if (val > root.val) {
root.rchild = BSTInsert(root.rchild, val);
}
return root;
}
// 二叉搜索树的创建方法
public Node BSTBuild(int[] nums) {
if (nums == null || nums.length == 0) {
return null;
}
this.root = new Node(nums[0]);
for (int i = 1; i < nums.length; i++) {
addNode(nums[i]);
}
return this.root;
}
// 二叉搜索树的删除方法
public Node BSTDelete(Node root, int val) {
if (root == null) {
return null;
}
if (val < root.val) {
root.lchild = BSTDelete(root.lchild, val);
} else if (val > root.val) {
root.rchild = BSTDelete(root.rchild, val);
} else {
if (root.lchild == null) {
return root.rchild;
} else if (root.rchild == null) {
return root.lchild;
} else {
Node cur = root.rchild;
Node parent = root;
while (cur.lchild != null) {
parent = cur;
cur = cur.lchild;
}
root.val = cur.val;
if (parent != root) {
parent.lchild = cur.rchild;
} else {
parent.rchild = cur.rchild;
}
}
}
return root;
}
}
四、总结
二叉搜索树实质上是建立在二叉树上,具有搜索功能的数据结构。相比较于数组和链表,二叉搜索树运用了二分查找的算法思想,将对多个数据进行相关操作的时间复杂度控制在 O(log n),提高了对大规模数据进行操作的时间效率。