二分查找有一个很好的优点就是查找的效率十分的高,可以在logn的时间内找到我们想要的数据,比如说下面有一个有序的数组,如果我们想在数组中查找一个数据的话就可以使用二分查找,比如说下面的代码:
#include
#include
using namespace std;
int Binary_lookup(vector<int>& v, int k)
{
int left = 0;
int right = v.size() - 1;
while (left <= right)
{
int mid = (right + left) / 2;
if (v[mid] > k)
{
right = mid-1;
}
else if (v[mid] == k)
{
return mid;
}
else
{
left = mid+1;
}
}
return -1;
}
int main()
{
vector<int> v = { 1,2,13,14,45,56,57,68,79 };
int k;
while (cin >> k)
{
int result = Binary_lookup(v, k);
if (result >= 0)
{
cout << "找到了!下标为:" << result << "值为:" << v[result] << endl;
}
else
{
cout << "不好意思输入的值有错误" << endl;
}
}
return 0;
}
这段代码的测试结果如下:
但是这么高的效率得有个前提就是查找的数据库得是有序的,如果数据在是无序的话二分查找是会出现问题的,并且数据在插入和删除的时候还得进行一些操作来保证数据的有序性,并且这些操作还得在顺序表作为载体的前提下进行实现,那么这就使得二分查找的有着很大的不便性,所以二分查找看起来很厉害很简单很高效,但是这都是有很多的前提的,那么为了改善上述二分查找的缺点就有了我们下面要讲的搜索二叉树。
搜索二叉树有下面几个规则:
也就是说每个父节点的值要大于其左子树的所有节点的值,小于其右子树的所有节点的值比如说下面的图片
根节点的值为8,他的右子树的所有的值都要大于8,左子树的所有的值都要小于8,当然这个规则不仅仅适用于根节点,对于每个父节点的值都是这样的,比如说值为3的父节点,他的左子树的值为1小于3,他的右子树的值为6,4,7都大于3,如果满足上面的规则就是一颗二叉搜索树,如果不满足则不是的,比如说下面的图片
这就不是二叉搜索树,因为值为3的父节点的右节点虽然比3大,但是他在爷爷节点的左边他比爷爷节点的值还要大,这就不符合搜索二叉树的规则,所以上述图片就不是搜索二叉树,希望大家这里可以理解。
看到这里想必大家心中会有个疑问搜索二叉树为什么要制定这样的规则呢?为什么左子树节点的值一定要小于根节点的值,右子树节点的值一定要大于根节点的值呢?原因很简单因为这样做之后我们对这个树使用中序遍历的话得到的结果是有序的,比如说下面图片
中序遍历的规则是先访问左节点再访问根节点最后访问左节点,所以上述搜索二叉树的访问结果就是:
null 1 null 3 null 4 null 6 null 7 null 8 null 10 null 13 null 14 null
将其简化一下得到的结果就是 1 3 4 6 7 8 10 13 14,可以看到中序遍历的结果就是有序的当然这里的特性不止这一个,由于左子树的所有节点的值都小于根节点了,右子树节点的值都大于根节点,所以使用二叉树查找数据的时候效率也很高,比如说用上面的树查找数值为7的节点时就可以这么查找,首先比较根节点的值与7的大小,因为这里根节点的值为8要大于7所以下一步我们就往这个父节点的左子树进行查找,这里就来到了3因为3的值要小于7,所以我们就往3的右子树进行查找,这里就来到6因为6的值还是小于7所以继续往6的右子树进行查找,这里就来到节点7,因为这个节点的值和要查找的节点的值相等,所以我们就找到了要查找的值,那么这就是搜索二叉树的两个特性,一个是中序遍历得到的结果是有序的,一个是查找的时候可以根据根节点的值来判断查找的节点的大致方向,当然还有一个小特性就是二叉树的节点不能有相同的值,这一点我们以后再谈。
通过上面的过程大家应该可以发现通过这样的方式进行查找效率非常的高其原理和二分查找非常的相似,每次与根节点进行对比基本上都可以过滤掉当前树的一半节点,所以使用二叉搜索树的时间复杂度就是logN,但是这个时间复杂度是最佳情况,对于一些特殊情况或者极端情况来看的话时间复杂度就变成了N。比如说下面的图片:
上面的这个树是搜索二叉树吗?他满足上面的所有规则所以他就是一个搜索二叉树,但是他搜索元素的时间复杂度是logN吗?好像不是的对吧,如果插入的数据十分有序,导致搜索二叉树变成一条直线或者两边高度极度不平衡的话他的效率就会大大降低跟单链表搜索效率差不多,那么为了解决搜索二叉树不平衡的问题我们后面就引进平衡因子和红黑二叉树这两个内容,他们就是专门来解决上述不平衡的现象,那么接下来我们就来看看如何实现搜索二叉树。
首先创建一个结构体专门用来描述树的每个节点,首先这个结构体得含有两个指针,一个指向左子树一个指向右子树,其次还得有一个变量来存储数据,因为这里的数据会是各种类型,所以我们得添加一个模板上去,比如说下面的代码:
template<typename K>
struct BSTreeNode
{
BSTreeNode<K>* left;
BSTreeNode<K>* right;
K key;
};
因为这里存在指针,为了防止出现野指针的问题我们就在写一个默认构造函数,key就调用它自己类型的默认构造,left和right就将其初始化为空指针,比如说下面的代码:
template<typename K>
struct BSTreeNode
{
BSTreeNode(K val=K())
:left(nullptr)
, right(nullptr)
, key(val)
{}
BSTreeNode<K>* left;
BSTreeNode<K>* right;
K key;
};
节点的类实现之后我们就来大致的实现一下二叉搜索树的类,首先这个类得有一个指针用来指向搜索二叉树的根节点,由于后面存在大量对BSTreeNode对象执行的操作所以这里为了方便后面的操作就使用typedef对BSTreeNode
类进行重命名改为Node,并且这个类要处理大量的数据,所以也得添加上模板,比如说下面的代码:
template<typename K>
class BSTree
{
public:
typedef BSTreeNode<K> Node;
private:
Node* root;
};
同样的道理,这个类一开始在实例化出对象的时候是没有任何节点的,所以这个类的构造函数就只用将root指针初始化为空即可:
template<typename K>
class BSTree
{
public:
typedef BSTreeNode<K> Node;
BSTree()
:root(nullptr)
{}
private:
Node* root;
};
有了基础的模板这里就得想一下这个类需要哪些成员函数?首先这个类最主要的功能就是查找数据,所以我们得添加一个find函数,但是查找元素的前提是得有元素啊,所以我们还得实现一个insert函数用来插入数据,有插入就会有删除函数,所以这里还得实现一个erase函数,当数据插入完了该删除的删除了想找到的也找到了,那如果我们想查看一下整体有哪些数据的话是不是还得实现一个中序遍历的InOrder函数,有了上述的方向我们下面就来一个一个的实现这些函数,首先实现插入函数
首先insert函数的返回类型为bool,因为二叉搜索树中的数据是不允许重复,当我们插入数据的时候发现了相同数据的时候我们就得返回false表示数据已经存在当前插入数据失败,那么这个函数的参数就是K类型的引用,比如说下面的代码
bool insert(const K& val)
{
}
插入数据的时候首先得判断一下当前树的根节点是否为空,如果为空的我们就直接new一个节点出来让root指针指向这个节点,如果不为空的话我们就执行其他操作,比如说下面的代码:
bool insert(const K& val)
{
if (root == nullptr)
{
root = new Node(val);
return true;
}
else
{
}
}
如果root不为空就表明当前树是含有节点的,因为这里得进行多次比较,所以我们可以创建一个while循环在循环里面进行比较,所以我们先创建一个Node类型的指针用于比较,然后再创建Node类型的指针用于找到位置之后对创建出来的节点进行链接,那这里就将这两个指针称为cur和parent,cur用于比较parent用于链接,while循环的作用是找到合适的查入位置,当cur的内容变为nullptr时就表明找到了插入位置,所以这里循环继续的条件就是cur不为空,在循环里面我们就拿cur里面的值和val的值进行比较,如果val值较大的话就把cur的值复制给parent,然后让cur往右边跑,如果val的值较小的话就把cur的值复制给parent,然后让cur往左边跑,如果发现两个值相等的话就打印一句话然后直接return false来结束这个函数表明插入的数据出现了错误那么这里的代码就如下:
bool insert(const K& val)
{
if (root == nullptr)
{
root = new Node(val);
return true;
}
else
{
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->key > val)
{
parent = cur;
cur = cur->left;
}
else if (cur->key < val)
{
parent = cur;
cur = cur->right;
}
else
{
cout << "数据重复:" << val << endl;
return false;
}
}
}
}
while循环结束就表明我们当前已经找到了节点插入的位置,那么接下来我们就要创建一个节点并将其值初始化为val,然后把这个节点和parent指向的节点链接起来,那么这里我们得先判断一下链接的节点是在parent的左边还是右边,不然链接的时候没有方向,那这里就可以使用parent的key和val值进行比较,如果val大的话就链接在右边,如果val较小的话就链接在左边,比如说下面的代码:
bool insert(const K& val)
{
if (root == nullptr)
{
root = new Node(val);
return true;
}
else
{
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->key > val)
{
parent = cur;
cur = cur->left;
}
else if (cur->key < val)
{
parent = cur;
cur = cur->right;
}
else
{
cout << "数据重复:" << val << endl;
return false;
}
}
cur = new Node(val);
if (parent->key < val)
{
parent->right = cur;
}
else
{
parent->left = cur;
}
}
return true;
}
有了insert函数我们就可以往树里面插入数据,但是上面的树实现的对不对呢?这个我们是不知道的,但是我们可以通过打印函数来验证验证,我们知道搜索二叉树中序遍历的结果是有序的,所以我们这里就可以写一个中序遍历的函数来打印二叉树的内容,首先这个函数是没有返回值的,我们在函数运行的过程中就将函数的内容打印完成了,其次这个函数需要一个参数,因为递归需要知道节点,所以这个函数的参数就是一个Node的指针,二叉树的前中后序遍历想必大家都已经非常的熟悉了,那这里我们就直接上代码:
void _inorder(const Node* root)
{
if (root == nullptr)
{
return;
}
_inorder(root->left);
cout << root->key << endl;
_inorder(root->right);
}
但是这里存在一个问题,这个类的使用能够传递参数root吗?好像不能吧root的属性是private,所以如果这个函数有参数的话使用者是无法进行传参的,那这里就存在两个常见的解决方法一个是再创建一个函数,这个函数的功能就是放回root的值,另外一个就是直接将root的属性变成public,由于这两个方法的风险比较大,开放了root就意味着使用者可以直接根据root的值修改树的内容 ,所以我们采用另外一种方法,在类外无法使用root的值,但是在类里面我们是可以使用root的值的,所以我们可以创建一个函数这个函数对于使用者来说是可以执行一些功能的,但是对于我们来说这个函数就是一个空壳子,我们把需要传参的函数放到private里面然后通过这个空壳函数进行调用不就可以了吗,这样使用者即无法通过root来修改里面的内容,也不用担心传参的问题,那这里的代码就如下:
public:
void inorder()
{
_inorder(root);
}
private:
void _inorder(const Node* root)
{
if (root == nullptr)
{
return;
}
_inorder(root->left);
cout << root->key <<" ";
_inorder(root->right);
}
Node* root;
那这里我们就可以来测试一下代码的正确性,比如说下面的代码:
void func1()
{
BSTree<int> T;
T.insert(3);
T.insert(4);
T.insert(2);
T.insert(1);
T.insert(7);
T.insert(8);
T.inorder();
}
int main()
{
func1();
return 0;
}
代码的运行结果如下:
我们可以看到插入的顺序是无序的,但是我们打印出来的顺序却是有序的,那么这就说明我们上面的函数实现的是正确的,并且我们故意插入一些相同的数据,这里也会正常的报错并没有将那些数据插入进去:
void func1()
{
BSTree<int> T;
T.insert(3);
T.insert(4);
T.insert(4);
T.insert(2);
T.insert(1);
T.insert(1);
T.insert(7);
T.insert(8);
T.insert(8);
T.inorder();
}
有了上面的insert函数那么要想实现查找函数就十分的简单,首先find函数的返回类型是bool类型,如果要查找的元素存在的话就返回true,如果要查找的元素不存在的话就返回false,然后find函数的参数也是K类型的参数,那这里的代码就如下:
bool find(const K& val)
{
}
这里实现的思路差不多,首先创建一个Node类型的指针cur将其值初始化为root,因为这里要进行多次查找所以我们创建一个while循环,在循环体里面我们就用cur里面的key和val进行比较,如果key的值大于val的话就让cur往左边跑,如果key的值小于val的话就让cur往右边跑,key和val的值相等的话就返回true,如果cur的值为空的话我们就结束循环然后返回false来结束这里的函数,那么这里的代码就如下:
bool find(const K& val)
{
Node* cur = root;
while (cur)
{
if (cur->key == val)
{
return true;
}
else if (cur->key > val)
{
cur = cur->left;
}
else
{
cur=cur->right;
}
}
return false;
}
那这里我们就可以用下面的代码来进行一下测试:
void func1()
{
BSTree<int> T;
T.insert(3);
T.insert(4);
T.insert(4);
T.insert(2);
T.insert(1);
T.insert(1);
T.insert(7);
T.insert(8);
T.insert(8);
T.inorder();
cout << T.find(1) << endl;
cout << T.find(2) << endl;
cout << T.find(30) << endl;
}
上面代码的运行结果如下:
符合我们的预期那么这就说明我们上面写的代码应该没有什么问题。
在这篇文章一开始的时候我们说二分查找有个缺点就是删除数据或者插入数据的时候得保证执行完操作之后的数据是有序的,那么这里也是同样的道理二叉搜索树在删除元素之后也得保证自己本身还是一个二叉搜索树,举个最简单的例子就是当我们要删除根节点的时候如果不做任何处理那这一颗树是不是就变成两颗树了啊,那我们还怎么再执行后面的操作呢对吧,所以这里的删除函数不能是直接删除还得考虑上下之间的关系,得保证数据删除之后还得是搜索二叉树,那么这里的删除我们就分为四种种情况,但是不管是哪种情况我们首先要干的事情就是先通过循环找到要删除的节点,这里我们就创建两个Node类型的指针cur和parent,cur负责查找节点,parent负责链接节点,因为节点删除之后肯定得改变父节点的链接情况,那么这里的代码就如下:
bool erase(const T& key)
{
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->val > key)
{
parent = cur;
cur = cur->left;
}
else if (cur->val < key)
{
parent = cur;
cur = cur->right;
}
else//走到这里就说明找到了要删除的节点
{
}
}
}
那么接下来我们就要根据查找节点的情况案来分别用不同的方法来删除节点。
第一种情况:要删除的节点不存在
当cur为空的话就说明当前要删除的节点不存在,对于这种情况我们的程序就已经跳出了while循环我们直接在外面返回一个fasle就可以了,表明当前要删除的节点不存在。
bool erase(const T& key)
{
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->val > key)
{
parent = cur;
cur = cur->left;
}
else if (cur->val < key)
{
parent = cur;
cur = cur->right;
}
else//走到这里就说明找到了要删除的节点
{
}
}
return false;
}
第二种:删除的节点是叶节点
比如说下面的图片:
第二种情况就是删除的节点是叶子节点,比如说删除1,4,7,13节点这就属于第一种情况,对于这种情况我们直接将其删除就可以,因为这种情况节点没有子节点,所以他的消失不会对树的整体产生影响,但是这里大家别忘了将这个节点删除之后还得将他的父节点的指针改为空,那么这里得代码就如下:
if (cur->left == nullptr && cur->right == nullptr)
{
if (cur == parent->left)
{
parent->left == nullptr;
}
if (cur == parent->right)
{
parent->right == nullptr;
}
delete cur;
cur = nullptr;
}
但是我们这么写的话就会存在一个问题,如果被删除的节点为根节点的话是不是就出问题了啊对吧,所以我们这里就加个判断,如果被删除的节点为根节点的话我们就直接删除节点然后修改root的指向如果被删除的节点不为根节点的话我们就按照上面的代码进行修改,那么这种情况的完整的代码就如下:
if (cur->left == nullptr && cur->right == nullptr)
{
if (cur == root)
{
delete cur;
cur = nullptr;
root = nullptr;
}
else
{
if (cur == parent->left)
{
parent->left == nullptr;
}
if (cur == parent->right)
{
parent->right == nullptr;
}
delete cur;
cur = nullptr;
}
}
第三种:删除的节点有一个孩子
比如说下面的图片:
第三种情况就是被删除的节点只有一个孩子,比如说上面图片的14号节点和10号节点就属于第三种情况,对于这种情况我们采取的方法就是将这个节点的孩子节点托付给父亲节点,比如说10号节点它的左为空右不为空,那么删除10号节点就属于第三种情况,我们就要把它不为空的那一部分与父节点链接起来,然后释放掉当前节点,那这里的链接得分为两种情况第一种就是当前节点如果为父节点的左的话就将当前的节点的子节点链接道父节点的左,第二种情况就是当前的节点如果为父节点的右的话就将该节点的子节点链接到父节点的右,那么这里也得考虑一下被删除的节点为根节点的情况,对于这种情况我们的处理方法就是将root指针指向不为空的那一块,然后再释放掉原来的根节点,比如说下面的代码:
else if (cur->left == nullptr || cur->right = nullptr)
//如果走到了这里说明当前的节点一定有且只有一个为空
{
if (cur == root)//如果当前的节点为根节点
{
if (cur->right != nullptr)//根节点左边的节点为空
{
root = cur->right;
delete cur;
cur = nullptr;
}
else//根节点的右边的节点为空
{
root = cur->left;
delete cur;
cur = nullptr;
}
}
else//当前的节点不为空
{
if (cur == parent->right)//当前的节点为父节点的右子树
{
if (cur->left != nullptr)//当前节点的左边不为空
{
parent->right = cur->left;
delete cur;
cur = nullptr;
}
else//当前节点的右边不为空
{
parent->right = cur->right;
delete cur;
cur = nullptr;
}
}
if (cur == parent->left)//当前的节点为父节点的左子树
{
if (cur->left != nullptr)//当前节点的左边不为空
{
parent->left = cur->left;
delete cur;
cur = nullptr;
}
else//当前节点的右边不为空
{
parent->left = cur->right;
delete cur;
cur = nullptr;
}
}
}
}
看到这里想必大家能够知道第二种情况的删除逻辑,但是这里大家有没有发现一个规律就是第一种情况好像和第二种情况差不多,对吧!那我们这里能不能把这两种情况进行一下合并呢?首先如果我的右为空的话就把我的左子树给我的父亲,如果我的左为空的话就把我的右子树给父亲,在给的时候就判断一下我是父亲的左还是右,第一种情况的左右子树都是空所以给哪个都是一样的,所以这里就可以进行合并,当然合并之后依然得判断一下删除的节点是不是根节点,那么合并的代码就如下:
if (cur->right == nullptr)//左子树不为空就将左子树给上面
{
if (cur == root)//如果当前的节点为根节点
{
root = cur->left;
}
else//如果当前的节点不为根节点
{
if (cur == parent->right)//当前的节点为父节点的右边
{
parent->right = cur->left;
}
else//当前的节点为父节点的左边
{
parent->left = cur->left;
}
}
delete cur;
cur == nullptr;
}
else if (cur->left == nullptr)//右子树不为空就将右子树给上面
{
if (cur == root)//如果当前的节点为根节点
{
root = cur->right;
}
else//如果当前的节点不为根节点
{
if (cur == parent->right)//当前的节点为父节点的右边
{
parent->right = cur->right;
}
else//当前的节点为父节点的左边
{
parent->left = cur->right;
}
}
delete cur;
cur == nullptr;
}
else//如果节点的左不为空右也不为空的话就来到这里
{
}
第四种情况:删除的节点的两个子树都不为空
比如说下面的图片:
这里的节点8 3 6 10都属于第三种情况的节点在删除这个节点的时候就得找个节点进行替换,在之前的学习种我们遇到过相似的情况,在删除堆顶元素的时候我们也是不能直接删除的,得先用尾部的元素和堆顶的元素进行替换,再删除尾部的元素,最后将堆顶的元素进行向下调整最终实现的删除,所以对于这种关联性很强的容器我们采用的删除方法就是先替换被删除的节点,让其来到一个容易删除的位置删除之后对结构整体没有影响的位置然后对其进行删除,最后看一下是否需要调整一下替换节点使得结构不被破坏,那么这里也是同样的道理,当我们想要删除节点8的时候得先找找到一个节点来替换,那我们想一下哪个节点能够替换10的位置,并且10来到的位置可以很好的删除呢?首先10左边的节点都是比10小的节点,10的右边都是比10大的节点,通过这个规律我们就知道10右边的节点都要比10左边的节点要大,如果我们要想替换10的话这个节点首先得保证比10左边的节点都要大,然后还要比10右边节点的值都要小,那哪个节点能够满足这两个特征呢?答案是右子树的最小节点和左子树的最大节点,也就是右子树的最左节点或者是左子树的最右节点,所以对于第三种情况我们可以首先找到该节点的右树的最左节点(也就是右树的最小节点)或者找到左树的最右节点(也就是左树的最大节点)然后再删除被替换的节点,那这里我们就采用第一个找到右数的最小节点,通过上面的操作我们就将第三种情况转换成为了第一种情况,但是这里在删除被转换的节点的时候不能采用递归删除的方法因为数据替换之后那个树已经不是搜索二叉树了,所以在找被替换节点的时候还是得再创建一个父节点,交换完之后让父节点指向它的右然后再delete这个节点,比如说下面的代码:
else//当前的节点有两个子树
{
Node* minnode = cur->right;
Node* parent = cur;//这里不能初始化为空
while (minnode->left != nullptr)
{
parent = minnode;
minnode = minnode->left;
}
cur->key = minnode->key;
if (parent->right == minnode)//替换的节点是父节点的右边
{
parent->right = minnode->right;
}
else//替换的节点是父节点的左边
{
parent->left = minnode->right;
}
delete minnode;
minnode = nullptr;
}
这里初始化父节点的时候不能初始化为空因为很可能循环不会进去,如果循环没有进去的话parent的值为空,那么在执行后面的代码的话就必定会出现问题,比如说下面的图片
这里要删除8的话右边的最小节点是10,minright一开始指向的就是10所以循环进不去,如果parent一开始初始化为空的话就会出现出现问题,所以我们将parent初始化为cur,那么这里还有一个问题就是当这里的10为最小节点的时候它位于parent的右边并不是左边,所以这里得添加一个if语句进行一下判断minright是在parent的左边还是右边,那么这就是第四种情况的完整代码,这3种情况判断完之后我们就可以返回true来结束循环,那么erase函数的完整代码就如下:
bool erase(const T& key)
{
Node* cur = root;
Node* parent = nullptr;
while (cur != nullptr)
{
if (cur->val > key)
{
parent = cur;
cur = cur->left;
}
else if (cur->val < key)
{
parent = cur;
cur = cur->right;
}
else//走到这里就说明找到了要删除的节点
{
if (cur->right == nullptr)//左子树不为空就将左子树给上面
{
if (cur == root)//如果当前的节点为根节点
{
root = cur->left;
}
else//如果当前的节点不为根节点
{
if (cur == parent->right)//当前的节点为父节点的右边
{
parent->right = cur->left;
}
else//当前的节点为父节点的左边
{
parent->left = cur->left;
}
}
delete cur;
cur = nullptr;
}
else if (cur->left == nullptr)//右子树不为空就将右子树给上面
{
if (cur == root)//如果当前的节点为根节点
{
root = cur->right;
}
else//如果当前的节点不为根节点
{
if (cur == parent->right)//当前的节点为父节点的右边
{
parent->right = cur->right;
}
else//当前的节点为父节点的左边
{
parent->left = cur->right;
}
}
delete cur;
cur = nullptr;
}
else//当前的节点有两个子树
{
Node* minnode = cur->right;
Node* parent = cur;//这里不能初始化为空
while (minnode->left != nullptr)
{
parent = minnode;
minnode = minnode->left;
}
cur->key = minnode->key;
if (parent->right == minnode)//替换的节点是父节点的右边
{
parent->right = minnode->right;
}
else//替换的节点是父节点的左边
{
parent->left = minnode->right;
}
delete minnode;
minnode = nullptr;
}
return true;
}
}
return false;
}
将上面的代码实现之后我们就可以写一段代码来测试一下正确性,那这里的代码代码就不展示了,测试的原理就是随便插入一些数据,然后每删除一个数据就打印当前容器中的所有内容,那这里代码的运行结果就如下:
可以看到这里的运行结果没有什么问题那这就说明我们的代码实现的是正确的。
在之前的学习中我们对拷贝构造函数的处理方式是直接一个元素一个元素的插入,但是在这里不能直接插入,因为值是一样的但是插入之后形状却不一样了,比如说树的形状是这样:
你一个一个的插之后你还能保证树的形状是这样的吗?好像不能对吧,所以对于二叉搜索树的拷贝构造我们采用的方法是递归一个节点一个节点的拷贝,同样的道理既然是递归就要传根节点,用户是拿不到根节点的,所以我们就再创建一个copy函数放到私有里面,然后用拷贝构造函数来调用这个copy函数,那这里的代码就如下:
public:
BSTree(const BSTree<K>& t)
{
root = copy(t.root);
}
private:
Node* copy(Node* root)
{
}
那在copy函数里面我们采用的方法就是递归赋值节点,这里采用前序递归的方式来进行赋值,首先遇到空节点就直接返回空,当不为空节点的时候我们就创建一个节点出来,并给这个节点赋值为当前root的val,然后root的left就等于往左边递归的结果,root的right就等于往右边递归的结果,最后再放回创建出来的节点指针。那这里完整的代码就如下:
public:
BSTree(const BSTree<K>& t)
{
root = copy(t.root);
}
private:
Node* copy(Node* root)
{
if (root == nullptr)
{
return nullptr;
}
Node* new_node = new Node(root->key);
new_node->left = copy(root->left);
new_node->right = copy(root->right);
return new_node;
}
我们可以用下面的例子测试一下看看代码的运行是否是正确的:
void func3()
{
BSTree<int> T1;
T1.insert(2);
T1.insert(3);
T1.insert(1);
T1.insert(4);
BSTree<int> T2(T1);
T2.inorder();
}
这段代码的运行结果如下:
我们可以看到T2输出的结果确实是对的,但是这不能说明什么我们更加关心的是结构是否正确,那这里我们可以通过调试看看:
大家可以看到结构也是一样的,那这就说明我们实现的代码是没有问题的。
有了上面拷贝构造函数我们这里的赋值重载就可以很轻松的实现了,直接根据新参是实参的拷贝的规则再交换一下根节点便可以轻松的实现,那这里的代码就如下:
BSTree<K>& operator= (BSTree<K> t)
{
swap(root, t.root);
return* this;
}
测试的函数如下:
void func4()
{
BSTree<int>T1;
T1.insert(2);
T1.insert(3);
T1.insert(1);
T1.insert(4);
BSTree<int> T2;
T2 = T1;
T2.inorder();
}
运行的结果如下:
拷贝构造函数是通过递归创建节点,那这里的析构函数就是通过递归删除节点,因为析构函数是没有参数的,所以我们得调用其他函数来实现递归删除,这里采用的是后序递归的方式来进行删除,那么代码就如下:
~BSTree<K>()
{
Destory(root);
root = nullptr;
}
private:
void Destory(Node* root)
{
if (root == nullptr)
{
return;
}
Destory(root->left);
Destory(root->right);
delete root;
}
看到这里我们的搜索二叉树已经实现的差不多了,那下面我们就来看看如何利用利用另外一种思路来实现上面的find inset erase函数,首先看看find函数,既然是递归的话我们这里也得通过调用其他函数来实现递归因为用户拿拿不到roo节点,拿这里的代码就如下:
bool RFind()
{
return _RFind(root);
}
private:
bool _RFind(const Node* root)
{
}
那么在递归函数里面我们就可以先检查一下root的值是否为空如果为空的话就返回false,如果不为空的话就比较一下该节点的值是否和要查找的值一样,如果一样的话就返回true,如果不一样的话就继续往下查找,那么这里就先往左边查找,找完之后再往右边进行查找,只要有一个找到了就返回,那这里的代码就就下:
public:
bool RFind(const K& val)
{
return _RFind(root,val);
}
private:
bool _RFind(const Node* root,const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->key == val)
{
return true;
}
return _RFind(root->left, val)||_RFind(root->right,val);
}
那这里可以用下面的代码来进行一下测试:
void func5()
{
BSTree<int> T1;
T1.insert(2);
T1.insert(3);
T1.insert(1);
T1.insert(4);
cout << T1.RFind(2) << endl;
cout << T1.RFind(5) << endl;
}
与find函数的实现原理差不多,如果插入的数据比较大的话就往右边进行递归,如果插入的数据比较小的话就往左边进行插入,如果为空了就说明找到了插入的节点,这时我们就得创建节点并且进行链接,那这里的链接需要改变树的结构,那么这里就有三个方法来解决这个问题,第一种就是再传递一个参数用于记录父节点的地址,但是这种情况得判断一些父节点是否存在的问题,第二种就是在递归的时候判断一下左边节点或者右边节点是否为空,如果不为空的话就进行递归,如果为空的话就直接插入,这种方法就可以避免找不到父节点的问题,前两种方法还是非常的简单的我们这里就不实现了,我们来看看第三种方法?第三种方法就是把参数的类型改成引用,这时就可以直接进行赋值替换,因为这个时候已经变成了父节点的子节点的别名可以直接改变,这种方法对于根节点是空也可以很好的解决 ,那这里的代码就如下:
public:
bool RInsert(const K& val)
{
return _RInsert(root, val);
}
private:
bool _RInsert( Node*& root, const K& val)
{
if (root == nullptr)
{
root = new Node(val);
}
if (root->key == val)
{
return false;
}
else if (root->key > val)
{
_RInsert(root->left, val);
}
else
{
_RInsert(root->right, val);
}
}
我们可以用下面的代码来进行一下测试:
void func4()
{
BSTree<int>T1;
T1.RInsert(5);
T1.RInsert(4);
T1.RInsert(2);
T1.RInsert(3);
T1.RInsert(1);
T1.RInsert(7);
T1.RInsert(6);
T1.RInsert(8);
T1.inorder();
}
这段代码的运行结果如下:
这里的运行结果是正确的,那么说明我们上面的代码实现的是正确的。
同样的道理根据上面的方法先找到删除节点,
public:
bool RErase(const K& val)
{
return _RErase(root, val);
}
private:
bool _RErase(Node*& root, const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->key > val)
{
_RErase(root->left, val);
}
else if (root->key < val)
{
_RErase(root->right, val);
}
else//走到这里就说明已经找到了要删除的东西了
{
}
}
这里依然是分为三种情况,第一种和第二种组合在一起,如果root的左为空root等于root的右,对于右边也是同样的道理,因为这里的root本质上是父节点的子节点的别名,所以这里可以直接赋值,但是这里得创建一个变量来记录被删除节点的位置,避免赋值之后就找不到了要释放的节点,这种方法也解决了根节点的问题,当我们删除根节点的时候也不会出现问题,因为这里是root的别名 ,那这里的代码就如下:
bool _RErase(Node*& root, const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->key > val)
{
_RErase(root->left, val);
}
else if (root->key < val)
{
_RErase(root->right, val);
}
else//走到这里就说明已经找到了要删除的东西了
{
Node* tmp = cur;
if (root->right == nullptr)//当前节点的右边为空
{
//把左边给根节点
cur = cur->left;
}
else if(root->left==nullptr)//当前节点的左边为空
{
//把右边给根节点
cur = cur->right;
}
else//当前节点的两边都不为空
{
}
delete tmp;
tmp = nullptr;
}
}
当要删除有两个子节点的节点时,引用就没什么用了那么这里就有两个方法来解决这里的问题首先就是就跟前面的一样替换并删除,第二个就是先找到替换的节点,然后递归删除这个节点,但是这里的删除不是从根节点的开始删除,因为数据替换之后这个树就不是搜索二叉树了,所以我们的删除从子树删除,因为子树还是搜索二叉树,那么这里的代码就如下:
bool _RErase(Node*& root, const K& val)
{
if (root == nullptr)
{
return false;
}
if (root->key > val)
{
_RErase(root->left, val);
}
else if (root->key < val)
{
_RErase(root->right, val);
}
else//走到这里就说明已经找到了要删除的东西了
{
Node* tmp = cur;
if (root->right == nullptr)//当前节点的右边为空
{
//把左边给根节点
cur = cur->left;
}
else if(root->left==nullptr)//当前节点的左边为空
{
//把右边给根节点
cur = cur->right;
}
else//当前节点的两边都不为空
{
Node* minright = root->right;
while (minright->left != nullptr)
{
minright - minright->left;
}
swap(minright->key, root->key);
return _REease(root->right, val);
}
delete tmp;
tmp = nullptr;
}
}
那么这里我们的递归版本的删除函数就完成了,我们可以通过下面的代码进行一下测试:
随便插入一些值然后再一个一个的删除分别打印出结构我们就可以看到下面的运行结果:
那么就说明我们的函数实现的是正确的。
学了上面的内容想必大家应该能够理解搜索树的作用那这里我们就简单的聊聊二叉搜索树有什么用,平时大家在写文档的时候都会见过这样的场景:写了一个不存在的英语单词软件会自动地给我们添加一个红色地下划线来告诉我们这个单词不存在,那这个是如何实现的呢?答案就是通过二叉搜索树实现的,比如说一共有2w个英文单词,他们就把这2w个单词全部都放到二叉树里面,你每写一个单词就通过这个二叉树判断一下看看这个单词存不存在,如果查找的结果不存在的话就添加一个下划线,那么这就是一个应用场景,当然类似的场景还有很多种比如说小区的车辆进出系统,它就把已经交钱办了卡的车辆车牌全部放到一个搜索二叉树里面,每来一个车辆就对这个车牌进行查找看在不在这个搜索二叉树里面,当然这样的场景还有很多种这里大家了解一下即可,我们上面实现的都是k模型的搜索二叉树,那么这里还有key value搜索模型就是通过key查找找到之后修改value的模型,比如说英汉互翻,比如说通过自己的学号就可以查找到自己借了多少书,自己的各种学生信息这些都是经典的kv模型,那么这里的k就是查找,查找之后就可以根据find函数的返回值修改val,这里的修改不能修改k ,比如说每个k就是一个节点这些节点相互关联起来,然后每个节点又链接一个val节点,这个val节点里面就存储着一些相关的信息,比如说下面的图片:
那要想实现KV结构的搜索二叉树我们就得修改一下描述节点的结构体,添加一个描述val的变量,并且find函数在返回的时候也不要返回bool类型了,而是节点的引用这样就可以修改节点的内容,那么这里就不多说了大家可以自行下去实现。本篇文章就到这啦。