【数据结构】基础:二叉搜索树

【数据结构】基础:二叉搜索树

摘要:本文为二叉树的进阶,主要介绍其概念与基本实现(递归与非递归),再介绍其应用,主要介绍内容为KV模型。最后为简单的性能分析。


一、概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

二、二叉搜索树的实现

在此使用c++进行最普通的二叉搜索树的实现,首先需要对节点进行定义,节点包括的内容为左右指针以及键值,其次再对该树进行定义,只需要对其根节点进行定义即可,代码示例如下:

template <class K>
class BinarySearchTreeNode {
public:
	BinarySearchTreeNode<K>* left_;
	BinarySearchTreeNode<K>* right_;
	K key_;
	BinarySearchTreeNode(const K& key)
		:left_(nullptr)
		, right_(nullptr)
		, key_(key)
	{}
};

template <class  K>
class BinarySearchTree {
private:
	BinarySearchTreeNode<K>* root_;
public:
	BinarySearchTree()
		:root_(nullptr)
	{}
};

三、非递归实现相关操作

3.1 插入

与一般的二叉树不同,插入节点时需要保护二叉搜索树的结构,因此对以下情况进行分类讨论:

  • 对于空树,直接构建节点并插入
  • 若不为空树,根据二叉搜索树的特性,寻找合适位置进行插入

代码示例如下:

bool Insert(const K& key) {
    // 对于空树,直接构建节点并插入
    if (root_ == nullptr) { 
        root_ = new BinarySearchTreeNode<K>(key);
        return true;
    }
    // 若不为空树,根据二叉搜索树的特性,寻找合适位置进行插入
    BinarySearchTreeNode<K>* parent = nullptr;
    BinarySearchTreeNode<K>* cur = root_;
    while (cur) {
        if (cur->key_ < key) {
            parent = cur;
            cur = parent->right_;
        }
        else if (cur->key_ > key) {
            parent = cur;
            cur = parent->left_;
        }
        else
            return false;
    }
    cur = new BinarySearchTreeNode<K>(key);
    // 虽然找到位置,但忘记了是插到左边还是右边,重新判断一下
    if (cur->key_ < parent->key_) {
        parent->left_ = cur;
    }
    else {
        parent->right_ = cur;
    }
    return true;
}

3.2 查找

根据二叉搜索树的性质进行查找,当遇到较大的节点找右子树,较小的节点找左子树,相等返回,如果不存在则返回空节点

bool Find(const K& key) {
    BinarySearchTreeNode<K>* cur = root_;
    while (cur) {
        if (cur->key_ == key) {
            return true;
        }
        else if (cur->key_ > key) {
            cur = cur->right_;
        }
        else if (cur->key_ < key) {
            cur = cur->left_;
        }
    }
    return false;
}

3.3 删除

对于二叉搜索树的删除是较为复杂的,当删除叶子节点或只有一个子节点的节点是较为简单的,因为这样不太困难的保持树的性质,然而对于存在两个叶子节点的树删除过程是比较困难的,为此需要对各个情况进行分类讨论,过程如下:

情况一:需要删去的节点cur没有子节点

此时,只需要将父节点parent指向该节点指针指向空指针nullptr即可,也可以理解为指向该节点cur的子节点。

情况二:需要删去的节点cur存在一个子节点

此时需要将父节点parent指向该节点cur指向需要删去的节点cur的非空子节点

以上两种情况可以将其归类为同一种删除方式,即将父节点指向需要删除节点的子节点,如果子节点非空,则优先指向该节点。

因此可写下如下代码:

// 情况一:不存在子节点
// 情况二:存在一个子节点,该子节点为右节点
if (cur->left_ == nullptr) {
    if (parent->left_ == cur) {
        parent->left_ = cur->right_;
    }
    else {
        parent->right_ = cur->right_;
    }
}
// 情况二:存在一个子节点,该子节点为左节点
else if (cur->right_ == nullptr){
    if (parent->left_ == cur) {
        parent->left_ = cur->left_;
    }
    else {
        parent->right_ = cur->left_;
    }
}

但是,同时需要考虑到如果需要删除的节点是根节点的情况,此时的parent是一个空节点,此特殊情况直接对根节点进行设置即可,代码修改如下:

// 情况一:不存在子节点
// 情况二:存在一个子节点,该子节点为右节点
if (cur->left_ == nullptr) {
    // 删除的为根节点
    if (parent == nullptr) {
        root_ = cur->right_;
    }
    else {
        if (parent->left_ == cur) {
            parent->left_ = cur->right_;
        }
        else {
            parent->right_ = cur->right_;
        }
    }
    delete cur;
}
// 情况二:存在一个子节点,该子节点为左节点
else if (cur->right_ == nullptr){
    // 删除的为根节点
    if (parent == nullptr) {
        root_ = cur->left_;
    }
    else {
        if (parent->left_ == cur) {
            parent->left_ = cur->left_;
        }
        else {
            parent->right_ = cur->left_;
        }
    }
    delete cur;
}

情况三:存在两个子节点,此时采用替代法。所谓替代法就是在子树中寻找一个适合的节点进行替换,一般为左子树的最大节点或右子树的最小节点。此处以右子树的最小节点进行举例,对于右子树的最小节点无疑有两种情况,分别为不存在子节点或者只存在右节点,因为不符合以上两种状态就是不是右子树最小节点。

方法为:

  • 从需要删除的右子树开始找最小子节点,由于二叉搜索树的特性,比较小的节点只能在右节点,如果右节点不存在则没有比其更小的节点,此处使用递归完成寻找
  • 由于进行替换后,还需要保持二叉树的连接,需要引入最小节点的父节点,该父节点需要赋值为需要删除的节点,如果赋值为空的话,可能会因为最小节点是右子树的根,此时父节点仍然是空,会导致程序崩溃
  • 在找到最小节点后,进行替换,覆盖和交换都无所谓,删除方法,因为与值无关
  • 在删除前,需要连接好二叉树,将父节点原本指向最小节点的指针指向最小节点的子节点,非空节点优先,而且只有是右节点有可能,因此直接指向右节点即可

// 情况三:存在两个子节点
else {
    // 从需要删除的右子树开始找最小子节点
    BinarySearchTreeNode<K>* min = cur->right_;
    // 对应的父节点
    BinarySearchTreeNode<K>* minParent = cur;
    // 找最小节点
    while (min->left_) {
        minParent = min;
        min = min->left_;
    }
    // 替换:覆盖法
    cur->key_ = min->key_;
    // 将父节点原本指向最小节点的指针指向最小节点的子节点,非空节点优先(只有右节点有可能)
    if (minParent->left_ == min) {
        minParent->left_ = min->right_;
    }
    else {
        minParent->right_ = min->right_;
    }
    delete min;
}

完整代码如下:

bool Erase(const K& key) {
    BinarySearchTreeNode<K>* parent = nullptr;
    BinarySearchTreeNode<K>* cur = root_;

    while(cur) {
        if (cur->key_ > key) {
            parent = cur;
            cur = cur->left_;
        }
        else if (cur->key_ < key) {
            parent = cur;
            cur = cur->right_;
        }
        else {
            // 情况一:不存在子节点
            // 情况二:存在一个子节点,该子节点为右节点
            if (cur->left_ == nullptr) {
                // 删除的为根节点
                if (parent == nullptr) {
                    root_ = cur->right_;
                }
                else {
                    if (parent->left_ == cur) {
                        parent->left_ = cur->right_;
                    }
                    else {
                        parent->right_ = cur->right_;
                    }
                }
                delete cur;
            }
            // 情况二:存在一个子节点,该子节点为左节点
            else if (cur->right_ == nullptr){
                // 删除的为根节点
                if (parent == nullptr) {
                    root_ = cur->left_;
                }
                else {
                    if (parent->left_ == cur) {
                        parent->left_ = cur->left_;
                    }
                    else {
                        parent->right_ = cur->left_;
                    }
                }
                delete cur;
            }
            // 情况三:存在两个子节点
            else {
                // 从需要删除的右子树开始找最小子节点
                BinarySearchTreeNode<K>* min = cur->right_;
                // 对应的父节点
                BinarySearchTreeNode<K>* minParent = cur;
                // 找最小节点
                while (min->left_) {
                    minParent = min;
                    min = min->left_;
                }
                // 替换:覆盖法
                cur->key_ = min->key_;
                // 将父节点原本指向最小节点的指针指向最小节点的子节点,非空节点优先(只有右节点有可能)
                if (minParent->left_ == min) {
                    minParent->left_ = min->right_;
                }
                else {
                    minParent->right_ = min->right_;
                }
                delete min;
            }
            return true;
        }
    }
    return false;
}

四、递归实现相关操作

4.1 插入

使用递归实现,我们可以将插入转换为子问题,如果空树,将会直接插入,如果不是空树且不存在与树中,将转换为插入到子树的子问题中,进行递归调用。需要注意的是这里的传参,可以使用引用或双指针,否则这里的递归传参是无效的传参。

由于这里设计的树的根节点是私有成员,因此对该递归函数进行了封装,代码示例如下:

public:
	bool Insert_Recursion(const K& key) {
		return _Insert_Recursion(root_, key);
	}
private:
	bool _Insert_Recursion(BinarySearchTreeNode<K>*& root, const K& key) {
		if (root == nullptr) {
			root = new BinarySearchTreeNode<K>(key);
			return true;
		}
		if (key > root->key_) {
			return _Insert_Recursion(root->right_, key);
		}
		else if (key < root->key_) {
			return _Insert_Recursion(root->left_, key);
		}
		else
			return false;
	}

4.2 查找

查找问题同样可以将其转换为子问题,但查找的树是空树或者找到了对应的节点时结束递归,否则根据大小转换为子树查找的子问题。代码示例如下:

public:
	BinarySearchTreeNode<K>* Find_Recurison(const K& key) {
		return _Find_Recurison(root_, key);
	}
private:
	BinarySearchTreeNode<K>* _Find_Recurison(BinarySearchTreeNode<K>* root, const K& key) {
		if (root == nullptr) {
			return nullptr;
		}
		else if (key == root->key_) {
			return root;
		}
		else if (key < root->key_) {
			return _Find_Recurison(root->left_, key);
		}
		else {
			return _Find_Recurison(root->right_, key);
		}
	}

4.3 删除

同样的思路将删除转换为子问题进行解决:

根据二叉树的性质将删除问题转换为子树进行删除的问题,结束条件为删除成功或者是该点不存在,当该点存在时,进行删除,判断其节点个数:

  • 若无节点或者只有一个节点,将父节点与子节点连接,优先非空子节点(由于是引用传递,因此传入的是父节点的别名,直接赋值即可)。
  • 如果存在两个节点,找出合适节点替换,如右子树最小节点,再进行递归删除该节点的右子树。

注意:非递归在书写时是不可以递归调用非递归删除函数的,因为非递归删除是从根节点开始删除,因此对于以根节点的二叉树来说,是不符合二叉搜索树的性质的。

public:
	bool Erase_Recursion(const K& key) {
		return _Erase_Recursion(root_, key);
	}
private:
	bool _Erase_Recursion(BinarySearchTreeNode<K>*& root, const K& key) {
		if (root == nullptr) {
			return false;
		}
		if (root->key_ < key) {
			_Erase_Recursion(root->right_, key);
		}
		else if(root->key_ > key) {
			_Erase_Recursion(root->left_, key);
		}
		else {
			BinarySearchTreeNode<K>* del = root;
			if (root->left_ == nullptr) {
				root = root->right_;
			}
			else if (root->right_ == nullptr) {
				root = root->left_;
			}
			else {
				BinarySearchTreeNode<K>* min = root->right_;
				while (min->left_)
				{
					min = min->left_;
				}
				std::swap(min->key_, root->key_);
				return _Erase_Recursion(root->right_, key);
			}
			delete del;
			return true;
		}
    }

五、应用

二叉搜索树的一般应用为:

  • 搜索功能:Key搜索模型、Key/Value搜索模型
  • 排序与去重功能

此处重点介绍Key搜索模型与Key/Value搜索模型

  • K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值
  • KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。

在之前实现的是K模型的,树的节点只有Key作为关键码储存,而对于KV模型,则需要用键值对进行储存。实际上可以简单的理解为一种检索,因此只需要对K模型进行简单的修改就可以完成KV模型的实现,主要步骤如下:

  • 引入新的成员Value,类型为由模板进行设定

  • 对相应的构造函数、插入函数进行修改,由于这些是与value相关的值

代码示例如下:

namespace ns_KeyValue {
	template <class K,class V>
	class BinarySearchTreeNode {
	public:
		BinarySearchTreeNode<K,V>* left_;
		BinarySearchTreeNode<K,V>* right_;
		K key_;
		V value_;
		BinarySearchTreeNode(const K& key,const V& value)
			:left_(nullptr)
			, right_(nullptr)
			, key_(key)
			, value_(value)
		{}
	};

	template <class  K,class V>
	class BinarySearchTree {
		typedef BinarySearchTreeNode<K, V> BinarySearchTreeNode;
	private:
		BinarySearchTreeNode* root_;
	private:
		void _InOrder(BinarySearchTreeNode* root)
		{
			if (root == nullptr)
			{
				return;
			}

			_InOrder(root->left_);
			cout << root->key_ << ":" << root->value_ << endl;
			_InOrder(root->right_);
		}
	public:
		BinarySearchTree()
			:root_(nullptr)
		{}
		BinarySearchTreeNode* Find(const K& key){
			BinarySearchTreeNode* cur = root_;
			while (cur){
				if (cur->key_ < key){
					cur = cur->right_;
				}
				else if (cur->key_ > key){
					cur = cur->left_;
				}
				else{
					return cur;
				}
			}
			return nullptr;
		}

		bool Insert(const K& key,const V& value) {
			if (root_ == nullptr) {
				root_ = new BinarySearchTreeNode(key,value);
				return true;
			}
			BinarySearchTreeNode* parent = nullptr;
			BinarySearchTreeNode* cur = root_;
			while (cur) {
				if (cur->key_ < key) {
					parent = cur;
					cur = parent->right_;
				}
				else if (cur->key_ > key) {
					parent = cur;
					cur = parent->left_;
				}
				else
					return false;
			}
			cur = new BinarySearchTreeNode(key,value);
			if (cur->key_ < parent->key_) {
				parent->left_ = cur;
			}
			else {
				parent->right_ = cur;
			}
			return true;
		}

		
		bool Erase(const K& key) {
			BinarySearchTreeNode* parent = nullptr;
			BinarySearchTreeNode* cur = root_;

			while (cur) {
				if (cur->key_ > key) {
					parent = cur;
					cur = cur->left_;
				}
				else if (cur->key_ < key) {
					parent = cur;
					cur = cur->right_;
				}
				else {
					// 情况一:不存在子节点
					// 情况二:存在一个子节点,该子节点为右节点
					if (cur->left_ == nullptr) {
						// 删除的为根节点
						if (parent == nullptr) {
							root_ = cur->right_;
						}
						else {
							if (parent->left_ == cur) {
								parent->left_ = cur->right_;
							}
							else {
								parent->right_ = cur->right_;
							}
						}
						delete cur;
					}
					// 情况二:存在一个子节点,该子节点为左节点
					else if (cur->right_ == nullptr) {
						// 删除的为根节点
						if (parent == nullptr) {
							root_ = cur->left_;
						}
						else {
							if (parent->left_ == cur) {
								parent->left_ = cur->left_;
							}
							else {
								parent->right_ = cur->left_;
							}
						}
						delete cur;
					}
					// 情况三:存在两个子节点
					else {
						// 从需要删除的右子树开始找最小子节点
						BinarySearchTreeNode* min = cur->right_;
						// 对应的父节点
						BinarySearchTreeNode* minParent = cur;
						// 找最小节点
						while (min->left_) {
							minParent = min;
							min = min->left_;
						}
						// 替换:覆盖法
						cur->key_ = min->key_;
						// 将父节点原本指向最小节点的指针指向最小节点的子节点,非空节点优先(只有右节点有可能)
						if (minParent->left_ == min) {
							minParent->left_ = min->right_;
						}
						else {
							minParent->right_ = min->right_;
						}
						delete min;
					}
					return true;
				}
			}
			return false;
		}

		void InOrder()
		{
			_InOrder(root_);
			cout << endl;
		}

	};
}

KV模型实例:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对,在此使用KV模型进行查找与使用,并打印对应的提示信息,代码示例如下:

void Test_Chinese_English() {
	BinarySearchTree<string, string> CE_KVTree;
	CE_KVTree.Insert("left", "左");
	CE_KVTree.Insert("right", "右");
	CE_KVTree.Insert("up", "上");
	CE_KVTree.Insert("down", "下");

	CE_KVTree.InOrder();
	string word;
	while (cin>>word )
	{
		BinarySearchTreeNode<string,string>* ret = CE_KVTree.Find(word);
		if (ret == nullptr) {
			cout << "Without this word\n";
		}
		else {
			cout << ret->value_ << endl;
		}
	}
}
down:下
left:左
right:右
up:上

down
下
left
左
right
右
up
上
dkfjld
Without this word

六、性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log2(N)
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N/2

image-20230130191133551


补充:

  1. 代码将会放到:C++/C/数据结构代码链接 ,欢迎查看!
    E_KVTree.Find(word);
    if (ret == nullptr) {
    cout << “Without this word\n”;
    }
    else {
    cout << ret->value_ << endl;
    }
    }
    }

```shell
down:下
left:左
right:右
up:上

down
下
left
左
right
右
up
上
dkfjld
Without this word

六、性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log2(N)
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N/2

[外链图片转存中…(img-eTDXJ0II-1675151605450)]


补充:

  1. 代码将会放到:C++/C/数据结构代码链接 ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fat one

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值