一.概念
二叉搜索树是一种特殊的二叉树,左子树上所有节点的值,一定小于等于根节点,右子树上所有节点的值,一定大于等于根节点。

二.关于复杂度的探讨
下面内容需要有关二叉树复杂度的知识储备,如有需要,请看这篇文章。

以一颗普通二叉搜索树来说,假设一个极端情况,去找树里有没有5,那么此时就会比对h次,我们通过等比数列求和知道,h = log(N),所以按理来说最坏复杂度应该是log(N)。
但这仅仅是一颗普通二叉树的最坏情况,如果二叉树本身结构就是一种非常极端的情况呢?

这时候还是去找5,依旧比对h次,但此时h与n直接近似,那么复杂度就退化成**O(N)**了,也就是说效率大大降低,二叉树直接退化到跟list,vector坐一桌了;更可气的是二叉树的实现比list和vector更为复杂,那么遇到这种情况,还不如用容器两兄弟来的实在。所以该如何解决这种效率低下的极端结构下的极端情况呢?
答案是平衡二叉搜索树和哈希,在后面的文章会讲到。
三.实现搜索二叉树
注:搜索章节里,模板参数不再叫作T,而是叫做K(key,关键字的意思),来代表关键字的类型
1.Insert
Ⅰ.相同值是否可插入的探讨

Ⅱ.不允许冗余(相同)插入的情况
遍历的过程既可用循环,也可用递归。但递归过深容易栈溢出,所以这里用循环。
要插入一个节点,首先比较它与当下父节点的大小,如果待插入节点大于父节点,往右子树走,如果小于,往左子树走。
template<class K>
class BSTree
{
typedef BSTNode<K> Node;
public:
bool Insert(const K& key)
{
//判空
if (_root == nullptr)
{
_root = new(Node);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{//让cur去遍历,每遍历一次之前就把cur的值赋值给parent,然后自己接着向下遍历。反复操作,直至cur为空。此时就可以在parent处插入子节点了。
if (key > cur->_key)
{//待插入值大,往右子树走
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{//待插入值小,往左子树走
parent = cur;
cur = cur->_left;
}
else//不允许存在值相同的节点
{
return false;
}
}
Node* newnode = new Node(key);
if (key > parent->_key)
{
parent->_right = newnode;
}
else if (key < parent->_key)
{
parent->_left = newnode;
}
return true;
}
private:
Node* _root = nullptr;
};
2.中序遍历
代码写好之后,_root和函数_Inorder()都是私有的,在使用上就很不方便,此时就有个小巧思,再在public区域封装个套壳函数:Inorder(),毕竟public区域里成员函数既可以去使用private区域的成员,也可以在类外被调用。

有个有趣的现象,二叉搜索树走中序遍历后的序列是顺序的。

void Inorder()
{//套壳函数,方便访问private的成员。
_Inorder(_root);
}
private:
void _Inorder(Node* root)//中序遍历
{
if (root == nullptr)
{
return;
}
_Inorder(root->_left);
cout << root->_key << " ";
_Inorder(root->_right);
}
Node* _root = nullptr;
3.查找


bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}
上面三位都是弟弟,接下来好戏才开场。
4.正式拷打------删除
由易到难分为三种情况:
假设待删除的节点为N。
①.N是叶子节点,直接释放,并让N父节点指向N的指针指向空。
②.N只有一颗子树(无论左右),改变N父节点对应子节点指针,使其指向N的子节点。
③.N左右两颗子树都存在,此时若想效仿②中操作是行不通的,二叉树一个父节点只能有两个子节点,此时若N父节点除N外,还有一颗子树,那么将N的两颗子树接给N父节点后,它就有了三颗子树,是不符合二叉树定义的。
解决方案:替换法。假设替换节点为R。R为N左子树的最右节点(左子树里最大的节点),或是N右子树的最左节点(右子树最小)中任意一个,这能保证替换,删除完以后整颗树还是二叉搜索树。
再来说说替换,将N与R节点的值交换,然后再释放掉R节点,并让R的父节点指向R节点的指针指向空。
图例:
删除的代码实现
遍历
先让cur在树里遍历(cur初始为_root,cur->_key与key比对,key小于_key,cur向左;key大于_key,向右),把每次向下遍历前的cur赋值给parent,然后cur向下遍历,当cur指向节点的值与key相等,就找到了要删除的目标。
删除
①.待删除节点cur只有一颗子树(将待删除的叶子节点也划归于此)
先判断cur的子树哪边是空。
再判断cur究竟是parent的左子节点还是右子节点。
然后将指向parent子节点的指针指向cur的子节点。
删除根的极端情况:

此时需要改变_root的指向,让其指向值为3的节点(根节点变成3)
②.cur有两颗子树
先确定替换节点replace,此处用cur右子树最左节点。
找到replace向下找到合适位置后,替换加删除。
else
{
//找替换节点,这里找cur右子树的最左节点。
Node* replace = cur->_right;
Node* replaceParent = nullptr;//须有替换节点的父节点,不然后面删除操作搞不了。
while (replace->_left)
{
replaceParent = replace;
replace = replace->_left;
}
//替换
cur->_key = replace->_key;
//删除
replaceParent->_left = replace->_right;
delete replace;
}
坑
问题分析
but...man,what can i say?删除根节点8还是走不通,还空指针引用了????

来结合图和代码逻辑分析一下:




由于8这个根节点不满足两个向下遍历的if条件,那么cur = 根节点,进入删除分支以后,由于根节点右子树最左节点**(replace->_left)为空** ,那么replace和replaceparent不进while循环,那么replaceparent无法更新,还是空指针。 那么执行删除操作时,replaceparent->_left就是引用空指针。
解决方案
将replaceparent的初始值设为cur,而非nullptr。

同时还有一个问题:

并非所有replace都是replaceparent的左子节点。

需要去判断一下replace的位置:

