二叉搜索树(搜索二叉树)又称二叉排序树,它可以是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
- 它的左右子树也分别为二叉搜索树。
也就是说左子树的值<根节点<右子树的值。
那我们再看一下这两个
都不行,第一个,16的左子树的任何值都要小于16,17>16不行。
第二个。19<28没问题。但是20的右子树要大于20,19<20也不行。
- 如果我们按左子树,根,右子树顺序排列,那这个一定是升序排列(左<根<右)所以我们进行中序排列。
在C语言中我们实现二叉树首先是定义一个结构体存放根节点的左右指针和数值。然后将函数名和功能实现分开实现。
typedef int BTDataType; typedef struct BinaryTreeNode {BTDataType data;struct BinaryTreeNode* left;struct BinaryTreeNode* right; }BTNode;
但是现在我们学到了类我们换种花样。
我先定义一个结构体存放根节点的左右指针和数值。
第二,用类用来实现二叉树的功能函数(增删查改)。
template
struct BSTreeNode {BSTreeNode * _left; //左指针BSTreeNode * _right;//右指针K _key;//节点值BSTreeNode(const K& key)//构造函数:_left(nullptr), _right(nullptr), _key(key){} }; template class BStree//树结构 {typedef BSTreeNode Node; public://构造函数只需要将根初始化为空就行了BSTree():_root(nullptr){}private:Node* _root;//根//Node* _root(nullptr); 构造函数不用写了 };
- 要插入二叉树中就要找位置:
如果是空树:直接插入,把插入的节点作为根节点。
- 不是空树,待机而动。
要实现插入,首先做好准备工作,cur指针从节点开始进行移动,直到插入合适位置,parent在cur移动时到cur的位置(相当于他的父节点)起到插入后的连接作用。key是要插入的节点值。
- 节点key<当前节点 ,parent到cur位置,cur左移,继续遍历。
- 节点key>当前节点 ,parent到cur位置,cur右移,继续遍历。
- 节点key=当前节点,返回false,因为二叉树中不允许有重复值。
bool Insert(const K& key){if (_root == nullptr)//若二叉树树为空{_root = new Node(key);//创造一个值为key的新节点return true;}Node* parent = nullptr;Node* cur = _root;//1、找位置while (cur){if (cur->_key < key)//_key节点的值,key是要插入的值{parent = cur;cur = cur->_right;//让cur往右走继续遍历}else if (cur->_key > key)//若key小于当前结点值{parent = cur;cur = cur->_left;//让cur往左走}else{return false;//若key等于当前结点值,说明插入的值不合法,返回false}}//2、链接cur = new Node(key);if (parent->_key < key){parent->_right = cur;//比父亲的值大连接在右子树}else{parent->_left = cur;//比父亲的值小链接在左子树}return true;}
递归实现的思路和上面大差不差,递归到合适的位置,然后在链接。
步骤根上面的一样。
- 若key > root指向的结点值,让root递归到右子树继续遍历。
- 若key < root指向的结点值,让root递归到左子树继续遍历。
- 若key = root指向的结点值,说明待插入的结点值与此树当前结点值重合,插入结点失败。返回false。
//插入 bool _InsertR(Node*& root, const K& key){if (root == nullptr)//找到位置了{root = new Node(key);return true;}if (key < root->_key)//到左子树去找位置{_InsertR(root->_left, key);}else if (key > root->_key)//到右子树去找位置{_InsertR(root->_right, key);}else//已存在,无需插入{return false;}}
root为啥要传引用?不能直接用指针?
当我们经历一些步骤到14的右子树处准备插入时,root是_root的别名,而最后一步递归是root->right,也就是说我们修改也只会修改_root->右子树,所以直接链接起来了。
- 1.找位置 (小于节点值往左走,大于往右走)
- 2.删除 --链接(递归实现,非递归实现)
在进行链接时会有两种情况:
- 1.删除的时叶子节点,下面没有节点了。
- 2.删除的节点还有孩子节点。
1.有一个孩子节点如图
就把该节点的孩子节点的链接给该节点的父亲,顶替自己的位置。
2.有两个孩子节点:
那我们就要找左孩子节点中的最大值或者右孩子节点中的最小值进行替换。
替换步骤如下:
- 定义myParent指针为cur指针的位置(myParent指针用于链接要删除结点的孩子)。
- 定义minRight指针为cur的右孩子结点指针的位置(minRight用于找到右子树的最小值)。
- 遍历minRight找到待删结点右子树的最小值(或左子树的最大值结点),中途不断更新myParent。
- 找到后,利用swap函数交换此最小值结点的值(minRight->_key)和待删结点的值(cur->_key)。
- 交换后,链接父亲myParent指针与minRight结点的孩子。
- 最后记得delete删除minRight结点。
如果遍历一遍都找不到要删除的值,就说明该数不存在,就返回false。
1.我们要删除3,cur先走找3
2.minparent在cur处,所以下一步找cur右子树中的最小值,把这个最小值给minright。
3.让minparent指向这个最小值的父结点处,起链接作用。
4.我们交换要删除的cur和最小值minright。
5.然后删除minright.出来minparent.
bool Erase(const K& key){Node* parent = nullptr;Node* cur = _root;while (cur){if (cur->_key > key)//就往左走继续遍历{parent = cur;cur = cur->_left;}else if (cur->_key < key)//比当前节点大,就往右走继续遍历{parent = cur;cur = cur->_right;}else//找到了,要删除{//1.删除的是叶子节点, 删除节点后把父亲指向自己的指针置空(特殊的第二类,parent指向空)。//2.有一个孩子的节点,就把它的孩子节点的链接给它的父亲,顶替自己的位置。//3.有两个孩子的节点,找比它自己的左孩子大,比它自己的右孩子小的节点替换它,//也就是它的左子树的最大节点或右子树的最小节点替换它,它就只有一个孩子或没有孩子了。if (cur->_left == nullptr)//cur左为空,就让父亲指向cur的右{//如果要删除根,直接让根的右孩子做根if (cur == _root){_root = cur->_right;}else //不是根{if (parent->_left == cur)//当cur是父亲的左时,就让父亲的左指向cur的右{parent->_left = cur->_right;}else//当cur是父亲的右时,就让父亲的右指向cur的右{parent->_right = cur->_right;}}//要删除节点的左孩子为空,不是要删除节点为左孩子,所以删除节点有左右两种情况delete cur;}else if (cur->_right == nullptr)//cur右为空,就让父亲指向cur的左{//如果要删除根,直接让根的右孩子做根if (cur == _root){_root = cur->_left;}else{if (parent->_left == cur)//当cur是父亲的左时,就让父亲的左指向cur的左{parent->_left = cur->_left;}else//当cur是父亲的右时,就让父亲的右指向cur的左{parent->_right = cur->_left;}}delete cur;//删除}else//左右孩子都存在,替换法删除{//找右子树最左节点 当右子树的左孩子不为空时就继续遍历Node* minRight = cur->_right;Node* minParent = cur;//这里不要初始化成null,否则左为空时,minParent->_left就会崩掉//当左不为空时,就一直向左走,直到找到右子树最左节点while (minRight->_left){minParent = minRight;minRight = minRight->_left;}//保存替换节点的值cur->_key = minRight->_key;//删除替换节点if (minParent->_left == minRight)//如果右子树最左节点是minParent的左,那就让minParent的左指向右子树最左节点的右{minParent->_left = minRight->_right;}else//如果右子树最左节点是minParent的右,那就让minParent的右指向右子树最左节点的右{minParent->_right = minRight->_right;}delete minRight;//删除}return true;}}return false;//cur不存在,直接返回}
我把没有孩子的情况当作情况2的一种特殊类型处理了。
一,思路和非递归基本一致,多次递归找到合适的删除位置:
非递归找合适的删除位置时用到了遍历,我们在这用递归更方便。
- 若当前结点root为空,说明此删除的结点不存在,返回false
- 若key > root指向的结点值,让root递归到右子树继续遍历。
- 若key < root指向的结点值,让root递归到左子树继续遍历。
二,找到待删数值进行链接时我们也会遇到两种情况:
1.待删除数有一个子树--------左子树,右子树,左右为空。
- 我们先将待删除的root放在del中保存起来。
- 判断root的左孩子存在还是右孩子(rright)存在。_root是root父节点,我们再把rright连接在_root的右节点(_rright)处。只要root大于_root,我们就把root的子树连接到_root的右子树处。在连接时&会直接帮我们进行两个结点的链接,我们不需要操心。
- 如果root左子树为空:执行root = root->_right。
- 如果root右子树为空:执行root = root->_left。
我们删除真正实现的是,让root(待删除数)的父节点直接链接root的子节点,把root跳过就是删除操作了。
2.待删数值子树全部存在。
这个跟非递归实现几乎一毛一样。
- 先用del保存root的值,设置一个minright保存root的右子树的最小值。
- 遍历minright找到最小值。
- 利用交换函数swap交换minright->key和root->key。
- 交换后利用递归进行删除minrght。
就是最后那个交换后利用递归删除可能有点麻烦。
//递归版删除 bool EraseR(const K& key) {return _EraseR(_root, key); } //删除的子树 bool _EraseR(Node*& root, const K& key) {//1、递归查找删除的位置if (root == nullptr){//如果是空就返回falsereturn false;}if (root->_key < key){return _EraseR(root->_right, key);//如果比key小,转换到右子树去插入}else if (root->_key > key){return _EraseR(root->_left, key);//如果比key大,转换到左子树去插入}//2、确认链接关系else{Node* del = root;//提前保存root结点的位置//开始删除if (root->_left == nullptr){//如果左为空root = root->_right;}else if (root->_right == nullptr){//如果右为空root = root->_left;}else{Node* minRight = root->_right;//minRight用于找到右子树的最小值while (minRight->_left){minRight = minRight->_left;}swap(root->_key, minRight->_key);return _EraseR(root->_right, key);}delete del;return true;} }
思路清晰又简单:
- 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
- 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
- 若key值等于当前结点的值,则查找成功,返回true。
- 若遍历一圈cur走到nullptr了说明没有此结点,返回false
//Find bool Find(const K& key) {Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。}else if (cur->_key > key){cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。}else{return true;//若key值等于当前结点的值,则查找成功,返回true。}}return false;//没找到返回false }
递归实现也是需要注意前几步:
- 如果是空树查找失败,返回nullptr.
- 若key值小于当前结点的值,则递归到该结点的左子树当中进行查找。
- 若key值大于当前结点的值,则递归到该结点的右子树当中进行查找。
- 若key值等于当前结点的值,则查找成功,返回对应结点的地址。
//查找Node* _FindR(Node* root, const K& key){if (root == nullptr)//没找到{return nullptr;}if (key < root->_key)//到左子树去找{FindR(root->_left, key);}else if (key > root->_key)//到右子树去找{FindR(root->_right, key);}else//找到了{return root;} }
构造函数我们可以让编译器直接默认生成即可。但是如果写了拷贝构造函数它就不在默认生成了。这里有两种解决方法。
1.我们强制让他默认生成:
//强制编译器自己生成构造函数,忽视拷贝构造带来的影响 BSTree() = default;//C++11才支持
2.我们自己写一个构造函数
public://构造函数需要将根初始化为空就行了BSTree():_root(nullptr){}
就比如我们节点的构造中写到的。
一般二叉树基本不用拷贝构造函数,效率低不说,空间浪费太大了。
此时我们直接用前序递归的方式创建一颗与原来一样的二叉树。再用CopyT进行一系列的封装实现。
Node* CopyT(Node* root) {if (root == nullptr)return nullptr;Node* copyNode = new Node(root->_key);//拷贝根结点//递归创建拷贝一棵树copyNode->_left = CopyT(root->_left);//递归拷贝左子树copyNode->_right = CopyT(root->_right);//递归拷贝右子树return copyNode; } //拷贝构造函数--深拷贝 BSTree(const BSTree
& t) {_root = t.CopyT(t._root); }
要实现t1,t2的赋值操作,那我们可以利用一下上面的拷贝构造函数。当t2传值传参时我们进行拷贝构造生出t,让后在交换t1和t的根节点即可。
//赋值运算符重载函数 BSTree
& operator=(BSTree t) {//现代写法swap(_root, t._root);return *this; }
历经了数年,就连年年出现在英语中的李华都考上大学了,但是析构函数的功能还是没有任何变化。释放二叉树的所以结点。这里我们采用后序遍历方式进行展开 。
void ~DestoryTree(Node* root) {if (root == nullptr)return;//通过递归删除所有结点~DestoryTree(root->_left);//递归释放左子树中的结点~DestoryTree(root->_right);//递归释放右子树中的结点delete root; } //析构函数 ~BSTree() {~DestoryTree(_root);//复用此函数进行递归删除结点_root = nullptr; }
咱上面的这个二叉搜索树就是K模型,所以我不在写代码了。
K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。在K模型中不存在重复值(本来树就复杂,你小子还重复)。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
KV模型:每一个关键码key,都有与之对应的值Value,即
的键值对。该种方式在现实生活中非常常见: 比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
- <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key。
- 查询英文单词时,只需给出英文单词,就可快速找到与其对应的key。
KV模型可以插入重复值,在K模型的基础上,节点增加了_value成员,用来_key去查找_value,_value的类型不确定,再增加一个模板参数即可。
namespace key_value {void TestBSTree1(){BSTree
Dict;Dict.InsertR("zuozishu", "左子树");Dict.InsertR("二叉树", "二叉树");Dict.InsertR("left", "左边");Dict.InsertR("right", "右边");string str;while (cin >> str){//BSTreeNode * ret = Dict.FindR(str);auto ret = Dict.FindR(str);if (ret != nullptr){cout << "对应的中文:" << ret->_value << endl;}else{cout << "未找到,请重新输入" << endl;}}} } int main() {key_value::TestBSTree1(); }
深度优先遍历有3种:
- 前序遍历(先根遍历) 根->左->右
- 中序遍历(中根遍历) 左->根->右
- 后序遍历(后根遍历) 左->右->根
广度优先遍历有1种:
- 层序遍历 :一层一层遍历