经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
C++数据结构之二叉搜索树的实现详解
来源:jb51  时间:2022/8/22 16:17:57  对本文有异议

前言

今天我们来学一个新的数据结构:二叉搜索树。

介绍

二叉搜索树也称作二叉排序树,它具有以下性质:

  • 非空左子树的所有键值小于其根节点的键值
  • 非空右子树的所有键值大于其根节点的键值
  • 左,右子树都是二叉搜索树

那么我先画一个二叉搜索树给大家看看,是不是真的满足上面的性质。

我们就以根节点6为例子来看,我们会发现比6小的都在6的左边,而比6大的都在6的右边。对于6的左右子树来说,所有的节点都遵循这个规则。

同时我们还可以发现如果对搜索二叉树进行一个中序遍历,我们得到的序列是个有序序列,这就是为什么二叉搜索树也可以称作二叉排序树。

实现

节点的实现

  1. template <typename K>
  2. struct BTreeNode
  3. {
  4. BTreeNode<K>* _left;
  5. BTreeNode<K>* _right;
  6. K _key;
  7.  
  8. BTreeNode(const K& key)
  9. :_key(key), _left(nullptr), _right(nullptr)
  10. {}
  11. };

二叉搜索树的查找

二叉搜索树的查找思路很简单:要找的值比当前节点小就去左子树找,比当前节点大就往右子树找,找到空节点就说明没找到返回false即可。

首先我们先看看递归的版本。

  1. bool findR(const T &val, Node *root) //T为节点的K, Node为节点
  2. {
  3. if (root == nullptr)
  4. {
  5. return false;
  6. }
  7.  
  8. if (root->_key < val)
  9. {
  10. return findR(root->_right, val);
  11. }
  12. else if (root->_key > val)
  13. {
  14. return findR(root->_left, val);
  15. }
  16. else
  17. {
  18. return true;
  19. }
  20. }

接着看看非递归的版本

  1. bool find(const T &val)
  2. {
  3. Node *cur = _root; //_root为二叉搜索树的根节点
  4. while (cur)
  5. {
  6. if (val < cur->_key)
  7. {
  8. cur = cur->_left;
  9. }
  10. else if (val > cur->_key)
  11. {
  12. cur = cur->_right;
  13. }
  14. else
  15. {
  16. return true;
  17. }
  18. }
  19. return false;
  20. }

二叉搜索树的插入

二叉搜索树的插入和查找差别不大,首先寻找插入位置,比当前节点小就往左子树找,比当前节点大就往右子树找,直到找到空指针时,就可以进行一个插入。

那么要插入的值和当前节点相同该怎么办呢?我们此时实现的二叉搜索树是一个无重复元素的二叉搜索树,所以对于相同的值我采取的方式是返回一个false,大家如果想实现一个有重复元素的二叉搜索树,可以选择将重复的值放在右子树或左子树都可。

二叉搜索树的插入和查找一样,有递归和非递归两个版本,首先我们先来看看非递归的版本。

  1. bool insert(const T &val)
  2. {
  3. //空树直接改变根节点
  4. if (_root == nullptr)
  5. {
  6. _root = new Node(val);
  7. return true;
  8. }
  9. //非空树先寻找插入位置
  10. Node *pre = nullptr;
  11. Node *cur = _root;
  12. while (cur)
  13. {
  14. if (val > cur->_key)
  15. {
  16. pre = cur;
  17. cur = cur->_right;
  18. }
  19. else if (val < cur->_key)
  20. {
  21. pre = cur;
  22. cur = cur->_left;
  23. }
  24. else
  25. {
  26. return false;
  27. }
  28. }
  29. //判断新节点该插入到哪里
  30. cur = new Node(val);
  31. if (val < pre->_key)
  32. {
  33. pre->_left = cur;
  34. }
  35. else
  36. {
  37. pre->_right = cur;
  38. }
  39. return true;
  40. }

接下来用一个动画来表示一下这个插入过程。

接下来我们来看看递归版本是如何实现的

  1. bool insertR(const T &val, Node *&root)
  2. {
  3. if (root == nullptr)
  4. {
  5. Node *newNode = new Node(val);
  6. root = newNode;
  7. }
  8.  
  9. if (root->_key < val)
  10. {
  11. return insertR(val, root->_right);
  12. }
  13. else if (root->_key > val)
  14. {
  15. return insertR(val, root->_left);
  16. }
  17. else
  18. {
  19. return false;
  20. }
  21. }

此时我们可以发现,递归版本没有非递归版本中的parent指针了,参数列表中只有一个root指针,这是为什么呢?

我们可以看见我们的root指针不仅是一个指针,同时它还是一个引用。这就意味着我们对root的修改也可以影响上一层传递过来的指针的值。所以此时我们不需要parent指针,就可以对上一个节点的指针进行一个修改。

二叉搜索树的删除

理论部分:

二叉搜索树的删除相比前面两个要麻烦那么一丢丢,首先当然是找要删除的节点,找到后通常有以下三种情况:

  • 此节点无左右子树
  • 此节点有右子树或右子树
  • 此节点有左右子树

我们要做的就是对这三种情况分别进行一个处理。

1.首先是此节点无左右子树,这种情况我们直接将此节点删除即可

2.然后是此节点只有一颗子树,这个也比较简单,如果此节点是父节点的左节点,那么我们只需要将父节点的左指针改成指向此节点的子树即可。

3.最后一种就是既有左子树又有右子树的情况了,此时为了不破坏结构,我们需要用到替换删除法。首先我们先找到要删除的节点,然后找节点的左子树的最右节点或右子树的最左节点,将两个节点进行一下互换,再将原节点删除。

代码部分:

首先是非递归版本

  1. bool erase(const T &val)
  2. {
  3. Node *cur = _root;
  4. Node *pre = nullptr;
  5. //寻找删除位置
  6. while (cur)
  7. {
  8. if (cur->_key < val)
  9. {
  10. pre = cur;
  11. cur = cur->_right;
  12. }
  13. else if (cur->_key > val)
  14. {
  15. pre = cur;
  16. cur = cur->_left;
  17. }
  18. else //找到了进行删除
  19. {
  20. if (cur->_left == nullptr) //左子树为空
  21. {
  22. if (cur == _root)
  23. {
  24. _root = cur->_right;
  25. }
  26. else
  27. {
  28. if (cur == pre->_left)
  29. {
  30. pre->_left = cur->_right;
  31. }
  32. else
  33. {
  34. pre->_right = cur->_right;
  35. }
  36. }
  37. delete cur;
  38. }
  39. else if (cur->_right == nullptr) //右子树为空
  40. {
  41. if (cur == _root)
  42. {
  43. _root = cur->_left;
  44. }
  45. else
  46. {
  47. if (cur == pre->_left)
  48. {
  49. pre->_left = cur->_left;
  50. }
  51. else
  52. {
  53. pre->_right = cur->_left;
  54. }
  55. }
  56. delete cur;
  57. }
  58. else //左右子树都不为空
  59. {
  60. //找左子树的最右节点
  61. Node *tmp = cur->_left;
  62. Node *tmp_pre = cur;
  63. while (tmp->_right)
  64. {
  65. tmp_pre = tmp;
  66. tmp = tmp->_right;
  67. }
  68. //节点互换
  69. cur->_key = tmp->_key;
  70.  
  71. if (tmp == tmp_pre->_left)
  72. {
  73. tmp_pre->_left = tmp->_left;
  74. }
  75. else
  76. {
  77. tmp_pre->_right = tmp->_left;
  78. }
  79.  
  80. delete tmp;
  81. }
  82. return true;
  83. }
  84. }
  85. return false;
  86. }

接下来是递归版本

  1. bool eraseR(const T &val, Node *&root)
  2. {
  3. //找不到返回false
  4. if (root == nullptr)
  5. {
  6. return false;
  7. }
  8. //寻找插入位置
  9. if (root->_key < val)
  10. {
  11. return eraseR(val, root->_right);
  12. }
  13. else if (root->_key > val)
  14. {
  15. return eraseR(val, root->_left);
  16. }
  17. else
  18. {
  19. if (root->_left == nullptr)//左子树为空
  20. {
  21. root = root->_right;
  22. }
  23. else if (root->_right == nullptr)//右子树为空
  24. {
  25. root = root->_left;
  26. }
  27. else //左右子树都不为空
  28. {
  29. Node *cur = root->_left;
  30. while (cur->_right)
  31. {
  32. cur = cur->_right;
  33. }
  34. swap(cur->_key, root->_key);
  35. return eraseR(val, root->_left);
  36. }
  37. return true;
  38. }
  39. }

总结

大家觉得二叉搜索树的时间复杂度是多少呢?O(logn)吗?不,它的时间复杂度还是O(n),当插入数据是有序的,二叉搜索树会发生退化,变成一个单支树。比如下面这张图:

为了解决这个问题,有人对二叉搜索树进行了一些优化,如:AVL树和红黑树,之后我也会带着大家来实现一个完整的AVL树和红黑树

到此这篇关于C++数据结构之二叉搜索树的实现详解的文章就介绍到这了,更多相关C++二叉搜索树内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号