赞
踩
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
由于搜索二叉树中,每个结点左子树上所有结点的值都小于该结点的值,右子树上所有结点的值都大于该结点的值,因此对搜索二叉树进行中序遍历后,得到的是升序序列,这也是使用搜索二叉树的意义。
以下面这个树为例,我们来介绍一下搜索二叉树常用的操作。
因为搜索二叉树存在左子树节点的值小于根节点的值,右子树节点的值大于根节点的值的性质,所以在二叉搜索树当中查找一个我们想要的数据的效率是非常快的,查找功能也可以算是二叉搜索树当中最重要的功能。
当我们需要在一个搜索二叉树中查找想要的数据时只需要按下面的过程:
插入的具体过程如下:
搜索二叉树最重要的就是左子树节点的值小于根节点的值,右子树节点的值大于根节点的值的性质,当我们删除树中的数据时不能破坏搜索二叉树的性质,否则这颗树也就失去了存在的意义,因此删除操作的过程较为复杂,需要考虑比较多的情况。
首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情
况:
看起来有待删除节点有4中情况,实际情况A可以与情况B或者C合并起来,因此真正的删除过程
如下:
下面我们将要模拟实现一个搜索二叉树,使其具有刚刚介绍的最基本的功能。
要实现二叉搜索树,我们首先需要实现一个结点类:
- 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;
- public:
- //构造函数
- BSTree();
-
- //拷贝构造函数
- BSTree(const BSTree<K>& t);
-
- //赋值运算符重载函数
- BSTree<K>& operator=(BSTree<K> t);
-
- //析构函数
- ~BSTree();
-
- //插入函数
- bool Insert(const K& key);
-
- //删除函数
- bool Erase(const K& key);
-
- //查找函数
- Node* Find(const K& key);
-
- //中序遍历
- void InOrder();
- private:
- Node* _root; //指向二叉搜索树的根结点
- };
构造函数非常简单,构造一个空树即可。
- //构造函数
- BSTree()
- :_root(nullptr)
- {}
拷贝构造函数实现也很简单,拷贝一棵和所给二叉搜索树相同的树即可。
- //拷贝树
- Node* _Copy(Node* root)
- {
- if (root == nullptr) //空树直接返回
- return nullptr;
-
- Node* copyNode = new Node(root->_key); //拷贝根结点
- copyNode->_left = _Copy(root->_left); //拷贝左子树
- copyNode->_right = _Copy(root->_right); //拷贝右子树
- return copyNode; //返回拷贝的树
- }
- //拷贝构造函数
- BSTree(const BSTree<K>& t)
- {
- _root = _Copy(t._root); //拷贝t对象的二叉搜索树
- }
传统方法
- //传统写法
- const BSTree<K>& operator=(const BSTree<K>& t)
- {
- if (this != &t) //防止自己给自己赋值
- {
- _root = _Copy(t._root); //拷贝t对象的二叉搜索树
- }
- return *this; //支持连续赋值
- }
现代写法
- //现代写法
- BSTree<K>& operator=(BSTree<K> t) //编译器接收右值的时候自动调用拷贝构造函数
- {
- swap(_root, t._root); //交换这两个对象的二叉搜索树
- return *this; //支持连续赋值
- }
析构函数完成对象中二叉搜索树结点的释放,注意释放时采用后序释放,当二叉搜索树中的结点被释放完后,将对象当中指向二叉搜索树的指针及时置空即可。
- //释放树中结点
- void _Destory(Node* root)
- {
- if (root == nullptr) //空树无需释放
- return;
-
- _Destory(root->_left); //释放左子树中的结点
- _Destory(root->_right); //释放右子树中的结点
- delete root; //释放根结点
- }
- //析构函数
- ~BSTree()
- {
- _Destory(_root); //释放二叉搜索树中的结点
- _root = nullptr; //及时置空
- }
在实现搜索二叉树的查找、插入、删除函数之前首先实现一个简单的函数提供打印这个二叉树的功能,方便测试下面其他的函数是否实现正常。
- void InOrder()
- {
- _InOrder(_root);
- cout << endl;
- }
-
- void _InOrder(Node* root)
- {
- if (root == nullptr)
- return;
-
- _InOrder(root->_left);
- cout << root->_key << " ";
- _InOrder(root->_right);
- }
- bool Insert(const K& key)
- {
- //空树的情况 直接申请值为key的结点作为二叉搜索树的根结点 插入成功,返回true
- if (_root == nullptr)
- {
- _root = new Node(key);
- return true;
- }
- Node* parent = nullptr;
- Node* cur = _root;
- while (cur != nullptr)
- {
- //key值等于当前结点的值 插入失败
- if (key == cur->_key)
- return false;
-
- //key值大于当前结点的值 往该结点的右子树走
- else if (key > cur->_key)
- {
- parent = cur;
- cur = cur->_right;
- }
- //key值小于当前结点的值 往该结点的左子树走
- else
- {
- parent = cur;
- cur = cur->_left;
- }
- }
-
- cur = new Node(key); //申请值为key的结点
- if (key < parent->_key) //key值小于当前parent结点的值
- {
- parent->_left = cur; //将结点连接到parent的左边
- }
- else //key值大于当前parent结点的值
- {
- parent->_right = cur; //将结点连接到parent的右边
- }
- return true; //插入成功,返回true
- }
递归实现二叉搜索树的插入操作也是很简单的,但是要特别注意的一点就是,递归插入函数的子函数接收参数root时,必须采用引用接收,因为只有这样我们才能将二叉树当中的各个结点连接起来。
- //插入函数(递归)
- bool InsertR(const K& key)
- {
- return _InsertR(_root, key);
- }
-
- //传引用时即可以直接修改节点的子节点指向,不需要再单独使用父节点控制
- bool _InsertR(Node*& root, const K& key)
- {
- if (root == nullptr)
- {
- root = new Node(key);
- return true;
- }
-
- if (root->_key < key)
- {
- return _InsertR(root->_right, key);
- }
- else if (root->_key > key)
- {
- return _InsertR(root->_left, key);
- }
- else
- {
- return false;
- }
- }
- //查找函数
- Node* Find(const K& key)
- {
- Node* cur = _root;
- while (cur)
- {
- if (key < cur->_key) //key值小于该结点的值
- {
- cur = cur->_left; //在该结点的左子树当中查找
- }
- else if (key > cur->_key) //key值大于该结点的值
- {
- cur = cur->_right; //在该结点的右子树当中查找
- }
- else //找到了值为key的结点
- {
- return cur; //查找成功,返回结点地址
- }
- }
- return nullptr; //树为空或查找失败,返回nullptr
- }
- //查找函数(递归)
- bool FindR(const K& key)
- {
- return _FindR(_root, key);
- }
-
- bool _FindR(Node* root, const K& key)
- {
- //空树查找失败
- if (root == nullptr)
- return false;
-
- //根节点值等于key值 查找成功
- if (root->_key == key)
- return true;
-
- //key值大于节点值 去右树找
- if (root->_key < key)
- return _FindR(root->_right, key);
- //key值小于节点值 去左树找
- else
- return _FindR(root->_left, key);
- }
- bool Erase(const K& key)
- {
- Node* parent = nullptr; //标记待删除结点的父结点
- Node* cur = _root; //标记待删除结点
- while (cur)
- {
- if (key < cur->_key) //key值小于当前结点的值
- {
- //往该结点的左子树走
- parent = cur;
- cur = cur->_left;
- }
- else if (key > cur->_key) //key值大于当前结点的值
- {
- //往该结点的右子树走
- parent = cur;
- cur = cur->_right;
- }
- else //找到了待删除的节点
- {
- //1.左为空
- if (cur->_left == nullptr)
- {
- if (cur == _root)//要删除的节点是根节点的情况
- {
- _root = cur->_right;
- }
- else //待删除结点不是根结点,此时parent不为nullptr
- {
- if (parent->_left == cur) //待删除结点是其父结点的左孩子
- {
- parent->_left = cur->_right;//父结点的左指针指向待删除结点的右子树即可
- }
- else //待删除结点是其父结点的右孩子
- {
- parent->_right = cur->_right; //父结点的右指针指向待删除结点的右子树即可
- }
- }
- delete cur; //释放待删除结点
- return true; //删除成功,返回true
- }
- //2.待删除结点的右子树为空
- else if (cur->_right == nullptr)
- {
- if (cur == _root)//要删除的节点是根节点的情况 此时parent为nullptr
- {
- _root = cur->_left; //父结点的左指针指向待删除结点的左子树即可
- }
- else //待删除结点不是根结点,此时parent不为nullptr
- {
- if (cur == parent->_left) //待删除结点是其父结点的左孩子
- {
- parent->_left = cur->_left; //父结点的左指针指向待删除结点的左子树即可
- }
- else //待删除结点是其父结点的右孩子
- {
- parent->_right = cur->_left; //父结点的右指针指向待删除结点的左子树即可
- }
- }
- delete cur; //释放待删除结点
- return true; //删除成功,返回true
- }
- //3.待删除结点的左右子树均不为空
- else
- {
- //替换法删除
- Node* minParent = cur; //标记待删除结点右子树当中值最小结点的父结点
- Node* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点
- //寻找待删除结点右子树当中值最小的结点
- while (minRight->_left)
- {
- //一直往左走
- minParent = minRight;
- minRight = minRight->_left;
- }
- cur->_key = minRight->_key; //将待删除结点的值改为minRight的值
- //注意一个隐含条件:此时minRight的_left为空
- if (minRight == minParent->_left) //minRight是其父结点的左孩子
- {
- minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
- }
- else //minRight是其父结点的右孩子
- {
- minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
- }
- delete minRight; //释放minRight
- return true; //删除成功,返回true
- }
- }
- }
- return false; //没有找到待删除结点,删除失败,返回false
- }
- //删除函数(递归)
- bool EraseR(const K& key)
- {
- return _EraseR(_root, key);
- }
-
- bool _EraseR(Node*& root, const K& key)
- {
- if (root == nullptr)
- return false;
-
- //key值大于节点值 去右树删除
- if (root->_key < key)
- {
- return _EraseR(root->_right, key);
- }
- //key值小于节点值 去左树删除
- if (root->_key > key)
- {
- return _EraseR(root->_left, key);
- }
- else //开始删除
- {
- Node* del = root;
-
- // 开始准备删除
- if (root->_right == nullptr)
- {
- root = root->_left;
- }
- else if (root->_left == nullptr)
- {
- root = root->_right;
- }
- else
- {
- Node* maxleft = root->_left;
- while (maxleft->_right)
- {
- maxleft = maxleft->_right;
- }
- //swap(root->_key, maxleft->_key);
- root->_key = maxleft->_key;
- return _EraseR(root->_left, maxleft->_key);
- }
-
- delete del;
- return true;
- }
- }
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到
的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
2. KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。
该种方式在现实生活中非常常见:
首先我们先将上面实现的搜索二叉树改为KV结构。
- namespace 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)
- {
- //空树的情况 直接申请值为key的结点作为二叉搜索树的根结点 插入成功,返回true
- if (_root == nullptr)
- {
- _root = new Node(key,value);
- return true;
- }
- Node* parent = nullptr;
- Node* cur = _root;
- while (cur != nullptr)
- {
- //key值等于当前结点的值 插入失败
- if (key == cur->_key)
- return false;
-
- //key值大于当前结点的值 往该结点的右子树走
- else if (key > cur->_key)
- {
- parent = cur;
- cur = cur->_right;
- }
- //key值小于当前结点的值 往该结点的左子树走
- else
- {
- parent = cur;
- cur = cur->_left;
- }
- }
-
- cur = new Node(key,value); //申请值为key的结点
- if (key < parent->_key) //key值小于当前parent结点的值
- {
- parent->_left = cur; //将结点连接到parent的左边
- }
- else //key值大于当前parent结点的值
- {
- parent->_right = cur; //将结点连接到parent的右边
- }
- return true; //插入成功,返回true
- }
-
- Node* Find(const K& key)
- {
- Node* cur = _root;
- while (cur)
- {
- if (key < cur->_key) //key值小于该结点的值
- {
- cur = cur->_left; //在该结点的左子树当中查找
- }
- else if (key > cur->_key) //key值大于该结点的值
- {
- cur = cur->_right; //在该结点的右子树当中查找
- }
- else //找到了值为key的结点
- {
- return cur; //查找成功,返回结点地址
- }
- }
- return nullptr; //树为空或查找失败,返回nullptr
- }
-
-
- //删除函数(非递归)
- bool Erase(const K& key)
- {
- Node* parent = nullptr; //标记待删除结点的父结点
- Node* cur = _root; //标记待删除结点
- while (cur)
- {
- if (key < cur->_key) //key值小于当前结点的值
- {
- //往该结点的左子树走
- parent = cur;
- cur = cur->_left;
- }
- else if (key > cur->_key) //key值大于当前结点的值
- {
- //往该结点的右子树走
- parent = cur;
- cur = cur->_right;
- }
- else //找到了待删除的节点
- {
- //1.左为空
- if (cur->_left == nullptr)
- {
- if (cur == _root)//要删除的节点是根节点的情况
- {
- _root = cur->_right;
- }
- else //待删除结点不是根结点,此时parent不为nullptr
- {
- if (parent->_left == cur) //待删除结点是其父结点的左孩子
- {
- parent->_left = cur->_right;//父结点的左指针指向待删除结点的右子树即可
- }
- else //待删除结点是其父结点的右孩子
- {
- parent->_right = cur->_right; //父结点的右指针指向待删除结点的右子树即可
- }
- }
- delete cur; //释放待删除结点
- return true; //删除成功,返回true
- }
- //2.待删除结点的右子树为空
- else if (cur->_right == nullptr)
- {
- if (cur == _root)//要删除的节点是根节点的情况 此时parent为nullptr
- {
- _root = cur->_left; //父结点的左指针指向待删除结点的左子树即可
- }
- else //待删除结点不是根结点,此时parent不为nullptr
- {
- if (cur == parent->_left) //待删除结点是其父结点的左孩子
- {
- parent->_left = cur->_left; //父结点的左指针指向待删除结点的左子树即可
- }
- else //待删除结点是其父结点的右孩子
- {
- parent->_right = cur->_left; //父结点的右指针指向待删除结点的左子树即可
- }
- }
- delete cur; //释放待删除结点
- return true; //删除成功,返回true
- }
- //3.待删除结点的左右子树均不为空
- else
- {
- //替换法删除
- Node* minParent = cur; //标记待删除结点右子树当中值最小结点的父结点
- Node* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点
- //寻找待删除结点右子树当中值最小的结点
- while (minRight->_left)
- {
- //一直往左走
- minParent = minRight;
- minRight = minRight->_left;
- }
- cur->_key = minRight->_key; //将待删除结点的值改为minRight的值
- //注意一个隐含条件:此时minRight的_left为空
- if (minRight == minParent->_left) //minRight是其父结点的左孩子
- {
- minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可
- }
- else //minRight是其父结点的右孩子
- {
- minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可
- }
- delete minRight; //释放minRight
- return true; //删除成功,返回true
- }
- }
- }
- return false; //没有找到待删除结点,删除失败,返回false
- }
-
- void InOrder()
- {
- _InOrder(_root);
- cout << endl;
- }
-
- BSTree()
- :_root(nullptr)
- {}
-
- BSTree(const BSTree<K,V>& t)
- {
- _root = _Copy(t._root);
- }
-
- ~BSTree()
- {
- Destroy(_root);
- }
- protected:
- void _InOrder(Node* root)
- {
- if (root == nullptr)
- return;
-
- _InOrder(root->_left);
- cout << root->_key << " ";
- _InOrder(root->_right);
- }
- Node* _Copy(Node* root)
- {
- if (root == nullptr)
- return nullptr;
-
- Node* newRoot = new Node(root->_key);
- newRoot->_left = _Copy(root->_left);
- newRoot->_right = _Copy(root->_right);
- return newRoot;
- }
-
- void Destroy(Node*& root)
- {
- if (root == nullptr)
- return;
-
- Destroy(root->_left);
- Destroy(root->_right);
-
- delete root;
- root = nullptr;
- }
- private:
- Node* _root = nullptr;
- };
-
- }
对修改好的KV结构搜索二叉树进行测试
- //测试函数1
- void BSTreeTest4()
- {
- using namespace key_value;
- // 输入单词,查找单词对应的中文翻译
- key_value::BSTree<string, string> dict;
- dict.Insert("string", "字符串");
- dict.Insert("tree", "树");
- dict.Insert("left", "左边、剩余");
- dict.Insert("right", "右边");
- dict.Insert("sort", "排序");
- // 插入词库中所有单词
- string str;
- while (cin >> str)
- {
- BSTreeNode<string, string>* ret = dict.Find(str);
- if (ret == nullptr)
- {
- cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
- }
- else
- {
- cout << str << "中文翻译:" << ret->_value << endl;
- }
- }
- }
测试结果正常:
测试函数2
- void BSTreeTest5()
- {
- using namespace key_value;
- // 统计水果出现的次数
- string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
- "苹果", "香蕉", "苹果", "香蕉" };
- 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();
- }
测试结果正常
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二
叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。