目录
1.二叉搜索树概念
2. 实现=二叉搜索树
2.1. 二叉搜索树的插入
2.2查找
2.3删除节点
3.二叉树的应用(KV结构)
1.二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
2. 实现=二叉搜索树
这是一个二叉搜索树:
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
定义结构+初始化列表:
template<class K> //定义节点 struct BSTreeNode {BSTreeNode<K>* _left;BSTreeNode<K>* _right;K _key;//初始化BSTreeNode(const K& key):_left(nullptr), _right(nullptr), _key(key){} };
成员变量:
template<class K> class BSTree {typedef BSTreeNode<K> Node;private:Node* _root = nullptr; };
2.1. 二叉搜索树的插入
(默认定义,搜索二叉树数据不允许冗余)
代码:
bool Insert(const K& key) {if (_root == nullptr){_root = new Node(key);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(key);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true; }
代码解释:
- 检查根节点是否为空:如果根节点为空,则创建一个新节点作为根节点并返回
true
。- 遍历树以找到插入位置:使用
parent
和cur
指针来遍历树,直到找到一个空位置(即cur
为nullptr
)或找到与key
相等的节点。- 处理重复键:如果找到与
key
相等的节点,函数返回false
,表示插入失败(因为BST中通常不允许重复的键)。- 插入新节点:在遍历结束后,根据
parent
节点的键值来决定新节点是应该作为左子节点还是右子节点。测试用例:
此处中序遍历(有序),我们需要访问到_root,但root是私有的,要么提供一个get函数,或者把测试函数设置成友元(没必要),这里我们用的方法是将_InOrder函数套了一层,因为我们默认是从根开始访问:
void InOrder(){_InOrder(_root);cout << endl;}private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << " ";_InOrder(root->_right);}private:Node* _root = nullptr;void BST1() {int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTree<int> t1;for (auto x : a){t1.Insert(x);}t1.InOrder(); }
运行结果:
2.2查找
Node* 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 cur;}}return cur;}
while (cur)
: 开始一个循环,只要cur
不是nullptr
(即当前节点存在),就继续执行循环体。
if (cur->_key < key)
: 如果当前节点的键小于给定的键,
cur = cur->_right;
: 移动到当前节点的右子节点。
else if (cur->_key > key)
: 如果当前节点的键大于给定的键,
cur = cur->_left;
: 移动到当前节点的左子节点。
else
: 如果当前节点的键与给定的键相等,
return cur;
: 返回当前节点的指针。测试用例:
void BST1() {int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTree<int> t1;for (auto x : a){t1.Insert(x);}t1.InOrder(); }
运行结果:
2.3删除节点
bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{// 删除// 左为空,父亲指向我的右if (cur->_left == nullptr){//if(parent == nullptr)if (cur == _root){_root = cur->_right;}else{if (cur == parent->_left){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}delete cur;}else if (cur->_right == nullptr){//if(parent == nullptr)if (cur == _root){_root = cur->_left;}else{// 右为空,父亲指向我的左if (cur == parent->_left){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}delete cur;}else{// 左右都不为空,替换法删除// // 查找右子树的最左节点替代删除Node* rightMinParent = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinParent = rightMin;rightMin = rightMin->_left;}swap(cur->_key, rightMin->_key);if (rightMinParent->_left == rightMin)rightMinParent->_left = rightMin->_right;elserightMinParent->_right = rightMin->_right;delete rightMin;}return true;}}return false;}
- 初始化:
parent
:用于追踪当前节点cur
的父节点。cur
:从根节点_root
开始遍历。- 查找过程:
- 使用
while
循环遍历树,直到找到目标节点(键值为key
的节点)或遍历到叶子节点的子节点为空。- 更新
parent
和cur
以指向正确的节点。- 删除逻辑:
- 当找到目标节点时,有三种情况需要考虑:
- 目标节点没有左子节点(
cur->_left == nullptr
):
- 如果目标节点是根节点,则将根节点设置为右子节点。
- 否则,根据目标节点是父节点的左子节点还是右子节点,更新父节点的对应指针。
- 释放目标节点的内存。
- 目标节点没有右子节点(
cur->_right == nullptr
):
- 与情况1类似,但这次更新指向左子节点。
- 目标节点既有左子节点又有右子节点:
- 在目标节点的右子树中找到最小的节点(即最左边的节点)。
- 交换目标节点和最小节点的键值。
- 删除右子树中的最小节点(现在具有目标节点的键值)。
- 返回值:
- 如果成功找到并删除了目标节点,则返回
true
。- 如果遍历到叶子节点的子节点为空且未找到目标节点,则返回
false
。注意:
- 在处理根节点时,直接比较
cur == _root
,这是正确的。但在注释掉的代码部分(//if(parent == nullptr)
),检查parent
是否为nullptr
是不必要的,因为当cur
是根节点时,parent
始终为nullptr
。
测试用例:
void BST3() {int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };BSTree<int> t1;for (auto x : a){t1.Insert(x);}t1.InOrder();cout << endl;t1.Erase(8);t1.InOrder(); }
运行结果:
3.二叉树的应用(KV结构)
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方
式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对。
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
这是搜索二叉树的KV模型:
实际每个节点,就是多了一个与key关联的值,搜索二叉树的实现逻辑只是与key有关。
template<class K, class V>struct BSTreeNode{BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;K _key;V _value;// pair<K, V> _kv;BSTreeNode(const K& key, const V& value):_left(nullptr), _right(nullptr), _key(key), _value(value){}};template<class K, class V>class BSTree{typedef BSTreeNode<K, V> Node;public:// logNbool Insert(const K& key, const V& value){if (_root == nullptr){_root = new Node(key, value);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(key, value);if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;}Node* 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 cur;}}return cur;}bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}else{// 删除// 左为空,父亲指向我的右if (cur->_left == nullptr){//if(parent == nullptr)if (cur == _root){_root = cur->_right;}else{if (cur == parent->_left){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}delete cur;}else if (cur->_right == nullptr){//if(parent == nullptr)if (cur == _root){_root = cur->_left;}else{// 右为空,父亲指向我的左if (cur == parent->_left){parent->_left = cur->_left;}else{parent->_right = cur->_left;}}delete cur;}else{// 左右都不为空,替换法删除// // 查找右子树的最左节点替代删除Node* rightMinParent = cur;Node* rightMin = cur->_right;while (rightMin->_left){rightMinParent = rightMin;rightMin = rightMin->_left;}swap(cur->_key, rightMin->_key);if (rightMinParent->_left == rightMin)rightMinParent->_left = rightMin->_right;elserightMinParent->_right = rightMin->_right;delete rightMin;}return true;}}return false;}void InOrder(){_InOrder(_root);cout << endl;}private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_key << ":" << root->_value << endl;_InOrder(root->_right);}private:Node* _root = nullptr;};
测试案例:使用
BSTree<string, int>
来统计一个字符串数组中各个字符串的出现次数。遍历字符串数组
arr
,对于数组中的每一个字符串str
:
- 调用
countTree
的Find
方法,查找该字符串是否已经存在于树中。- 如果
Find
方法返回nullptr
(或者类似表示未找到的值),说明这个字符串还没有被统计过,因此调用Insert
方法将它插入到树中,并设置其值为1(表示这是第一次出现)。- 如果
Find
方法返回了一个非nullptr
的值(表示找到了对应的节点),说明这个字符串已经存在于树中,那么就将该节点对应的值(即出现次数)自增1。void TestBSTree(){// 统计次数string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉","苹果","草莓", "苹果","草莓" };BSTree<string, int> countTree;for (const auto& str : arr){auto ret = countTree.Find(str);if (ret == nullptr){countTree.Insert(str, 1);}else{ret->_value++;}}countTree.InOrder();} }
运行结果: