赞
踩
目录
各位想看代码去我的码云仓库看吧,这里就不在粘贴了。
https://gitee.com/j-jun-jie/c---advanced.githttps://gitee.com/j-jun-jie/c---advanced.git
二叉搜索树(搜索二叉树)又称二叉排序树,它可以是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值。
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值。
- 它的左右子树也分别为二叉搜索树。
也就是说左子树的值<根节点<右子树的值。
那我们再看一下这两个
都不行,第一个,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<class K> struct BSTreeNode { BSTreeNode<K>* _left; //左指针 BSTreeNode<K>* _right;//右指针 K _key;//节点值 BSTreeNode(const K& key)//构造函数 :_left(nullptr) , _right(nullptr) , _key(key) {} }; template<class K> class BStree//树结构 { typedef BSTreeNode<K> Node; public: //构造函数只需要将根初始化为空就行了 BSTree() :_root(nullptr) {} private: Node* _root;//根 //Node* _root(nullptr); 构造函数不用写了 };这段代码使用了C++中的模板(template)机制,它允许我们编写通用的数据结构或算法,以支持多种数据类型。
第一次出现template是在定义BSTreeNode结构体时,它使用了一个类型参数K,表示节点的值的类型可以是任意类型。这样,我们可以在创建树的实例时,指定具体的节点值类型,例如int、double、string等。
第二次出现template是在定义BStree类时,它也使用了一个类型参数K,表示整个二叉搜索树中的节点值类型。这个类型参数和BSTreeNode结构体中的类型参数是一样的,它们是共享的。
在使用模板时,通常需要在定义和实现中都使用template关键字,并在尖括号中指定类型参数。这样,编译器就能够根据实际类型生成特定的代码。
当我们写好插入删除等功能后,我们可以用中序遍历进行打印,因为他的遍历方式是左节点->根->右节点,而搜索二叉树正好是左,根,右升序排列,所以我们用中序遍历。
void _InOrder(Node* root) //中序遍历。但是这个必须传参,参数是私有的,不能用,那咋办?套一层 { if (root == nullptr) { return ; } _InOrder(root->_left); cout << root->_key << " "; _InOrder(root->_right); }但是要想调用这个函数,我们要给他传参就是传过去_root,但是这个_root是私有成员,没办法出了类使用,那我们咋办,
我们可以在共有区域里在写一个不需要参数的函数在这个函数里面调用这个函数。
void InOrder() { _InOrder(_root); cout << endl; }这样就不需要传参了。就可以实现遍历了。
思路清晰又简单:
- 若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值等于当前结点的值,则查找成功,返回对应结点的地址。
我们在这也可以向上面的中序遍历一样再套一个函数,这样既能隐藏递归函数,也能在类外调用。
bool FindR(const K& key) { return _FindR(_root, key); } private: bool _FindR(Node* root,const K& key) { if (root == nullptr) return false; if (root->_key < key) return _FindR(root->_right, key); else if (root->_key > key) return _FindR(root->_left, key); else return true; }这里的_FindR定义在私有区里,FindR定义在共有区里面,我们到下面在解释。
- 要插入二叉树中就要找位置:
如果是空树:直接插入,把插入的节点作为根节点。
- 不是空树,待机而动。
要实现插入,首先做好准备工作,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结点。
其实这个就是往一个孩子节点或者没有孩子节点上转换,为啥要找要删除节点的左孩子的最大右孩子节点或者右孩子的最小左孩子节点。
替换节点要满足两个条件:
- 1.替换节点要接近删除节点,要不不一定满足这个替换节点大于删除节点的左孩子,小于删除节点的右孩子。
- 2.例如最小左孩子节点要么他有叶子节点,要么它有一个右孩子节点,那这个就符合上面的有一个孩子节点或者没有孩子节点的情况了,这就带回去了。
如果遍历一遍都找不到要删除的值,就说明该数不存在,就返回false。
我们先写一下只有其中一个孩子节点的。但是会出现以下情况:
- 首先我们要先判断待删除节点位于父节点的左边还是右边,就比如上面这两种情况,我们不能把cur的右子节点随意链接父节点。
//删除函数非递归实现 bool Erase(const K& key) { Node* parent = nullptr; //起链接作用 Node* cur = _root; //节点移动的 while (cur) { if (cur->_key < key) { parent = cur; cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。 } else if (cur->_key > key) { parent = cur; cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。 } else// 找到了,要删除 { //1.左孩子为空 //2.右孩子为空 //3.左右孩子都不为空 if (cur->_left == nullptr) //删除节点的左孩子为空 { if (cur == parent->left) //cur在父节点的左边 { parent->_left = cur->_right; //把cur的右节点连接到父节点的左节点处。 } else { parent->_right = cur->_right; //连接到右节点处 } } //2.右孩子为空 else if (cur->_right == nullptr) { if (cur == parent->left) //cur在父节点的左边 { parent->_left = cur->_left; //把cur的左节点连接到父节点的左节点处。 } else { parent->_right = cur->_left; //连接到右节点处 } } //3.左右孩子都不为空 } } }
- 这是其中的普通情况,来个极端情况,删除父节点。
此时parent时空指针,在调用其他就错,所以我们还要再来一步,判断是不是删除根节点.
else// 找到了,要删除 { //1.左孩子为空 //2.右孩子为空 //3.左右孩子都不为空 if (cur->_left == nullptr) //删除节点的左孩子为空 { //要删除根节点 if (cur == _root) { _root = cur->_right; } else //删除其他节点 { if (cur == parent->left) //cur在父节点的左边 { parent->_left = cur->_right; //把cur的右节点连接到父节点的左节点处。 } else { parent->_right = cur->_right; //连接到右节点处 } } delete cur; } //2.右孩子为空 else if (cur->_right == nullptr) { if(cur==_root) { _root = cur->_left; //根节点右孩子为空,就指向他的左孩子 } else { if (cur == parent->left) //cur在父节点的左边 { parent->_left = cur->_left; //把cur的左节点连接到父节点的左节点处。 } else { parent->_right = cur->_left; //连接到右节点处 } } delete cur; }
- 那这两种情况弄完就差左右孩子都存在了。
但是删除时会出现这个情况:
我找到了最小左节点时,但是它下面还有一个右孩子。所以我们用minparent来连接这个右孩子。
//3.左右孩子都不为空 else { Node* minparent = nullptr; Node* minright = cur->_right; while(minright->_left) //找最小左节点 { minparent = minright; minright = minright->_left; } swap(minright->_key, cur->_key); minparent->_left = minright->_right; //把最小左节点的右节点给父节点的左节点 delete minright; }这个思路是找到最小左孩子后交换待删除数和这个左孩子,然后删除这个最小左孩子。
- 但是如果要删除根节点8呢?
此时10是最小左节点 ,,所以下面的while就不会进入,那minparen他就又是空指针了。所以我们可以minparent=cur。但是我们看倒数第二步,minright是10,minparent是根节点8,这一步直接把10的右节点连接到根节点的左边了,这就错了,他要连接到右边。之所以出现这一步是因为10并没有左节点,所以就不能连接到左节点处。所以我们要先判断一下到底在哪边。
else { Node* minparent = cur; //赋值为空时,删除根节点时就错了,一步到位赋值cur Node* minright = cur->_right; while(minright->_left) //找最小左节点 { minparent = minright; minright = minright->_left; } swap(minright->_key, cur->_key); if (minparent->_left == minright) //如果minright在minparent的左边 { minparent->_left = minright->_right; //把他连接到左边 } else { minparent->_right = minright->_right; } delete minright;直接把没有孩子的情况当作情况2的一种特殊类型处理了。
我们来一份完整的删除代码:
//删除函数非递归实现 bool Erase(const K& key) { Node* parent = nullptr; //起链接作用 Node* cur = _root; //节点移动的 while (cur) { if (cur->_key < key) { parent = cur; cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。 } else if (cur->_key > key) { parent = cur; cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。 } else// 找到了,要删除 { //1.左孩子为空 //2.右孩子为空 //3.左右孩子都不为空 if (cur->_left == nullptr) //删除节点的左孩子为空 { //要删除根节点 if (cur == _root) { _root = cur->_right; } else //删除其他节点 { if (cur == parent->_left) //cur在父节点的左边 { parent->_left = cur->_right; //把cur的右节点连接到父节点的左节点处。 } else { parent->_right = cur->_right; //连接到右节点处 } } delete cur; } //2.右孩子为空 else if (cur->_right == nullptr) { if(cur==_root) { _root = cur->_left; //根节点右孩子为空,就指向他的左孩子 } else { if (cur == parent->_left) //cur在父节点的左边 { parent->_left = cur->_left; //把cur的左节点连接到父节点的左节点处。 } else { parent->_right = cur->_left; //连接到右节点处 } } delete cur; } //3.左右孩子都不为空 else { Node* minparent = cur; //赋值为空时,删除根节点时就错了,一步到位赋值cur Node* minright = cur->_right; while(minright->_left) //找最小左节点 { minparent = minright; minright = minright->_left; } swap(minright->_key, cur->_key); if (minparent->_left == minright) //如果minright在minparent的左边 { minparent->_left = minright->_right; //把他连接到左边 } else { minparent->_right = minright->_right; } delete minright; } return true; } } return false; }
一,思路和非递归基本一致,多次递归找到合适的删除位置:
非递归找合适的删除位置时用到了遍历,我们在这用递归更方便。
- 若当前结点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(Node*& root, const K& key) { if (root == nullptr) { return false; //空树找个毛线。 } if (root->_key < key) //比节点值大,往右走 { return _EraseR(root->_right, key); } else if (root->_key > 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* minright = root->_right; while (minright->left) { minright = minright->_left; } swap(minright->_key, root->_key); return _EraseR(key); } delete del; } }那各位看一下我的左右节点都在那个情况,交换完之后利用递归删除minright思路对不对?
不对
按照刚才的思路,4,3交换完之后,我们重新调用递归删除,但是我们找一下3在哪,3<8,往左走,3<4往左走,3>1,往右走,那3现在在1的右孩子处,不是6的左孩子处了,所以我们应该所以调用下面的递归是我们要让它指向root的右孩子处。
//递归实现 bool _EraseR(Node*& root, const K& key) { if (root == nullptr) { return false; //空树找个毛线。 } if (root->_key < key) //比节点值大,往右走 { return _EraseR(root->_right, key); } else if (root->_key > 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* minright = root->_right; while (minright->_left) { minright = minright->_left; //让minright成为最小左节点 } swap(minright->_key, root->_key); return _EraseR(root->_right,key); } delete del; return true; } }
构造函数我们可以让编译器直接默认生成即可。但是如果写了拷贝构造函数它就不在默认生成了。这里有两种解决方法。
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<K>& t) { _root = t.CopyT(t._root); }
要实现t1,t2的赋值操作,那我们可以利用一下上面的拷贝构造函数。当t2传值传参时我们进行拷贝构造生出t,让后在交换t1和t的根节点即可。
//赋值运算符重载函数 BSTree<K>& operator=(BSTree<K> 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,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:
- <单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key。
- 查询英文单词时,只需给出英文单词,就可快速找到与其对应的key。
KV模型可以插入重复值,在K模型的基础上,节点增加了_value成员,用来_key去查找_value,_value的类型不确定,再增加一个模板参数即可。
namespace key_value { void TestBSTree1() { BSTree<string, string> Dict; Dict.InsertR("zuozishu", "左子树"); Dict.InsertR("二叉树", "二叉树"); Dict.InsertR("left", "左边"); Dict.InsertR("right", "右边"); string str; while (cin >> str) { //BSTreeNode<string, string>* 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种:
- 层序遍历 :一层一层遍历
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。