【数据结构】二叉搜索树

【数据结构】二叉搜索树,第1张

【数据结构】二叉搜索树,第2张

樊梓慕:个人主页

 个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

每一个不曾起舞的日子,都是对生命的辜负


前言

本篇文章博主会对二叉搜索树的一些特性进行讲解,并且进行模拟实现。


欢迎大家收藏以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:樊飞 (fanfei_c) - Gitee.com

=========================================================================


1.概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

2.模拟实现

2.1节点类

一般像这种链式结构,我们都要实现一个节点类出来用来构建联系。

  • 结点类当中包含三个成员变量:结点值、左指针、右指针。

template<class K> struct BSTreeNode { BSTreeNode<K>* _left; BSTreeNode<K>* _right; K _key; BSTreeNode(const K& key = 0) :_left(nullptr) , _right(nullptr) , _key(key) {} };


2.2构造函数

因为这里的构造不需要特定的要求,只需要构造一棵空树,即给root赋值为nullptr即可。

//构造函数 BSTree() :_root(nullptr) {}

或者你可以让系统自己生成一个默认的构造。

BSTree() = default;


2.3拷贝构造函数

注意这里完成的是深拷贝,不能是值拷贝,防止二次析构发生。

//拷贝构造函数 BSTree(const BSTree<K>& t) { _root = Copy(t._root); }//设置private限定,不要暴露该接口 Node* Copy(Node* root) { if (root == nullptr) return nullptr; Node* newRoot = new Node(root->_key); newRoot->_left = Copy(root->_left); newRoot->_right = Copy(root->_right); return newRoot; }


2.4赋值运算符重载

在之前数据结构的模拟实现中,一般对于赋值运算符这块我们已经学习了现代写法,直接swap即可。

//赋值运算符重载函数 BSTree<K>& operator=(BSTree<K> t) { swap(_root, t._root); return *this; }

原理:=的右值由于参数传递的不是引用,所以会调用自身的拷贝构造形成一个临时对象,交换临时对象与左值根节点后,此时左值根节点已经是之前的右值根节点了,然后返回左值根节点完成赋值,结束后右值根节点会被析构(即之前的左值)。

当然还有以下这种传统写法。

//传统写法 const BSTree<K>& operator=(const BSTree<K>& t) { if (this != &t) //防止自己给自己赋值 { _Destory(_root); //先将当前的二叉搜索树中的结点释放 _root = _Copy(t._root); //拷贝t对象的二叉搜索树 } return *this; //支持连续赋值 }


2.5析构函数

二叉树的析构一定采用『 后序』的方式。

//析构函数 ~BSTree() { Destroy(_root); }void Destroy(Node* root) { if (root == nullptr) return; Destroy(root->_left); Destroy(root->_right); delete root; }


2.6查找函数

根据二叉搜索树的特性(左子树都小于根,右子树都大于根),我们在二叉搜索树当中查找指定值的结点的方式如下:

  • 若树为空树,则查找失败,返回nullptr。
  • 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  • 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  • 若key值等于当前结点的值,则查找成功,返回对应结点的地址。

非递归方式: 

bool Find(const K& key) { Node* cur = _root; while (cur) { if (cur->_key < key) { cur = cur->_right; } else if (cur->_key > key) { cur = cur->_left; } else { return true; } } return false; }

 递归方式:

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时,我们没法直接传递根节点『 根节点是private域』,所以我们需要通过FindR获取到*this从而获取到_root,然后再调用_FindR,_FindR设计的参数为root和key就可以实现逻辑了,包括后面的递归方式都是这个思路。


2.7插入函数

插入的具体过程如下:

  • 树为空,则直接新增节点,赋值给root指针;
  • 树不空,按二叉搜索树性质查找插入位置,插入新节点。

非递归方式: 

使用非递归方式实现二叉搜索树的插入函数时,找到插入位置后我们需要new新节点,然后将该节点与对应的父节点进行连接,所以我们需要定义一个parent指针,该指针用于标记待插入结点的父结点。

注意:连接parent和cur时,需要判断应该将cur连接到parent的左边还是右边。

//插入函数 bool Insert(const K& key) { if (_root == nullptr) { _root = new Node(key); return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key < key) { parent = cur; cur = cur->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else { return false; } } cur = new Node(key); if (parent->_key < key) //key值大于当前parent结点的值 { parent->_right = cur; //将结点连接到parent的右边 } else //key值小于当前parent结点的值 { parent->_left = cur; //将结点连接到parent的左边 } return true; }

递归方式:

同样的,对于递归方式来讲,我们如何将新节点与父节点进行连接呢?

这里只需要给参数上加一个『 引用』即可。

bool _InsertR(Node*& root, const K& key) { if (root == nullptr) { root = new Node(key); return true; } if (root->_key < key) { return _InsertR(root->_right, key); } else if (root->_key > key) { return _InsertR(root->_left, key); } else { return false; } }


2.8删除函数

二叉搜索树的删除函数情况比较复杂,场景比较多。

若是在二叉树当中没有找到待删除结点,则直接返回false表示删除失败即可,但若是找到了待删除结点,此时就有以下三种情况:

  • 待删除结点的左子树为空(待删除结点的左右子树均为空包含在内)。
  • 待删除结点的右子树为空。
  • 待删除结点的左右子树均不为空。

分情况进行讨论:

(1)待删除结点的左子树为空(待删除结点的左右子树均为空包含在内)。

若待删除结点的左子树为空,那么找到待删除结点后,只需先让其父结点指向该结点的右孩子结点,然后再将该结点释放。

(2)待删除结点的右子树为空。

若待删除结点的右子树为空,那么找到待删除结点后,只需先让其父结点指向该结点的左孩子结点,然后再将该结点释放。

(3)待删除结点的左右子树均不为空。

比如以下场景,要删除节点『 7』。

【数据结构】二叉搜索树,第3张

利用替换法:

step1:

  • 将待删除结点与待删除结点左子树当中值最大的结点“替换”(这里的替换指将该最大节点赋值给待删除节点)
  • 或者将待删除结点与待删除结点右子树当中值最小的结点“替换”(这里的替换指将该最小节点赋值给待删除节点)『 以此为例』

【数据结构】二叉搜索树,第4张

step2:

然后将该替换后的节点删除,该替换后的节点必然左右子树当中至少有一个为空树,因此删除该结点的方法与前面说到的情况一和情况二的方法相同。

注意:只能是待删除结点左子树当中值最大的结点,或是待删除结点右子树当中值最小的结点代替待删除结点被删除,因为只有这样才能使得进行删除 *** 作后的二叉树仍保持二叉搜索树的特性。

【数据结构】二叉搜索树,第5张

非递归方式:

  • 使用minParent标记待删除结点右子树当中值最小结点的父结点。
  • 使用minRight标记待删除结点右子树当中值最小的结点。

左子树的最大一定是左子树中最右面的节点;

右子树的最小一定是右子树中最左面的节点。 

当找到待删除结点右子树当中值最小的结点时,先将待删除结点的值改为minRight的值,之后直接判断此时minRight是minParent的左孩子还是右孩子,然后对应让minParent的左指针或是右指针转而指向minRight的右孩子(注意:minRight的左孩子为空),最后将minRight结点进行释放即可。

bool Erase(const K& key) { Node* parent = nullptr; //记录待删除结点的父结点 Node* cur = _root; //记录待删除结点 while (cur) { if (key < cur->_key) //key值小于当前结点的值 { parent = cur; cur = cur->_left; } else if (key > cur->_key) //key值大于当前结点的值 { parent = cur; cur = cur->_right; } else //找到了待删除结点 { if (cur->_left == nullptr) //待删除结点的左子树为空 { if (cur == _root) //待删除结点是根结点,此时parent为nullptr { _root = cur->_right; //二叉搜索树的根结点改为根结点的右孩子即可 } else //待删除结点不是根结点,此时parent不为nullptr { if (cur == parent->_left) //待删除结点是其父结点的左孩子 { parent->_left = cur->_right; //父结点的左指针指向待删除结点的右子树即可 } else //待删除结点是其父结点的右孩子 { parent->_right = cur->_right; //父结点的右指针指向待删除结点的右子树即可 } } delete cur; //释放待删除结点 return true; //删除成功,返回true } else if (cur->_right == nullptr) //待删除结点的右子树为空 { if (cur == _root) //待删除结点是根结点,此时parent为nullptr { _root = cur->_left; //二叉搜索树的根结点改为根结点的左孩子即可 } else //待删除结点不是根结点,此时parent不为nullptr { if (cur == parent->_left) //待删除结点是其父结点的左孩子 { parent->_left = cur->_left; //父结点的左指针指向待删除结点的左子树即可 } else //待删除结点是其父结点的右孩子 { parent->_right = cur->_left; //父结点的右指针指向待删除结点的左子树即可 } } delete cur; //释放待删除结点 return true; //删除成功,返回true } else //待删除结点的左右子树均不为空 { //替换法删除 Node* minParent = cur; //标记待删除结点右子树当中值最小结点的父结点 Node* minRight = cur->_right; //标记待删除结点右子树当中值最小的结点 //寻找待删除结点右子树当中值最小的结点 while (minRight->_left) { //右子树中最小一定在最左面 minParent = minRight; minRight = minRight->_left; } cur->_key = minRight->_key; //将待删除结点的值改为minRight的值 //注意一个隐含条件:此时minRight的_left为空 if (minRight == minParent->_left) //minRight是其父结点的左孩子 { minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可 } else //minRight是其父结点的右孩子 { minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可 } delete minRight; //释放minRight return true; //删除成功,返回true } } } return false; //没有找到待删除结点,删除失败,返回false }

 递归方式:

在找到了待删除节点后的思路与 *** 作一样,未找到之前改换为递归方式即可。 

bool _EraseR(Node*& root, const K& key) { if (root == nullptr) return false; if (key < root->_key) //key值小于根结点的值 return _EraseR(root->_left, key); //待删除结点在根的左子树当中 else if (key > root->_key) //key值大于根结点的值 return _EraseR(root->_right, key); //待删除结点在根的右子树当中 else //找到了待删除结点 { if (root->_left == nullptr) //待删除结点的左子树为空 { Node* del = root; //保存根结点 root = root->_right; //根的右子树作为二叉树新的根结点 delete del; //释放根结点 } else if (root->_right == nullptr) //待删除结点的右子树为空 { Node* del = root; //保存根结点 root = root->_left; //根的左子树作为二叉树新的根结点 delete del; //释放根结点 } else //待删除结点的左右子树均不为空 { Node* minParent = root; //标记根结点右子树当中值最小结点的父结点 Node* minRight = root->_right; //标记根结点右子树当中值最小的结点 //寻找根结点右子树当中值最小的结点 while (minRight->_left) { //右子树中最小一定在最左面 minParent = minRight; minRight = minRight->_left; } root->_key = minRight->_key; //将根结点的值改为minRight的值 //注意一个隐含条件:此时minRight的_left为空 if (minRight == minParent->_left) //minRight是其父结点的左孩子 { minParent->_left = minRight->_right; //父结点的左指针指向minRight的右子树即可 } else //minRight是其父结点的右孩子 { minParent->_right = minRight->_right; //父结点的右指针指向minRight的右子树即可 } delete minRight; //释放minRight } return true; //删除成功,返回true } }bool EraseR(const K& key) { return _EraseR(_root, key); //删除_root当中值为key的结点 }

3.二叉搜索树的应用

3.1『 K模型』

K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

比如:

给一个单词word,判断该单词是否拼写正确,具体方式如下:

  • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

3.2『 KV模型』

每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。

namespace key_value { template<class K, class V> struct BSTreeNode { typedef BSTreeNode<K, V> Node; Node* _left; Node* _right; K _key; V _value; BSTreeNode(const K& key, const V& value) :_left(nullptr) ,_right(nullptr) ,_key(key) ,_value(value) {} }; template<class K, class V> class BSTree { typedef BSTreeNode<K, V> Node; public: bool Insert(const K& key, const V& value) { if (_root == nullptr) { _root = new Node(key, value); return true; } Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key < key) { parent = cur; cur = cur->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else { return false; } } cur = new Node(key, value); if (parent->_key < key) { parent->_right = cur; } else { parent->_left = cur; } return true; } Node* Find(const K& key) { Node* cur = _root; while (cur) { if (cur->_key < key) { cur = cur->_right; } else if (cur->_key > key) { cur = cur->_left; } else { return cur; } } return nullptr; } bool Erase(const K& key) { Node* parent = nullptr; Node* cur = _root; while (cur) { if (cur->_key < key) { parent = cur; cur = cur->_right; } else if (cur->_key > key) { parent = cur; cur = cur->_left; } else { if (cur->_left == nullptr) { if (cur == _root) { _root = cur->_right; } else { if (cur == parent->_right) { parent->_right = cur->_right; } else { parent->_left = cur->_right; } } delete cur; return true; } else if (cur->_right == nullptr) { if (cur == _root) { _root = cur->_left; } else { if (cur == parent->_right) { parent->_right = cur->_left; } else { parent->_left = cur->_left; } } delete cur; return true; } else { // 替换法 Node* rightMinParent = cur; Node* rightMin = cur->_right; while (rightMin->_left) { rightMinParent = rightMin; rightMin = rightMin->_left; } cur->_key = rightMin->_key; if (rightMin == rightMinParent->_left) rightMinParent->_left = rightMin->_right; else rightMinParent->_right = rightMin->_right; delete rightMin; return true; } } } return false; } void InOrder() { _InOrder(_root); cout << endl; } private: void _InOrder(Node* root) { if (root == nullptr) return; _InOrder(root->_left); cout << root->_key << " "; _InOrder(root->_right); } private: Node* _root = nullptr; }; }


4.二叉树的性能分析

『 查找效率』代表了二叉搜索树中各个 *** 作的性能,因为插入和删除 *** 作都必须先查找。

对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

【数据结构】二叉搜索树,第6张

  • 最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log(N)
  • 最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N/2

时间复杂度描述的是最坏情况下算法的效率,因此普通二叉搜索树各个 *** 作的时间复杂度都是O(N)。

如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?(即log(N))

我们后面会学习AVL树与红黑树,他们对二叉搜索树进行了一定的优化,使得二叉搜索树的性能都能达到最优。


=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

博主很需要大家的支持,你的支持是我创作的不竭动力

~ 点赞收藏+关注 ~

=========================================================================

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/tougao/13518590.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2024-02-07
下一篇 2024-02-12

发表评论

登录后才能评论

评论列表(0条)

保存