合集地址
二叉树合集(一):二叉树基础(含四种遍历,图文详解)
二叉树合集(二):霍夫曼树(图文详解)
二叉树合集(三):线索二叉树(图文详解)
二叉树合集(四):对称二叉树(递归和迭代实现)
二叉树合集(五):二叉搜索树(图片详解,含基本操作)
二叉树合集(六):高度平衡二叉树
定义
二叉搜索树
(BST)是二叉树的一种特殊表示形式,它满足如下特性:
- 每个节点中的值必须
大于
(或等于)存储在其左侧子树中的任何值。 - 每个节点中的值必须
小于
(或等于)存储在其右子树中的任何值。
下面是一个二叉搜索树的例子:
像普通的二叉树一样,我们可以按照前序、中序和后序来遍历一个二叉搜索树。 但是值得注意的是,对于二叉搜索树,我们可以通过中序遍历
得到一个递增的
有序序列。因此,中序遍历是二叉搜索树中最常用的遍历方法。
二叉搜索树代码实现
1、二叉搜索树的结点
每个二叉搜索树的结点,包含关键字key、左孩子指针lchild、右孩子指针rchild以及父结点指针parent。在C++实现中,我们定义一个结点类BSTNode来表示一个结点,并初始化结点的关键字等于0,左右孩子指针和父结点指针为NIL。
/* 二叉搜索树节点 */
class BSTNode
{
private:
double key; // 关键字
BSTNode *lchild; // 左孩子
BSTNode *rchild; // 右孩子
BSTNode *parent; // 父节点
friend class BSTree;
public:
BSTNode(double k = 0.0, BSTNode *l = NULL, BSTNode *r = NULL, BSTNode *p = NULL) :key(k), lchild(l), rchild(r), parent(p){}
};
二叉搜索树的基本操作
二叉搜索树主要支持三个操作:搜索、插入和删除。
在二叉搜索树中实现搜索操作
根据BST的特性,对于每个节点:
- 如果目标值等于节点的值,则返回节点;
- 如果目标值小于节点的值,则继续在左子树中搜索;
- 如果目标值大于节点的值,则继续在右子树中搜索。
我们一起来看一个例子:我们在上面的二叉搜索树中搜索目标值为 4 的节点。
class Solution {
public TreeNode searchBST(TreeNode root, int val) {
if (root == null || val == root.val) return root;
return val < root.val ? searchBST(root.left, val) : searchBST(root.right, val);
}
}
在二叉搜索树中实现插入操作
二叉搜索树中的另一个常见操作是插入一个新节点。有许多不同的方法去插入新节点,我们只讨论一种使整体操作变化最小的经典方法。 它的主要思想是为目标节点找出合适的叶节点位置,然后将该节点作为叶节点插入。 因此,搜索将成为插入的起始。
与搜索操作类似,对于每个节点,我们将:
- 根据节点值与目标节点值的关系,搜索左子树或右子树;
- 重复步骤 1 直到到达外部节点;
- 根据节点的值与目标节点的值的关系,将新节点添加为其左侧或右侧的子节点。
这样,我们就可以添加一个新的节点并依旧维持二叉搜索树的性质。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode insertIntoBST(TreeNode root, int val) {
TreeNode node= root;
while(node!=null){
if(val>node.val){
if(node.right==null){
node.right = new TreeNode(val);
return root;
}
else node = node.right;
}
if(val<node.val){
if(node.left==null){
node.left = new TreeNode(val);
return root;
}
else node = node.left;
}
}
return new TreeNode(val);
}
}
在二叉搜索树中实现删除操作
删除要比我们前面提到过的两种操作复杂许多。有许多不同的删除节点的方法,这篇文章中,我们只讨论一种使整体操作变化最小的方法。我们的方案是用一个合适的子节点来替换要删除的目标节点。根据其子节点的个数,我们需考虑以下三种情况:
- 如果目标节点***没有子节点***,我们可以直接移除该目标节点。
2. 如果目标节***只有一个子节点***,我们可以用其子节点作为替换。
3. 如果目标节点***有两个子节点***,我们需要用其中序后继节点或者前驱节点来替换,再删除该目标节点。
我们来看下面这几个例子,以帮助你理解删除操作的中心思想:
例 1:目标节点没有子节点
例 2:目标节只有一个子节点
例 3:目标节点有两个子节点
通过理解以上的示例,你应该可以独立实现删除操作了。
public int successor(TreeNode root) {
root = root.right;
while (root.left != null) root = root.left;
return root.val;
}
/*
One step left and then always right
*/
public int predecessor(TreeNode root) {
root = root.left;
while (root.right != null) root = root.right;
return root.val;
}
public TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
// delete from the right subtree
if (key > root.val) root.right = deleteNode(root.right, key);
// delete from the left subtree
else if (key < root.val) root.left = deleteNode(root.left, key);
// delete the current node
else {
// the node is a leaf
if (root.left == null && root.right == null) root = null;
// the node is not a leaf and has a right child
else if (root.right != null) {
root.val = successor(root);
root.right = deleteNode(root.right, root.val);
}
// the node is not a leaf, has no right child, and has a left child
else {
root.val = predecessor(root);
root.left = deleteNode(root.left, root.val);
}
}
return root;
}
}
小结
我们已经介绍了二叉搜索树的相关特性,以及如何在二叉搜索树中实现一些基本操作,比如搜索、插入和删除。熟悉了这些基本概念之后,相信你已经能够成功运用它们来解决二叉搜索树问题。
二叉搜索树的有优点是,即便在最坏的情况下,也允许你在O(h)
的时间复杂度内执行所有的搜索、插入、删除操作。
通常来说,如果你想有序地存储数据或者需要同时执行搜索、插入、删除等多步操作,二叉搜索树这个数据结构是一个很好的选择。
一个例子
问题描述:设计一个类,求一个数据流中第k大的数。
一个很显而易见的解法是,先将数组降序排列好,然后返回数组中第k个数。
但这个解法的缺点在于,为了在O(1)
时间内执行搜索操作,每次插入一个新值都需要重新排列元素的位置。从而使得插入操作的解法平均时间复杂度变为O(N)
。因此,算法总时间复杂度会变为O(N^2)
。
鉴于我们同时需要插入和搜索操作,为什么不考虑使用一个二叉搜索树结构存储数据呢?
我们知道,对于二叉搜索树的每个节点来说,它的左子树
上所有结点的值均小于
它的根结点的值,右子树
上所有结点的值均大于
它的根结点的值。
换言之,对于二叉搜索树的每个节点来说,若其左子树共有m
个节点,那么该节点是组成二叉搜索树的有序数组中第m + 1
个值。
操作的解法平均时间复杂度变为O(N)
。因此,算法总时间复杂度会变为O(N^2)
。
鉴于我们同时需要插入和搜索操作,为什么不考虑使用一个二叉搜索树结构存储数据呢?
我们知道,对于二叉搜索树的每个节点来说,它的左子树
上所有结点的值均小于
它的根结点的值,右子树
上所有结点的值均大于
它的根结点的值。
换言之,对于二叉搜索树的每个节点来说,若其左子树共有m
个节点,那么该节点是组成二叉搜索树的有序数组中第m + 1
个值。
你可以先独立思考这个问题。请先尝试把多个节点存储到树中。你可能还需要在每个节点中放置一个计数器,以计算以此节点为根的子树中有多少个节点。