二叉搜索树的概念:
- 若它的左⼦树不为空,则左⼦树上所有结点的值都⼩于等于根结点的值;
- 若它的右⼦树不为空,则右⼦树上所有结点的值都⼤于等于根结点的值;
- 它的左右⼦树也分别为⼆叉搜索树;
- ⼆叉搜索树中可以⽀持插⼊相等的值,也可以不⽀持插⼊相等的值,具体看使⽤场景定义,后续map/set/multimap/multiset系列容器底层就是⼆叉搜索树,其中map/set不⽀持插⼊相等值,multimap/multiset⽀持插⼊相等值;
二叉搜索树的性能分析:
- 最优情况下,⼆叉搜索树为完全⼆叉树(或者接近完全⼆叉树),其⾼度为: O()
- 最差情况下,⼆叉搜索树退化为单⽀树(或者类似单⽀),其⾼度为: O()
- 需要存储在⽀持下标随机访问的结构中,并且有序。
- 插⼊和删除数据效率很低,因为存储在下标随机访问的结构中,插⼊和删除数据⼀般需要挪动数据。
无序数组 | 二叉搜索树 | |
查找元素 | O() | O() |
插入元素 | O() | O() |
删除元素 | O() | O() |
二叉搜索树的实现:
以下的实现是实现不冗余的:
就是二叉搜索树的值是各不相同的,也就是对数据节点插入二叉搜索树只有在不重复数据的情况才可以执行插入操作
定义一个节点:
template<class K>
struct BSTNode
{BSTNode(const K& key):_key(key), _left(nullptr), _right(nullptr){}K _key;BSTNode<K>* _left;BSTNode<K>* _right;
};
插入操作:
在达到插入操作,我们可以通过递归来实现:
-
递归逻辑:
- 如果当前节点为空,说明找到了插入位置,创建一个新的
TreeNode
实例并返回。 - 如果要插入的键值小于当前节点的值,递归地在左子树中插入。
- 如果要插入的键值大于当前节点的值,递归地在右子树中插入。
- 如果键值等于当前节点的值(在不允许重复值的BST中),则不进行插入。
- 如果当前节点为空,说明找到了插入位置,创建一个新的
template<class K>
class BSTree
{
public:BSTree() : root(nullptr) {} // 构造函数初始化根节点为nullptr// 插入函数void insert(const K& key) {root = insert(root, key);}private:BSTNode<K>* root;// 递归插入函数BSTNode<K>* insert(BSTNode<K>* node, const K& key) {if (node == nullptr) {// 3.1 如果当前节点为空,创建并返回新节点return new BSTNode<K>(key);}if (key < node->_key) {// 3.2 如果键值小于当前节点值,递归插入左子树node->_left = insert(node->_left, key);}else if (key > node->_key) {// 3.3 如果键值大于当前节点值,递归插入右子树node->_right = insert(node->_right, key);}// 3.4 如果键值等于当前节点值,不插入(BST不允许重复值)return node;}
};
但是,我们也可以是不适用递归实现,因为根本就没有必要用递归实现,我们可以使用循环来实现。
这种方法直接从根节点开始,逐步向下搜索合适的插入位置,直到找到空位置为止。
-
循环逻辑:
- 创建一个新的节点
newNode
,其值为要插入的键值。 - 从根节点开始,使用一个循环来寻找插入位置。
- 在循环中,比较新节点的值与当前节点的值:
- 如果新节点的值小于当前节点的值,并且当前节点的左子节点为空,则在当前节点的左侧插入新节点。
- 如果新节点的值小于当前节点的值,并且当前节点的左子节点不为空,则将当前节点设置为当前节点的左子节点,并继续循环。
- 如果新节点的值大于当前节点的值,并且当前节点的右子节点为空,则在当前节点的右侧插入新节点。
- 如果新节点的值大于当前节点的值,并且当前节点的右子节点不为空,则将当前节点设置为当前节点的右子节点,并继续循环。
- 如果循环结束还没有找到合适的插入位置,说明新节点应该插入到树的最底层。
- 创建一个新的节点
-
处理根节点:如果树为空(即根节点为
nullptr
),则新节点成为根节点。
Insert代码实现:
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;}}Node* newnode = new Node(key);cur = newnode;if (parent->_key < key){parent->_right = cur;}else{parent->_left = cur;}return true;
}
查找操作:
查找逻辑:(递归,循环都可,选循环)
- 从根开始⽐较,查找x,x⽐根的值⼤则往右边⾛查找,x⽐根值⼩则往左边⾛查找;
- 最多查找⾼度次,⾛到到空,还没找到,这个值不存在;
- 如果不⽀持插⼊相等的值,找到x即可返回;
- 如果⽀持插⼊相等的值,意味着有多个x存在,⼀般要求查找中序的第⼀个x。如下图,查找3,要找到1的右孩⼦的那个3返回;
Find代码实现:
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;
}
删除操作:(难点)
- 要删除结点N左右孩⼦均为空
- 要删除的结点N左孩⼦为空,右孩⼦结点不为空
- 要删除的结点N右孩⼦为空,左孩⼦结点不为空
- 要删除的结点N左右孩⼦结点均不为空
- 把N结点的⽗亲对应孩⼦指针指向空,直接删除N结点(情况1可以当成2或者3处理,效果是⼀样的)
- 把N结点的⽗亲对应孩⼦指针指向N的右孩⼦,直接删除N结点
- 把N结点的⽗亲对应孩⼦指针指向N的左孩⼦,直接删除N结点
- ⽆法直接删除N结点,因为N的两个孩⼦⽆处安放,只能⽤替代法删除。找N左⼦树的值最⼤结点R(最右结点)或者N右⼦树的值最⼩结点R(最左结点)替代N,因为这两个结点中任意⼀个,放到N的位置,都满⾜⼆叉搜索树的规则。替代N的意思就是N和R的两个结点的值交换,转⽽变成删除R结点,R结点符合情况2或情况3,可以直接删除。
Erase代码实现:
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){//坑的解决(当删除的是根节点,根节点左为空时)if (cur == _root){_root = cur->_right;}else{//既是左为空,又是父亲的左孩子if (parent->_left == cur){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}delete cur;return true;}//右为空else if (!cur->_right){if (cur == _root){_root = cur->_left;}else{//既是左为空,又是父亲的左孩子if (parent->_right == cur){parent->_right = cur->_left;}else{parent->_left = cur->_left;}}delete cur;return true;}//左右不为空else{//replace节点是我们的替代节点//用右子树的最左节点,或者左子树的最右节点//这里我们使用前者Node* replaceParent = cur;//这里不要给nullptr,就怕replaceParent不进入循环,是空Node* replace = cur->_right;while (replace->_left){replaceParent = replace;replace = replace->_left;}cur->_key = replace->_key;if (replaceParent->_left == replace){replaceParent->_left = replace->_right;}else{replaceParent->_right = replace->_right;}delete replace;return true;}}}return false;}
⼆叉搜索树key和key/value使⽤场景:
key搜索场景:
key/value搜索场景:
#include<iostream>
#include<string>using namespace std;namespace key_value
{template<class K, class V>struct BSTNode{BSTNode(const K& key, const V& value):_key(key), _value(value), _left(nullptr), _right(nullptr){}K _key;V _value;BSTNode<K>* _left;BSTNode<K>* _right;};template<class K, class V>class BSTree{//typedef BSTNode<K, V> Node;//C++11,与typedef基本一样using Node = BSTNode<K, V>;public:BSTree() = default;BSTree(const BSTree<K, V>& t){_root = Copy(t._root);}BSTree<K, V>& operator=(BSTree<K, V> t){swap(_root, t._root);return *this;}~BSTree(){Destroy(_root);_root = nullptr;}bool Insert(const K& key, const V& value){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;}}Node* newnode = new Node(key, value);cur = newnode;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 nullptr;}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){//坑的解决(当删除的是根节点,根节点左为空时)if (cur == _root){_root = cur->_right;}else{//既是左为空,又是父亲的左孩子if (parent->_left == cur){parent->_left = cur->_right;}else{parent->_right = cur->_right;}}delete cur;return true;}//右为空else if (!cur->_right){if (cur == _root){_root = cur->_left;}else{//既是左为空,又是父亲的左孩子if (parent->_right == cur){parent->_right = cur->_left;}else{parent->_left = cur->_left;}}delete cur;return true;}//左右不为空else{//replace节点是我们的替代节点//用右子树的最左节点,或者左子树的最右节点//这里我们使用前者Node* replaceParent = cur;//这里不要给nullptr,就怕replaceParent不进入循环,是空Node* replace = cur->_right;while (replace->_left){replaceParent = replace;replace = replace->_left;}cur->_key = replace->_key;if (replaceParent->_left == replace){replaceParent->_left = replace->_right;}else{replaceParent->_right = replace->_right;}delete replace;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 << " ";_InOrder(root->_right);}void Destroy(Node* root){if (root == nullptr)return;Destroy(root->_left);Destroy(root->_right);delete root;}Node* Copy(Node* root){if (root == nullptr)return nullptr;Node* newRoot = new Node(root->_key, root->_value);newRoot->_left = Copy(root->_left);newRoot->_right = Copy(root->_right);return newRoot;}Node* _root = nullptr;};
}
测试代码:
int main()
{key_value::BSTree<string, string> dict;//BSTree<string, string> copy = dict;dict.Insert("left", "左边");dict.Insert("right", "右边");dict.Insert("insert", "插⼊");dict.Insert("string", "字符串");string str;while (cin >> str){auto ret = dict.Find(str);if (ret){cout << "->" << ret->_value << endl;}else{cout << "⽆此单词,请重新输⼊" << endl;}}return 0;
}
int main()
{string arr[] = { "苹果", "西⽠", "苹果", "西⽠", "苹果", "苹果", "西⽠","苹果", "⾹蕉", "苹果", "⾹蕉" };key_value::BSTree<string, int> countTree;for (const auto& str : arr){// 先查找⽔果在不在搜索树中// 1、不在,说明⽔果第⼀次出现,则插⼊<⽔果, 1>// 2、在,则查找到的结点中⽔果对应的次数++//BSTreeNode<string, int>* ret = countTree.Find(str);auto ret = countTree.Find(str);if (ret == NULL){countTree.Insert(str, 1);}else{ret->_value++;}}countTree.InOrder();return 0;
}
二叉搜索树的遍历:
思想:(点击进入)
-
前序遍历(Pre-order Traversal):
- 访问顺序:根节点 -> 左子树 -> 右子树。
- 特点:在二叉搜索树中,前序遍历得到的节点序列是递增的。
-
中序遍历(In-order Traversal):
- 访问顺序:左子树 -> 根节点 -> 右子树。
- 特点:在二叉搜索树中,中序遍历可以得到节点键值的有序序列,即从小到大的顺序。
-
后序遍历(Post-order Traversal):
- 访问顺序:左子树 -> 右子树 -> 根节点。
- 特点:在二叉搜索树中,后序遍历得到的节点序列是递减的。
-
层序遍历(Level-order Traversal):
- 访问顺序:从上到下,从左到右。
- 特点:在二叉搜索树中,层序遍历得到的节点序列不一定是有序的,因为层序遍历只考虑节点的深度,不考虑键值的大小。
在二叉搜索树中,中序遍历特别有用,因为它可以以最小的成本(时间复杂度为O(n))得到一个有序的节点序列。而前序和后序遍历则常用于需要保留树结构信息的场景,如复制树或者计算树的高度等。层序遍历则常用于需要按层次处理节点的场景,如广度优先搜索(BFS)算法。