文章目录
- 一、二叉搜索树概念
- 二、二叉搜索树操作
- 1. 二叉搜索树的查找
- 2. 二叉搜索树的插入
- 3. 二叉搜索树的删除
- 三、二叉搜索树的实现
- 四、二叉搜索树的性能分析
一、二叉搜索树概念
二叉搜索树又称二叉排序树/二次查找树,它是一棵空树或者是每颗子树都具有以下性质的二叉树
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};
二、二叉搜索树操作
1. 二叉搜索树的查找
- 从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
- 最多查找高度次,走到到空,还没找到,这个值不存在。
2. 二叉搜索树的插入
- 树为空,则直接新增节点,赋值给root指针。
- 树不空,按二叉搜索树性质查找插入位置,插入新节点。
3. 二叉搜索树的删除
- 左为空,父亲指向他的右。 也就是说左子节点为空,让它父节点指向该节点的右子节点,再直接删除该节点。
- 右为空,父亲指向他的左。 也就是说右子节点为空,让它父节点指向该节点的左子节点,再直接删除该节点。
- 左右子节点都不为空时,使用替换法删除
在第一节的例子中,删除1、4、7、13、14、10节点属于前两种情况,删除3、8、10、6节点属于第三种情况。
替换法删除
找到左子树的最右节点或者右子树的最左节点,替换该节点赋值给删除节点,直接删除替换节点,因为替换节点没子节点或者只有一个子节点,再归类到前两种情况。
修改
K模型的搜索二叉树不支持修改,增删查的时间复杂度为O(h),h是树的高度,最坏的情况是h为N。
三、二叉搜索树的实现
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。 该种方式在现实生活中非常常见,例如:刷卡进楼。
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> //key
class BSTree
{typedef BSTreeNode<K> Node;
public:bool Insert(const K& key){if (_root == nullptr){_root = new Node(key);return true;}Node* parent = nullptr; //这里父节点指针给nullptr不会报错,因为考虑极端情况只有一个节点,那也会进入下面的循环中,执行parent = cur;,所以父节点->不会报错Node* cur = _root;while (cur){if (cur->_key < key){parent = cur;cur = cur->_right;}else if (cur->_key > key){parent = cur;cur = cur->_left;}elsereturn false; //不能重复}cur = new Node(key); if (parent->_key < key)parent->_right = cur;elseparent->_left = cur;return true;}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;elsereturn true;}return false;}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 //找到了{//开始删除//1、左为空 ,父亲指向它的右 ,把另一个孩子托管给父亲//2、右为空 ,父亲指向它的左//3、左右都不为空if (cur->_left == nullptr) // 左为空 ,父亲指向它的右{if (cur == _root)//考虑极端情况删除的是_root根节点的情况{_root = cur->_right; //左为空,更新根}else{if (cur == parent->_left) //判断该节点是父亲的左还是右parent->_left = cur->_right;elseparent->_right = cur->_right;}delete cur;cur = nullptr;}else if (cur->_right == nullptr) //右为空 ,父亲指向它的左{if (cur == _root)//考虑极端情况删除的是_root根节点的情况{_root = cur->_left;}else{if (cur == parent->_left)parent->_left = cur->_left;elseparent->_right = cur->_left;}delete cur;cur = nullptr;}else//左右都不为空{// 替换法删除,替换的节点可以是左树的最大节点或者是右树的最小节点// 这里采用 找到右树的最小节点进行替换Node* minParent = cur; // 考虑极端情况,要删除的是根节点,所以minparent不可以为空Node* min = cur->_right;while (min->_left){minParent = min;min = min->_left;}swap(cur->_key, min->_key);if (minParent->_left == min) //判断minParent和min的关系{minParent->_left = min->_right;}elseminParent->_right = min->_right;delete min;min = nullptr;}return true;}}return false;}void InOrder()//中序遍历打印{_InOrder(_root);//套用了一层,第一次传过去了_root,后面递归就传他的子树cout << endl;}bool FindR(const K& key)//递归版本的查找{return _Find(_root, key);}bool InsertR(const K& key){return _InsertR(_root, key);}bool EraseR(const K& key){return _EraseR(_root, key);}BSTree() = default;//C++11 强制编译器生成默认的构造~BSTree(){_Destsory(_root);}BSTree(const BSTree<K>& t){_root = _Copy(t._root);}// 传值传参BSTree<K>& operator = (BSTree<K> t){swap(_root, t._root);return *this;}private:Node* _Copy(Node* root){if (root == nullptr)return nullptr;Node* copyRoot = new Node(root->_key);copyRoot->_left = _Copy(root->_left);copyRoot->_right = _Copy(root->_right);return copyRoot;}void _Destsory(Node*& root){if (root == nullptr)return;_Destsory(root->_left);_Destsory(root->_right);delete root;root = nullptr;}//_InOrder(_root) 递归调用不能写这个,不然每次传过去都是根节点,只能第一次传递根节点,用一个形参cur来接收根节点,void _InOrder(Node* cur) //递归必须 显示 的传递子树,而在外面调用的时候要传根节点_root,但是拿不到私有的_root。可以写一个getroot函数去拿到_root,或者像这样{if (cur == nullptr)return;_InOrder(cur->_left);cout << cur->_key << " ";_InOrder(cur->_right);}bool _Find(Node* cur, const K& key){if (cur == nullptr)return false;if (cur->_key < key)return _Find(cur->_right, key);else if (cur->_key > key)return _Find(cur->left, key);elsereturn true; //return true 按顺序依次从开辟的栈帧返回到最外层}bool _InsertR(Node*& cur, const K& key){if (cur == nullptr){cur = new Node(key); //cur是一个局部变量,所以要用引用return true;}if (cur->_key < key)return _InsertR(cur->_right, key);else if (cur->_key > key)return _InsertR(cur->_right, key);elsereturn false; //天然去重}bool _EraseR(Node*& cur, const K& key){if (cur == nullptr)return false;if (cur->_key < key)return _EraseR(cur->_right, key);else if (cur->_key > key)return _EraseR(cur->_left, key);else //删除{Node* del = cur;if (cur->_left == nullptr) //左为空{cur = cur->_right; }else if (cur->_right == nullptr) //右为空{cur = cur->_left;}else //左右都不为空{//找右树的最左节点Node* min = cur->_right;while (min->_left){min = min->_left;}swap(cur->_key, min->_key);return _EraseR(cur->_right, key); //从这个点的右树开始删除key}delete del;del = nullptr;return true;}}Node* _root = nullptr;
};
应用:
int main()
{BSTree<int> t;int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (auto& e : a){t.Insert(e);}//排序+去重t.InOrder(); t.Erase(3);t.InOrder();
}
- KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。 该种方式在现实生活中也非常常见,例如:统计某物品出现的次数
template<class K, class V>
struct BSTreeNode
{BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;K _key;V _value;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: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 nullptr;}bool Erase(const K& key){//...return true;}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;
};
应用:
int main()
{// 统计水果出现的次数string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
"苹果", "香蕉", "苹果", "香蕉" };BSTree<string, int> countTree;for (const auto& str : arr){//BSTreeNode<string, int>* ret = countTree.Find(str);auto ret = countTree.Find(str);if (ret == NULL){countTree.Insert(str, 1);}else{ret->_value++;}}countTree.InOrder();
}
四、二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树
- 最优情况下,二叉搜索树为完全二叉树,或者接近完全二叉树,其平均比较次数为:log2 N
- 最差情况下,二叉搜索树退化为单支树,或者类似单支,其平均比较次数为:N
如果退化成单支树,二叉搜索树的性能就失去了。将二叉搜素树改进为AVL树和红黑树,不论按照什么次序插入关键码,性能都能达到最优。