最全解析之二叉搜索树:高效查找与增删

一、什么是二叉搜索树?

1、二叉搜索树的定义:

若它的左子树不为空,则左子树的所有结点的值均小于它的根结点的值,若它的右子树不为空,则它的右子树所有结点的值均大于它的根结点的值,它的子树也为二叉搜索树。仅仅看定义或许有些抽象,下面我将画图展示一颗二叉搜索树以便更好地理解二叉搜索树的定义!

如上图所示,左子树的所有结点的值均小于根结点的值,右结点所有子树的值均大于根节点的值,符合这种规则的树可以称之为一颗二叉搜索树,也称为二叉查找树。

二、二叉搜索树的插入:

二叉搜索树的插入操作分为两种情况:

第一种情况就是当二叉搜索树为空时,则直接插入结点,并将其赋值给根结点,此时我将根节结点命名为_root。

第二种情况,二叉搜索树不为空,则按二叉搜索树的性质插入位置,插入新结点即可!

		bool insert(const k& key, const v& value)
		{
			if (_root == nullptr)
			{
				_root = new node(key, value);
				return true;
			}
			node* cur = _root;
			node* parents = nullptr;
			while (cur)
			{
				if (cur->_key < key)
				{
					parents = cur;
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					parents = cur;
					cur = cur->_left;
				}
				else
				{
					return false;//碰到的某个结点和要插入的节点key相等,不能重复插入
				}
			}
			cur = new node(key, value);
			if (parents->_key < key)
			{
				parents->_right = cur;
			}
			else
			{
				parents->_left = cur;
			}
			return true;
		}

二叉搜索树的插入操作分为两个步骤,先要进行查找,其次才是插入。

cur=_root,当cur不为空的时候循环就一直执行,如果它的key值小于待插入结点的key值,那么cur就往它的右子树走,parents=cur,是为了能够找到cur的上一级,如果大于待插入结点的key值,那么cur就往左子树走,如果都不满足,证明此时二叉搜索树中存在和待插入结点具有相等key的结点,二叉搜索树不能插入重复结点,所以return false,否则创建一个结点,再判断把待插入结点是插入到父结点的左结点还是右结点,插入完成后,return true即可完成插入操作!

三、二叉搜索树的查找功能:

二叉搜索树,也叫二叉查找树,那么查找功能一定是二叉搜索树必备的,二叉搜索树的查找步骤如下所示:

先从根结点开始进行比较,比根大则往右边走,比根小则往左边走,因为小于根结点的都在根节点的左子树,大于根结点的都在根结点的右子树!

最多查找高度次,如果走到空了但是还没有找到,那么这个值并不存在!

二叉搜索树的效率之高在于它最多查找它的高度次,时间复杂度在绝大多数情况下为logn,但是右一种情况需要注意。

在这种情况下,二叉搜索树的左子树为空,那么二叉搜索树退化成一条链表,也就是最坏情况下,二叉搜索树查找的时间复杂度为o(n),为了解决这个问题,后面有AVL树和红黑树,本次就不做详解了,后续的文章会进行详解。

bool Find(const K& key)
		{
			Node* cur = _root;
			while (cur)
			{
				if (cur->_key < key)
				{
					cur = cur->_right;
				}
				else if (cur->_key > key)
				{
					cur = cur->_left;
				}
				else
				{
					return true;
				}
			}

			return false;
		}

查找只需要按照二叉搜索树的性质进行查找即可,这里还是比较好理解的,我就不做解释了!接着往下看

四、二叉搜索树的删除:

二叉搜索树的删除算是这三个常见操作里面最复杂的,也需要分情况进行讨论。

第一种情况:空树,这种情况直接返回false即可,这里就不解释了。

第二种情况:左为空,且此结点为根结点:

这种情况,只需要把新的根为cur的右结点即可,也就是cur->_right。

第三种情况:左为空,但它不是根结点:

要删除结点12(cur),那么这种情况只需要把12父节点的左指针指向cur的右结点即可,这样15<24<37,依然满足二叉搜索树的规则,删除完成。

这里,第二种情况和第三种情况可以合并为一种情况讨论,那么只需要讨论要删除的结点是否为根结点即可了。

第四种情况:右为空,也需要讨论是否为根结点:

1、如果要删除的结点右结点为空,并且还是根结点的话,与第二种情况类似的处理方法,只需要把新的根结点变成原来根结点的左结点即可!

2、要删除的结点不为根结点:

只需要把待删除结点的父结点的右指针指向待删除结点(cur)的左结点即可!

第五种情况:左右都不为空,这种情况也是最难处理的,如果为根结点:

如上图所示,如果要删除45那么该怎么删除呢?直接删除吗?很显然是不行的,那么我们将采取一种更为高效且巧妙的方式进行删除,首先,我们要找到右子树的最小结点,因为即使是根结点右子树的最小结点也比根结点要大,用右子树的最小结点,也就是这里的50,去代替45,那么再把代替结点删除即可!

这样删除之后依然满足左子树都比50小,右子树都比50大,这样就可以把45删除了,至此,二叉搜索树的删除操作讲解完毕!

删除操作代码如下:

			bool erase(const k& key)
			{
				node* cur = _root;
				node* parents = nullptr;
				while (cur)
				{
					if (cur->_key < key)
					{
						parents = cur;
						cur = cur->_right;
					}
					else if (cur->_key > key)
					{
						parents = cur;
						cur = cur->_left;
					}
					else
					{
						//1、左为空
						if (cur->_left == nullptr)
						{
							if (cur == _root)
							{
								_root = cur->_right;
							}
							else
							{
								if (parents->_left = cur)
								{
									parents->_right = cur->_right;
								}
								else if (parents->_right = cur)
								{
									parents->_right = cur->_right;
								}
								delete cur;
							}
						}
						else if (cur->_right == nullptr)//2、右为空
						{
							if (cur == _root)
							{
								_root = cur->_left;
							}
							else
							{
								if (parents->_left = cur)
								{
									parents->_left = cur->_left;
								}
								else if (parents->_right = cur)
								{
									parents->_right = cur->_left;
								}
								delete cur;
							}
						}
						else//左右都不为空
						{
							node* minright = cur;
							node* pminright = cur->_right;
							while (minright->_left)
							{
								pminright = minright;
								minright = minright->_left;
							}
							cur->_key = minright->_key;
							if (pminright->_left = minright)
							{
								pminright->_left = minright->_left;
							}
							else
							{
								pminright->_right = minright->_right;
							}
							delete minright;
						}
						return true;
						}
					}
						return false;
					}

五、二叉搜索树的应用:

补充:对二叉搜索树进行中序遍历得到的是有序的元素。

KV模型:

<key,value>的键值对,下面将用这个模型,实现一个统计学生成绩的程序,代码如下:

// 二叉搜索树类(KV 模型)
template<class K, class V>
class BSTree {
    typedef BSTNode<K, V> Node;
public:
    // 构造函数
    BSTree() : _root(nullptr) {}

    // 插入函数:按学号插入,重复则更新成绩
    bool Insert(const K& id, const V& score = V()) {
        if (_root == nullptr) {
            _root = new Node(id, score);
            return true;
        }

        Node* cur = _root;
        Node* parent = nullptr;
        while (cur) {
            if (id < cur->_id) {
                parent = cur;
                cur = cur->_left;
            }
            else if (id > cur->_id) {
                parent = cur;
                cur = cur->_right;
            }
            else {
                // 学号已存在,更新成绩
                cur->_score = score;
                return true;
            }
        }

        // 插入新节点
        cur = new Node(id, score);
        if (id < parent->_id) {
            parent->_left = cur;
        }
        else {
            parent->_right = cur;
        }
        return true;
    }

    // 查询函数:按学号查找对应的成绩
    bool Search(const K& id, V& score) const {
        Node* cur = _root;
        while (cur) {
            if (id < cur->_id) {
                cur = cur->_left;
            }
            else if (id > cur->_id) {
                cur = cur->_right;
            }
            else {
                score = cur->_score;
                return true;
            }
        }
        return false; // 未找到
    }

    // 中序遍历(用于调试,按学号升序输出)
    void InorderPrint() const {
        _InorderPrint(_root);
        cout << endl;
    }

private:
    // 递归中序遍历
    void _InorderPrint(Node* root) const {
        if (root == nullptr) return;

        _InorderPrint(root->_left);
        cout << "学号:" << root->_id << ",成绩:" << root->_score << endl;
        _InorderPrint(root->_right);
    }

    Node* _root; // 根节点指针
};

// 模拟录入学生成绩并统计查询
void StudentScoreManager() {
    BSTree<int, int> bst; // 键:学号(int),值:成绩(int)

    // 模拟插入成绩(可替换为用户输入或文件读取)
    bst.Insert(101, 95);
    bst.Insert(103, 88);
    bst.Insert(102, 92);
    bst.Insert(104, 79);
    bst.Insert(101, 96); // 重复学号,更新成绩

    // 输出所有学生成绩(按学号升序)
    cout << "学生成绩列表(按学号升序):" << endl;
    bst.InorderPrint();

    //查询指定学号成绩
    int targetId = 102;
    int score;
    if (bst.Search(targetId, score)) {
        cout << "学号 " << targetId << " 的成绩:" << score << endl;
    }
    else {
        cout << "未找到学号 " << targetId << endl;
    }
}

int main() {
    StudentScoreManager();
    return 0;
}

上述代码中,我对每一个功能加了清晰的注释,二叉搜索树中序遍历后就是顺序的,具有类似于排序的功能,我这里是按学号排序的,从小到大,并打印出了对应学号的对应成绩。

二叉搜索树还要很多的应用场景,具体的我就不多做展开了,二叉搜索树也是map和set的基础,map和set的底层实现为红黑树,这里就不多讲了,后续的文章可能会详细讲解。

本章完,如果您看完上面的内容有所收获,希望可以点赞收藏以防丢失,可以留个关注!

创造不易,本人水平有限,如果有错误或者写的不好的地方可在评论区进行更正,共勉!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值