目录
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; else rightMinParent->_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: // logN bool 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; else rightMinParent->_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(); } }
运行结果: