1.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 左边小:若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 右边大:若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
1.2 二叉搜索树操作
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
0. 二叉搜索树节点的创建 与 基础框架的构建
节点类
template<class K> struct BSTreeNode { typedef BSTreeNode<K> Node; K _key; Node* _left; Node* _right; BSTreeNode(const K& val) :_key(val) ,_left(nullptr) , _right(nullptr) {} };
基础框架
template<class K> class BSTree { typedef BSTreeNode<K> Node; public: private: Node* _root = nullptr; };
1. 二叉搜索树的查找 find
a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b、最多查找高度次,走到到空,还没找到,这个值不存在。
// 1、查找 bool Find(const K& val) { if (_root == nullptr) { return false; } Node* cur = _root; while (cur) { if (cur->_key > val) { cur = cur->_left; } else if (cur->_key < val) { cur = cur->_right; } else return true; } return false; }
2. 二叉搜索树的插入 insert
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给 root 指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
Node* Insert(const K& val) { // (1)先判断节点是否存在 if (Find(val)) { cout << "结点已存在" << '\n'; return _root; } // (2)如果根为空,则直接插入一个节点 if (_root == nullptr) { _root = new Node(val); return _root; } // (3)按照二叉搜索树的性质,走到合适的位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > val) { parent = cur; cur = cur->_left; } else if (cur->_key < val) { parent = cur; cur = cur->_right; } } // (4)插入节点:和父节点链接上 cur = new Node(val); if (parent->_key > val) { parent->_left = cur; } else if (parent->_key < val) { parent->_right = cur; } return _root; }
3. 二叉搜索树的删除
代码逻辑:
(1)先判断该节点是否存在:如果不存在,则返回
(2)按照搜索树的规则,找到目标节点位置
(3)分情况执行删除操作:
要删除的结点可能分下面四种情况:
a. 要删除的结点无孩子结点:没有孩子
b. 要删除的结点只有左孩子结点:只有 左孩子
c. 要删除的结点只有右孩子结点:只有 右孩子
d. 要删除的结点有左、右孩子结点:有左右孩子
看起来有待删除节点有 4 种情况,实际情况 a 可以与情况 b 或者 c 合并起来
因此真正的删除过程 如下:
⭐有一个孩子 或 零个孩子:
- 情况1:删除该节点,使该节点的 父亲 指向 该节点的左孩子
- 情况2:删除该节点,使该节点的 父亲 指向 该节点的右孩子
⭐有两个孩子:
- 情况 3:替换法(删掉一个节点,可以找一个继承人继承你当前的位置)
你当前的位置有个特征:左边节点一定比你小,右边节点一定比你大
因此,你找的继承人一定也要满足这个条件:即可以是 左子树的最大节点 或 右子树的最小节点
- 左子树的最大节点:一定比左子树的根要大,一定比右子树的根要小
- 右子树的最小节点:一定比左子树的根要大,一定比右子树的根要小
替换规则:
直接将要删除的节点的键值key 替换成 继承人的键值key,然后删除继承人(因为继承人一定是叶子节点,可以直接删除,不用处理是否有孩子的问题)
替换法 使得删除节点变得简单,而便利
代码中有详细的 步骤解释了
// 3、删除 void Erase(const K& val) { // (1)先看节点是否存在 if (!Find(val)) { cout << "结点不存在" << '\n'; return; } // (2)按照搜索树的规则,找到目标节点位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > val) { parent = cur; cur = cur->_left; } else if (cur->_key < val) { parent = cur; cur = cur->_right; } else break; } // (3)分情况执行删除操作: // 0~1个孩子:删除该节点后,让父节点指向我的孩子 if (cur->_right == nullptr) { // 特殊情况:当父亲为空时,证明当前要删除的节点是根,直接删除,让根指针指向孩子 if (parent == nullptr) { _root = cur->_left; } else { if (parent->_key > cur->_key) { parent->_left = cur->_left; } else if (parent->_key < cur->_key) { parent->_right = cur->_left; } } delete cur; } else if (cur->_left == nullptr) { if (parent == nullptr) { _root = cur->_right; } else { if (parent->_key > cur->_key) { parent->_left = cur->_right; } else if (parent->_key < cur->_key) { parent->_right = cur->_right; } } delete cur; } // 2 个孩子 及以上:替换法 // 我们这里继承人是 :右子树的最左节点 else { // 删除 右子树的最左节点 Node* MinR_parent = cur; // 右子树的最左节点的 父节点:这里一定要赋值为 cur,不能赋值为 nullptr Node* MinRight = cur->_right; // 右子树的根 while (MinRight->_left) { MinR_parent = MinRight; MinRight = MinRight->_left; } // 继承 cur 的位置 cur->_key = MinRight->_key; // 删掉继承人,同时父节点接管 cur 的右孩子(为什么一定是右孩子:因为我们本代码要删除 右子树的最左节点,因此你就是最左的节点了,不可能还存在左节点) // 这里的问题是:Min 不知道是 MinParent的 左孩子 or 右孩子 if (MinR_parent->_right == MinRight) { MinR_parent->_right = MinRight->_right; // 可以是 空,也可以是一个节点 } else MinR_parent->_left = MinRight->_right; // 可以是 空,也可以是一个节点 delete MinRight; } }
4. 二叉树的中序遍历(便于观察结果)
void _InorderSearch(Node* root) { if (root == nullptr) { return; } _InorderSearch(root->_left); cout << root->_key << ' '; _InorderSearch(root->_right); }
1.3 总代码 BinarySearchTree.h
#pragma once #include<iostream> #include<assert.h> using namespace std; template<class K> struct BSTreeNode { typedef BSTreeNode<K> Node; K _key; Node* _left; Node* _right; BSTreeNode(const K& val) :_key(val) ,_left(nullptr) , _right(nullptr) {} }; template<class K> class BSTree { typedef BSTreeNode<K> Node; public: // 1、查找 bool Find(const K& val) { if (_root == nullptr) { return false; } Node* cur = _root; while (cur) { if (cur->_key > val) { cur = cur->_left; } else if (cur->_key < val) { cur = cur->_right; } else return true; } return false; } // 2、插入 Node* Insert(const K& val) { // (1)先判断节点是否存在 if (Find(val)) { cout << "结点已存在" << '\n'; return _root; } // (2)如果根为空,则直接插入一个节点 if (_root == nullptr) { _root = new Node(val); return _root; } // (3)按照二叉搜索树的性质,走到合适的位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > val) { parent = cur; cur = cur->_left; } else if (cur->_key < val) { parent = cur; cur = cur->_right; } } // (4)插入节点:和父节点链接上 cur = new Node(val); if (parent->_key > val) { parent->_left = cur; } else if (parent->_key < val) { parent->_right = cur; } return _root; } // 3、中序遍历:直接就是排序了 void InorderSearch() { _InorderSearch(_root); cout << '\n'; } // 4、删除 void Erase(const K& val) { // (1)先看节点是否存在 if (!Find(val)) { cout << "结点不存在" << '\n'; return; } // (2)按照搜索树的规则,找到目标节点位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key > val) { parent = cur; cur = cur->_left; } else if (cur->_key < val) { parent = cur; cur = cur->_right; } else break; } // (3)分情况执行删除操作: // 0~1个孩子:删除该节点后,让父节点指向我的孩子 if (cur->_right == nullptr) { // 特殊情况:当父亲为空时,证明当前要删除的节点是根,直接删除,让根指针指向孩子 if (parent == nullptr) { _root = cur->_left; } else { if (parent->_key > cur->_key) { parent->_left = cur->_left; } else if (parent->_key < cur->_key) { parent->_right = cur->_left; } } delete cur; } else if (cur->_left == nullptr) { if (parent == nullptr) { _root = cur->_right; } else { if (parent->_key > cur->_key) { parent->_left = cur->_right; } else if (parent->_key < cur->_key) { parent->_right = cur->_right; } } delete cur; } // 2 个孩子 及以上:替换法 // 我们这里继承人是 :右子树的最左节点 else { // 删除 右子树的最左节点 Node* MinR_parent = cur; // 右子树的最左节点的 父节点:这里一定要赋值为 cur,不能赋值为 nullptr Node* MinRight = cur->_right; // 右子树的根 while (MinRight->_left) { MinR_parent = MinRight; MinRight = MinRight->_left; } // 继承 cur 的位置 cur->_key = MinRight->_key; // 删掉继承人,同时父节点接管 cur 的右孩子(为什么一定是右孩子:因为我们本代码要删除 右子树的最左节点,因此你就是最左的节点了,不可能还存在左节点) // 这里的问题是:Min 不知道是 MinParent的 左孩子 or 右孩子 if (MinR_parent->_right == MinRight) { MinR_parent->_right = MinRight->_right; // 可以是 空,也可以是一个节点 } else MinR_parent->_left = MinRight->_right; // 可以是 空,也可以是一个节点 delete MinRight; } } private: // 将该函数直接写成 私有 void _InorderSearch(Node* root) { if (root == nullptr) { return; } _InorderSearch(root->_left); cout << root->_key << ' '; _InorderSearch(root->_right); } Node* _root = nullptr; };
1.4 二叉搜索树的应用(Key/Value 模型的 代码)
1.4.1 两种模型
(1)K模型:K模型即只有 key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到的值。
key :在不在的场景
如门禁系统:人脸识别一对一
如检查当前写的文字中有没有错误单词:通过录入正确的词典,在词典中搜索对应的单词,若找不到,则说明该单词拼写错误
(2)KV模型:每一个关键码 key,都有与之对应的值Value,即的键值对。该种方式在现实生活中非常常见:
key / value:通过一个值找另一个值
如 字典
如 车库收费系统:按时收费
车辆驶入时,机器扫描 车牌作为 key,value 存为 进入时间
当你出门时,再次扫描车牌,匹配 key,通过当前时间和 value 的时间差,计算收费价格
1.4.2 <key, value> 模型的 代码
将节点改成 pair<key, value> 类型,即键值对模型
节点类模板
template<class K, class V> struct BSTreeNode { typedef BSTreeNode<K, V> Node; pair<K, V> _kv; Node* _left; Node* _right; BSTreeNode(const pair<K, V>& kv) :_kv(kv) , _left(nullptr) , _right(nullptr) {} };
二叉搜索树类模板
template<class K, class V> class BSTree { typedef BSTreeNode<K, V> Node; public: // 1、查找 bool Find(const K& val) { if (_root == nullptr) { return false; } Node* cur = _root; while (cur) { if (cur->_kv.first > val) { cur = cur->_left; } else if (cur->_kv.first < val) { cur = cur->_right; } else return true; } return false; } // 2、插入 Node* Insert(const pair<K, V>& data) { // (1)先判断节点是否存在 if (Find(data.first)) { cout << "结点已存在" << '\n'; return _root; } // (2)如果根为空,则直接插入一个节点 if (_root == nullptr) { _root = new Node(data); return _root; } // (3)按照二叉搜索树的性质,走到合适的位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_kv.first > data.first) { parent = cur; cur = cur->_left; } else if (cur->_kv.first < data.first) { parent = cur; cur = cur->_right; } } // (4)插入节点:和父节点链接上 cur = new Node(data); if (parent->_kv.first > data.first) { parent->_left = cur; } else if (parent->_kv.first < data.first) { parent->_right = cur; } return _root; } // 3、中序遍历:直接就是排序了 void InorderSearch() { _InorderSearch(_root); cout << '\n'; } // 4、删除 void Erase(const K& val) { // (1)先看节点是否存在 if (!Find(val)) { cout << "结点不存在" << '\n'; return; } // (2)按照搜索树的规则,找到目标节点位置 Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_kv.first > data.first) { parent = cur; cur = cur->_left; } else if (cur->_kv.first < data.first) { parent = cur; cur = cur->_right; } else break; } // (3)分情况执行删除操作: // 0~1个孩子:删除该节点后,让父节点指向我的孩子 if (cur->_right == nullptr) { // 特殊情况:当父亲为空时,证明当前要删除的节点是根,直接删除,让根指针指向孩子 if (parent == nullptr) { _root = cur->_left; } else { if (parent->_key > cur->_key) { parent->_left = cur->_left; } else if (parent->_key < cur->_key) { parent->_right = cur->_left; } } delete cur; } else if (cur->_left == nullptr) { if (parent == nullptr) { _root = cur->_right; } else { if (parent->_kv.first > data.first) { parent->_left = cur->_right; } else if (parent->_kv.first < data.first) { parent->_right = cur->_right; } } delete cur; } // 2 个孩子 及以上:替换法 // 我们这里继承人是 :右子树的最左节点 else { // 删除 右子树的最左节点 Node* MinR_parent = cur; // 右子树的最左节点的 父节点:这里一定要赋值为 cur,不能赋值为 nullptr Node* MinRight = cur->_right; // 右子树的根 while (MinRight->_left) { MinR_parent = MinRight; MinRight = MinRight->_left; } // 继承 cur 的位置 cur->_kv.first = MinRight.first; // 删掉继承人,同时父节点接管 cur 的右孩子(为什么一定是右孩子:因为我们本代码要删除 右子树的最左节点,因此你就是最左的节点了,不可能还存在左节点) // 这里的问题是:Min 不知道是 MinParent的 左孩子 or 右孩子 if (MinR_parent->_right == MinRight) { MinR_parent->_right = MinRight->_right; // 可以是 空,也可以是一个节点 } else MinR_parent->_left = MinRight->_right; // 可以是 空,也可以是一个节点 delete MinRight; } } private: // 将该函数直接写成 私有 void _InorderSearch(Node* root) { if (root == nullptr) { return; } _InorderSearch(root->_left); cout << root->_kv.first << " : " << root->_kv.second << '\n'; _InorderSearch(root->_right); } Node* _root = nullptr; };
使用测试代码:
void TestKV() { vector<pair<string, string>>v = { {"string", "字符串"}, {"apple", "苹果"}, {"banana", "香蕉"} }; my::BSTree<string, string> tree; for (auto e : v) { tree.Insert(e); } tree.InorderSearch(); }
1.5 二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
一种是较为 平衡的结构,一种是退化成链表的结构
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:O(logn)
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:O(n)
问题:如果退化成单支树(像一条链表一样),二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?(即树的节点分布比较平衡)那么我们后续章节学习的 AVL树 和 红黑树 就可以上场了。
AVL树、红黑树 都是 平衡二叉树,使得 节点的分布平衡,使 搜索性能达到 O(logn)