文章目录
二叉搜索树
二叉搜索树的基本概念和性质
二叉搜索树指的是一颗满足下面性质的二叉树
- 这棵树的左子树的所有结点都要小于根,这棵树的右子树的所有结点都要大于根
- 这棵树的左右子树也满足这个性质
例如
8的左子树的所有结点都小于8,8的右子树的所有结点都大于8。8的子树3和10也满足这个性质,二叉搜索树每一个结点中存的值称为键值,也成为key.
将一颗二叉搜索树进行中序遍历可以得到一个升序的序列,因此二叉搜索树又被称之为二叉排序树,标准的二叉搜索树是左子树的所有结点都小于根,右子树的所有结点都大于根。也可以让左子树的所有结点都大于根,右子树的所有结点都小于根,这样的搜索二叉树进行中序遍历可以得到一个降序的序列。
二叉搜索树的操作
查找
二叉搜索树的性质天然支持查找,所以又被称为二叉查找树。例如在下面这个树中查找7,时间复杂度是O(h),h是这个二叉搜索树的高度。
二叉搜索树的查找方法返回值是一个bool类型,只能判断能否找到,无法将找到的值以引用的方式返回,因为二叉搜索树不支持对节点的值进行修改,否则会破坏二叉搜索树的性质。
template<class K>
class BinarySearchTree
{
public:
bool find(const K& val)
{
Node* cur=root;
while(cur)
{
if(cur->val==val)
return true;
else if(cur->val>val)
cur=cur->left;//到左子树中找
else
cur=cur->right;//到右子树中找
}
return false;
}
//......
private:
Node* root=nullptr;
};
插入
二叉搜索树的插入有2种情况
- 树为空,此时需要新增节点赋值给root
- 数不为空,要寻找合适的位置进行插入
例如在这棵树中插入9
二叉搜索树的插入方法返回值也是一个bool类型,为了防止外界进行修改影响二叉搜索树的性质,不能将插入成功的那个节点进行返回。由于二叉搜索树不允许键值冗余(二叉搜索树中不能出现重复的元素),所以插入的值不能为原二叉搜索树中已有的值,否则返回false,且本次插入过程不进行任何操作。
template<class K>
class BinarySearchTree
{
public:
bool insert(const K& val)
{
if(root==nullptr)
{
root=new Node(val);
return true;
}
Node* par=root;
Node* cur=root;
while(cur)
{
if(cur->val==val)
return false;//不允许键值冗余
else if(cur->val>val)
{
par=cur;
cur=cur->right;
}
else
{
par=cur;
cur=cur->left;
}
}
//cur==nullptr
cur=new Node(val);
//cur的改变不影响par->left和par->right,因为cur和par是完全独立的,父子关系是通过简单赋值来完成的.
if(par->left==nullptr)
par->left=cur;
else
par->right=cur;
return true;
}
//......
private:
Node* root=nullptr;
};
中序遍历
二叉搜索树的中序遍历可以得到一个有序序列。由于二叉搜索树的root属于private,在类外无法访问,而中序遍历一般又采用递归完成需要依赖root,且在inorder函数的参数中又不能使用root作为默认参数,例如void inorder(Node* cur=root)
(在参数中不能使用this指针访问root),所以中序遍历一般采用2次封装
template<class K>
class BinarySearchTree
{
public:
vector<K> inorder()
{
vector<K> ret;
InorderByRecurison(root,ret);
return ret;
}
//......
private:
void InorderByRecurison(Node* cur,vector<K>& ret)
{
if(cur==nullptr)
return;
InorderByRecurison(cur->left,ret);//
ret.push_back(cur->val);
InorderByRecurison(cur->right,ret);
}
private:
Node* root=nullptr;
};
int main()
{
BinarySearchTree<char> tree;
vector<char> in=tree.inorder();
return 0;
}
删除
二叉树搜索树的删除操作有多种情况
-
要删除的是根,且根的左或者右是nullptr,直接root=root->left或root=root->right
-
要删除的结点不是根,但是该结点的左或者右为nullptr
例如要删除10,且10的左子树为nullptr。10作为8的右子树,10的非空子树一定要比8大,要删除10,只需要把10的非空子树接到8上,然后delete 10即可。若10作为其父的左子树,则将其父的left接到10的非空子树,若10作为其父的右子树,则把其父的right接到10的非空子树。
-
要删除的结点存在左右子树,这种情况下使用替换法删除。例如要删除8,8有左右子树
先把8和8的右子树的最小值进行交换(或者和左子树的最大值进行交换)。交换以后10的左子树满足全部小于10,且此时10的左子树还是搜索二叉树,10的右子树除了8以外其余全部都大于10(因为10是原来右子树中最小的那一个),现在只需要删除8并把8的右子树接到10上面即可。
二叉搜索树的删除方法返回值也是一个bool类型。
template<class K>
class BinarySearchTree
{
public:
bool erase(const K& val)
{
//0.找到要删除的那个节点
Node* del=root;
Node* par=del;
while(del)
{
if(del->val==val)
break;
else if(del->val>val)
{
par=del;
del=del->left;
}
else
{
par=del;
del=del->right;
}
}
if(del==nullptr)
return false;//找不到要删除的结点
//1.要删除的结点是根,且根的左或者右为nullptr
if(del==root&&(root->left==nullptr||root->right==nullptr))
{
Node* tmp=root;
if(root->left==nullptr)
root=root->right;
else
root=root->left;
delete tmp;
}
//2.要删除的结点不是根,但是该结点的左或者右为nullptr.
//要删除的结点不是根,那么del的par一定不是del
else if(del->left==nullptr||del->right==nullptr)
{
if(par->left==del)//父节点的左是要删除的结点
{
if(del->left==nullptr)
par->left=del->right;
else
par->left=del->left;
}
else//父节点的右是要删除的结点
{
if(del->left==nullptr)
par->right=del->right;
else
par->right=del->left;
}
}
//3.要删除的结点存在左右子树,采用替换法(这种情况del是根或者普通可以一并处理)
else
{
//a.将要删除的结点与其右子树的最小值交换(也可以是左子树的最大值)
Node* rightmin=del->right;
par=del;//此时par充当rightmin的父节点
while(rightmin->left)
{
par=rightmin;
rightmin=rightmin->left;
}
swap(rightmin->val,del->val);
//b.将rightmin结点删除,且将rightmin的右边接到rightmin父节点上
/*
此时是不知道par->left==rightmin还是par->right==rightmin的.
例如上面的二叉搜索树如果删除8,就是par->right==rightmin;如果删除3,就是par->left==rightmin
*/
if(par->left==rightmin)
par->left=rightmin->right;
else
par->right=rightmin->right;
delete rightmin;
}
return true;
}
//......
private:
Node* root=nullptr;
};
二叉搜索树的递归操作
递归查找
template<class K>
class BinarySearchTree
{
public:
bool find(const K& val)
{
return FindByRucursion(root,val,flag);
}
//......
private:
bool FindByRucursion(Node* cur,const K& val)
{
if(cur==nullptr)
return false;
if(cur->val==val)
return true;
if(val>cur->val)//充分利用搜索树的性质进行递归
return FindByRucursion(cur->right,val);
else
return FindByRucursion(cur->left,val);
}
private:
Node* root=nullptr;
};
递归插入
template<class K>
class BinarySearchTree
{
public:
bool insert(const K& val)
{
return InsertByRecursion(root,val);
}
//......
private:
bool InsertByRecursion(Node*& cur,val)//使用引用
{
if(cur==nullptr)
{
cur=new Node(val);
return true;
}
if(cur->val==val)
return false;
else if(cur->val>val)
return InsertByRecursion(cur->left,val);
else
return InsertByRecursion(cur->right,val);
}
private:
Node* root=nullptr;
};
二叉搜索树递归插入使用引用,可以得到想要的效果。
假设插入5,初始时cur指向8,8>5,cur->left放下去进行递归。由于采用引用,传递的是别名,即第n+1层递归的cur是第n层递归的cur->left或者cur->right的别名。所以当第x次递归后curnullptr,这个cur是第x-1次递归的cur的left或right的别名,x次递归的curnullptr,再把第x次递归的cur=new Node(val),等价于第x-1次递归的cur的left=new Node(val)或第x-1次递归的cur的right=new Node(val)。这个引用实现了链接的要求
如果使用传值,那么插入5,第一次调用cur是root的一份拷贝,cur也指向8,第二次的cur是上一次的cur的left的拷贝,指向3,第三次的cur是上一次的cur的right的拷贝,指向6,第四次的cur是上一次的cur的right的拷贝,指向4,第5次的cur是上一次cur的right的拷贝,指向nullptr,第5次的cur=new Node(val)不改变第四次的cur的right,第四次的cur的right还是nullptr.
递归删除
递归删除也采用引用的方法,引用的核心就是对于一些特殊情况可以不用记录要删除的结点的父节点。
template<class K>
class BinarySearchTree
{
public:
bool erase(const K& val)
{
return EraseByRecursion(root,val);
}
//......
private:
bool EraseByRecursion(Node*& cur,const K& val)
{
if(cur==nullptr)
return false;
if(cur->val>val)
return EraseByRecursion(cur->left,val);
else if(cur->val<val)
return EraseByRecursion(cur->right,val);
else
{
//此时cur指向要删除的结点,且cur是上一层cur的left/right的引用
Node* tmp=cur;
if(cur->left==nullptr)
{
cur=cur->right;//上一层cur的left/right也随之变化
//即使cur是根节点也能处理
delete tmp;
}
else if(cur->right==nullptr)
{
cur=cur->left;//上一层cur的left/right也随之变化
delete tmp;
}
else//cur的左右都不为nullptr
{
//a.将要删除的结点与其右子树的最小值交换(也可以是左子树的最大值)
Node* rightmin=cur->right;
while(rightmin->left)
rightmin=rightmin->left;
swap(rightmin->val,cur->val);
//此时cur的右子树依然是一颗二叉搜索树,且rightmin->left==nullptr
//但此时整颗二叉树已经不是二叉搜索树了
EraseByRecursion(cur->right,val);//在这个右子树上递归删除
}
return true;
}
}
private:
Node* root=nullptr;
};
此时4的右树是二叉搜索树,但是整棵树已经不是二叉搜索树,现在在4的右树上删除3即可,3这个结点的左一定为nullptr,因为3这个结点在交换之前对应rightmin.
二叉搜索树的特殊成员函数
拷贝构造函数
二叉搜索树的拷贝构造函数要完成深拷贝
template<class K>
class BinarySearchTree
{
public:
BinarySearchTree(const BinarySearchTree<K>& tree)
{
root=CopyRecursion(tree.root);
}
//......
private:
Node* CopyRecursion(Node* copy)
{
if(copy==nullptr)
return nullptr;
Node* cur=new Node(copy->val);
cur->left=CopyRecursion(copy->left);
cur->right=CopyRecursion(copy->right);
return cur;
}
private:
Node* root=nullptr;
};
赋值运算符重载
template<class K>
class BinarySearchTree
{
public:
//tree1=tree2
BinarySearchTree& operator=(const BinarySearchTree<K> tree)
{
//传值传参,tree是tree2的拷贝
std::swap(tree.root,root);
return *this;
}
//......
private:
Node* root=nullptr;
};
析构函数
template<class K>
class BinarySearchTree
{
public:
~BinarySearchTree()
{
clear(root);
}
//......
private:
void clear(Node*& root)
{
if(root==nullptr)
return;
clear(root->left);
clear(root->right);
delete root;
root=nullptr;//由于参数是引用,所以可以实现把private中的root置空
}
private:
Node* root=nullptr;
};
搜索二叉树的缺陷
搜索二叉树在进行插入的时候由于数据状况的不同可能导致整棵树不平衡,此时进行插入或删除时间复杂度可能是O(N),需要引入平衡数进行改进,常见的改进方案是红黑树和AVL树,这两种树通过旋转的方式使得搜索二叉树较为平衡。
搜索二叉树的应用
key模型
key模型主要用于判断某个关键字在不在搜索二叉树中,例如判断某一个英文单词在不在字典中。
int main()
{
BinarySearchTree<string> tree;
tree.insert("abandon");
//insert("......");
bool ret=tree.find("sort");
return 0;
}
key-Value模型
把关键字存放在一颗搜索二叉树,每一个关键字都对应一个value,在查找value的时候,根据关键字进行查找。key-value模型与key模型的区别在于key-value的模型中每一个结点存放2个数据:key和value,根据key的值找value,在进行插入的时候,比较大小是根据key进行比较的。
template<class K,class V>
struct TreeNode
{
TreeNode(K k=K(),V v=V()):key(k),val(v),left(nullptr),right(nullptr){}
K key;
V val;
TreeNode<K,V>* left;
TreeNode<K,V>* right;
};