目录
二叉搜索树概念
二叉搜索树的应用
二叉搜索树K模型的实现
二叉搜索树的非递归写法
插入
查找
删除
二叉搜索树的递归写法
查找Find
插入Insert
删除Erase
中序遍历打印二叉搜索树
二叉搜索树的key_value模型
实现
创建节点
插入Insert
查找
删除
中序遍历打印
测试
二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
二叉搜索树的应用
1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV模型:每一个关键码key,都有与之对应的值Value,即
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
- <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key
- 查询英文单词时,只需给出英文单词,就可快速找到与其对应的key
二叉搜索树K模型的实现
先定义一个节点类,有左节点、右节点和自己的值。
templatestruct BSTreeNode { BSTreeNode * _left; BSTreeNode * _right; K _key; BSTreeNode(const K& key) :_left(nullptr) , _right(nullptr) , _key(key) {} };
将这棵树定义一个节点,设置这个节点为空。那么这个空树就造好了,我们可以开始对这颗二叉树进行操作。
templatestruct BSTree { typedef BSTreeNode Node; public: BSTree()//构造初始化 :_root(nullptr) {} private: Node* _root; }
二叉搜索树的非递归写法
插入
找到要插入的值key的位置,new一个新节点,并与新节点的父亲链接起来。这里如果遇到树中已经存在的值,就不插入。所以二叉搜索树的插入不仅可以插入数据,还可以去重。
所以定义两个节点,一个cur当前节点用来建立新节点,一个parent父节点用来链接新节点。
跟节点是空,就向根节点插入第一个值。这里要找到插入节点所在位置,如果比当前比较节点大,当前节点就向右遍历;如果比当前节点小,向当前节点的左节点遍历。父亲节点parent也要更新。如果当前节点和要插入的节点值相等,就返回false。如果cur节点走向空,就可以创建新节点了,此时我们也记录了父节点的位置。此时cur和parent分别指向一个节点,还没有链接关系,所以要把parent节点和cur节点链接起来。
bool Insert(const K& key)
{
if (_root == nullptr)
{
_root = new Node(key);
}
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
//cur为空
cur = new Node(key);
if (key > parent->_key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
查找
定义一个节点,因为只需要查找,没有要改变的节点,没有链接关系,所以不需要定义父亲节点。
如果要找的key值比当前cur的值大,那么更新cur几点,cur向右走;如果key比当前cur的值要小,cur向左更新。如果找到这个值就返回true;当cur走到空都没找到,返回false。
//查找
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;
}
删除
思路:先找到要删除的那个值,和它的父亲
1.没有孩子/只有一个孩子的节点:直接删除--如果左孩子为空,那么父亲指向右孩子;右孩子为空,父亲指向左孩子。左右都为空,也算作这个情况,左为空,父节点就指向右边的空节点。
当删除2、8这样的节点,如果要删除的cur是parent的左节点,那么parent的左指向cur的子树;如果cur是parent的右节点,那么parent右节点指向cur的子树。
2.有两个孩子:左子树的最大节点 / 右子树的最小节点进行替换--如删除5、7这样的节点
替换之后,这个右子树的最小节点可能存在右子树(因为已经是右子树的最小节点,所以不存在左节点了),所以还要和父节点进行链接。如果最小节点mincur是minparent的左子树,那么minparent的左链接mincur的右子树。如果mincur是minparent的右节点,那么minparent的右链接mincur的右子树(比如删除7,它的右子树最小节点是8,7和8交换后,minparent的右要链接mincur的右)
右子树最小值替换:
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
//找节点
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else//找到了
{
//2、8节点情况
if (cur->_left == nullptr)
{
if (cur == _root)
_root = cur->_right;
else
{
if (cur == parent->_left)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
}
delete cur;
}
else if (cur->_right == nullptr)
{
if (cur == _root)
_root = cur->_left;
else
{
if (cur == parent->_left)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else//两边都不为空
{
//右边最小节点进行替换
//有可能最小节点还有右子树
Node* minparent = cur;
Node* mincur = cur->_right;
if (mincur->_left)
{
minparent = mincur;
mincur = mincur->_left;
}
swap(cur->_key, mincur->_key);
if (minparent->_left == mincur)
minparent->_left = mincur->_right;
else//父亲的右节点是最小节点
minparent->_right = mincur->_right;
delete mincur;
}
return true;
}
}
return false;
}
左子树最小值替换:
else//左右都不为空--删除5、7--找到左子树的最大值
{
Node* max = cur->_left;
Node* max_parent = cur;
while (max->_right)//找到左子树最大值
{
max_parent = max;
max = max->_right;
}
swap(cur->_key, max->_key);
if (max_parent->_left == max)
max_parent->_left = max->_left;
else
max_parent->_right = max->_left;
delete max;
}
二叉搜索树的递归写法
首先说明,因为递归的实现需要传入根节点,而跟节点是私有的成员变量。无法在测试函数中传根节点这个参数,所以我们可以将实现函数写成一个子函数,再在类中写一个主函数,然后把这个子函数传给主函数,就可以在主函数用到根节点参数,在类外面的测试函数中,就可以直接用这个主函数,且不用传入参数。
查找Find
在树种查找值key,如果根节点是空直接返回,如果key的值比根节点值大,就向右子树查找;如果key的值比根节点值小,就向左子树查找;如果key的值跟根节点的值相同,就返回当前节点。
//查找
Node* FindR(const K& key)
{
return _FindR(_root, key);
}
Node* _FindR(Node* root, const K& key)
{
if (root == nullptr)
return nullptr;
//大就在右子树找,小就在左子树找,相等就返回节点,不相等找不到返回nullptr
if (key > root->_key)
return _FindR(root->_right, key);
else if (key < root->_key)
return _FindR(root->_left, key);
else//找到了就返回
return root;
}
插入Insert
如果比当前节点值大,就向右子树插入;如果比当前节点小,就向左子树插入;如果遇到和当前节点相等的值,就不要插入了。因为该函数返回类型是bool,所以当遇到值相等的节点就返回false,如果插入成功过了,返回true。
//插入
//主函数
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 (key > root->_key)
{
_InsertR(root->_right, key);
}
else if (key < root->_key)
{
_InsertR(root->_left, key);
}
else
return false;
}
删除Erase
思路:先查找,找到要删除的数字key,就删除,找不到返回false。如果要删除的数字比根节点数字要大,就向右子树查找,在右子树中删除;如果要删除的数字比根节点数字小,向左子树查找,在左子树中删除值。
当找到要删除的节点了,那么我们关心的是链接关系,这里有两种情况:
第一种:删除2、8这样的节点,只有一个孩子或没有孩子的节点。
如果左节点为空的话,那么直接让该节点的父节点和该节点的右孩子链接,但是有的人说这里没有父节点呀,哪来的父节点?这里有一个非常巧妙的地方,在传入root跟节点的时候用的是引用。这就意味着,当找到要删除的节点后,最后一次递归_Erase(root->right,key);或_Erase(root->left,key);后进入了下一个栈帧,那么root是root->right的引用。也就是说root是父节点的链接关系。所以要链接右孩子的话,不用像非递归一样还要区分删除的节点是父节点的左孩子还是右孩子。这里直接用root链接删除结点的右孩子即可,root=root->right;这样父节点就链接上右孩子了。最后要记录要删除的节点,在链接之后删除它就可以。
当要删除的节点的右孩子是空的话,同上,最后一次传入递归_Erase(root->right,key);或_Erase(root->right,key);进入最后一层栈帧的时候,root是root->left或root->right的引用,因为右孩子是空,所以直接链接左孩子即可。
第二种,删除5、7这样的有两个孩子的节点。
和非递归的思路一样,有两个孩子,那么从右子树找到最小的节点和key节点交换,或者从左子树找最大节点和key节点交换,那么定义一个节点找到这个最小节点,然后交换。这里以右子树最小节点为例:在交换之后,要删除右子树的最小节点,(因为交换之后的节点还是右子树的最小值),因为在交换之后搜索树的顺序已经被打乱,所以不能用递归从根节点删除。所以要从右子树的节点开始删除,也就重复了上面没有节点,或只有右孩子的情况。(因为如果没有左孩子,它就是最小节点,可能存在比他大的右子树,这就是上面正常的链接情况),最后递归调用会自动删除这个5节点。
//主函数
//删除
bool EraseR(const K& key)
{
return _EraseR(_root, key);
}
//子函数
bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)//没找到这个节点,返回nullptr
return false;
if (key > root->_key)
return _EraseR(root->_right, key);
else if (key < root->_key)
return _EraseR(root->_left, key);
else//找到了
{
Node* del = root;
if (root->_left == nullptr)//左空,右空,两边不空
{
root = root->_right;
}
else if (root->_right == nullptr)
{
root = root->_left;
}
else
{
Node* min = root->_right;
while (min->_left)
{
min = min->_left;
}
swap(root->_key, min->_key);
return _EraseR(root->_right,key);
}
delete del;
return true;
}
}
};
中序遍历打印二叉搜索树
二叉搜索树的根节点,左中右。
void InOrder()
{
_inOrder(_root);
cout << endl;
}
//中序遍历
void _inOrder(Node* root)
{
if (root == nullptr)
return;
_inOrder(root->_left);
cout << root->_key << " ";
_inOrder(root->_right);
}
二叉搜索树的key_value模型
key_value模型和Key模型非常类型,可以说实现代码几乎一样,但是kv模型增加了一个类模板value,就是在搜索对应节点的时候,会找到对应的value值。举个例子,当在我们坐高铁刷票的时候,刷票要找到你的姓名和身份证号,因为如果找到你的名字还不行,如果有重名,这样会搞乱,所以增加一个身份证号可以立即确认(这只是例子,实际可能不是这样的,我们只是举例子)。
那么当我们要搜索一个人的时候,找到对应的名字,树的节点不仅存储了人的姓名,还包含了value值--身份证,在搜索姓名的同时,身份证号也获取到了。
实现
创建节点
template
struct BSTreeNode
{
BSTreeNode* _left;
BSTreeNode* _right;
K _key;
V _value;
BSTreeNode(const K& key, const V& value)
:_left(nullptr)
, _right(nullptr)
, _key(key)
, _value(value)
{}
};
插入Insert
template
struct BSTree
{
typedef BSTreeNode Node;
public:
BSTree()
:_root(nullptr)
{}
bool Insert(const K& key, const V& value)
{
if (_root == nullptr)
{
_root = new Node(key, value);
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
cur = new Node(key, value);
if (key>parent->_key)
parent->_right = cur;
else
parent->_left = cur;
return true;
}
private:
Node* _root;
};
查找
template插入Insertstruct BSTreeNode { BSTreeNode * _left; BSTreeNode * _right; K _key; V _value; BSTreeNode(const K& key, const V& value) :_left(nullptr) , _right(nullptr) , _key(key) , _value(value) {} };
template查找struct BSTree { typedef BSTreeNode Node; public: BSTree() :_root(nullptr) {} bool Insert(const K& key, const V& value) { if (_root == nullptr) { _root = new Node(key, value); return true; } Node* cur = _root; Node* parent = nullptr; while (cur) { if (key > cur->_key) { parent = cur; cur = cur->_right; } else if (key < cur->_key) { parent = cur; cur = cur->_left; } else return false; } cur = new Node(key, value); if (key>parent->_key) parent->_right = cur; else parent->_left = cur; return true; } private: Node* _root; };
查找就是查找key,跟value就没有关系啦
Node* 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 cur;
}
return nullptr;
}
删除
bool Erase(const K& key)
{
Node* parent = nullptr;
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
parent = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
parent = cur;
cur = cur->_left;
}
else//找到了
{
if (cur->_left == nullptr)
{
if (parent == nullptr)
_root = cur->_right;
else
{
if (parent->_left = cur)
parent->_left = cur->_right;
else
parent->_right = cur->_right;
delete cur;
}
}
else if (cur->_right==nullptr)
{
if (parent == nullptr)
_root = cur->_left;
else
{
if (parent->_left = cur)
parent->_left = cur->_left;
else
parent->_right = cur->_left;
}
delete cur;
}
else//两个孩子
{
Node* minparent = cur;//用来删除之后链接
Node* min = cur->_right;
while (min->_left)
{
minparent = min;
min = min->_left;//找右子树的最小值,找左节点
}
cur->_key = min->_key;
cur->_value = min->_value;
if (minparent->_left == min)
minparent->_left = min->_right;
else
minparent->_right = min->_right;
delete min;
}
return true;
}
}
return false;
}
中序遍历打印
void InOrder()
{
_InOrder(_root);
cout << endl;
}
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_key << ":" << root->_value << endl;
_InOrder(root->_right);
}
测试
void TestTree1()
{
BSTree dict;
dict.Insert("sort", "排序");
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("map", "地图,映射");
string str;
while (cin >> str)
{
BSTreeNode* ret = dict.Find(str);
if (ret)
cout << "对应中文解释:" << ret->_value << endl;
else
cout << "无此单词" << endl;
}
}
void TestTree1()
{
BSTree dict;
dict.Insert("sort", "排序");
dict.Insert("left", "左边");
dict.Insert("right", "右边");
dict.Insert("map", "地图,映射");
string str;
while (cin >> str)
{
BSTreeNode* ret = dict.Find(str);
if (ret)
cout << "对应中文解释:" << ret->_value << endl;
else
cout << "无此单词" << endl;
}
} 


