当前位置:   article > 正文

数据结构:二叉搜索树(简单C++代码实现)

数据结构:二叉搜索树(简单C++代码实现)

目录

前言

1. 二叉搜索树的概念

2. 二叉搜索树的实现

2.1 二叉树的结构

2.2 二叉树查找

2.3 二叉树的插入和中序遍历

2.4 二叉树的删除

3. 二叉搜索树的应用

3.1 KV模型实现

3.2 应用

4. 二叉搜索树分析

总结


前言

本文将深入探讨二叉搜索树这一重要的数据结构。二叉搜索树不仅是一个功能强大的数据结构,而且在实际应用中展现出了极高的实用性。它以其独特的组织方式,使得查找、插入和删除操作都能在平均对数到线性时间内完成,从而大大提高了数据处理的效率。为了更好地理解二叉搜索树的工作原理,我们使用C++语言实现了一个简单的二叉搜索树。


1. 二叉搜索树的概念

二叉搜索树(Binary Search Tree),也称二叉排序树,是一种特殊的二叉树。二叉搜索树可以为空树。如果不为空树,有以下的性质:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 左子树和右子树也都是二叉搜索树。

2. 二叉搜索树的实现

2.1 二叉树的结构

先自定义一个二叉树结点的类,该类将使用模版。一般来说,有两种类型的二叉搜索树。

  • K模型:K模型即只有key作为关键码,自定义结点类型中只需要存储Key即可。
  • KV模型:每一个关键码key,都有与之对应的值Value,即<Key,Value>键值对。

下面的代码是两种模型自定义类的实现,把下面的代码放到BSTree.h文件下。分别封装到key和keyValue的命名空间中。我们先实现K模型的二叉树成员函数。再如法炮制实现第二种模型的成员函数。

    1. #pragma once
    2. #include <iostream>
    3. using namespace std;
    4. namespace key
    5. {
    6. template<class K>
    7. struct BSTNode
    8. {
    9. BSTNode(const K& key = K())
    10. :_key(key)
    11. , _left(nullptr)
    12. , _right(nullptr)
    13. {}
    14. K _key;
    15. BSTNode<K>* _left;
    16. BSTNode<K>* _right;
    17. };
    18. template<class K>
    19. class BSTree
    20. {
    21. typedef BSTNode<K> Node;
    22. //...
    23. private:
    24. Node* root;
    25. }
    26. }
    27. namespace keyValue
    28. {
    29. template<class K, class V>
    30. struct BSTNode
    31. {
    32. BSTNode(const K& key = K(), const V& value = V())
    33. :_key(key)
    34. ,_value(value)
    35. , _left(nullptr)
    36. , _right(nullptr)
    37. {}
    38. K _key;
    39. V _value;
    40. BSTNode<K, V>* _left;
    41. BSTNode<K, V>* _right;
    42. };
    43. template<class K, class V>
    44. class BSTree
    45. {
    46. typedef BSTNode<K, V> Node;
    47. //...
    48. private:
    49. Node* root;
    50. }
    51. }

2.2 二叉树查找

二叉搜索树的查找操作比较简单。

  • 函数的返回值是个布尔值。查找成功返回true,失败返回false。
  • 从根开始比较关键码,进行查找。如果关键码比根的大往右边查找,比根的小就往左边查找。
  • 最多会查找这颗二叉树的高度次,如果走到空,说明这个值不存在。
  1. bool Find(const K& key)
  2. {
  3. Node* cur = _root;
  4. //如果为空,说明这个值不存在
  5. while (cur)
  6. {
  7. //比较关键值大小
  8. if (cur->_key < key)
  9. {
  10. cur = cur->right;
  11. }
  12. else if (cur->_key > key)
  13. {
  14. cur = cur->left;
  15. }
  16. else
  17. {
  18. return true;
  19. }
  20. }
  21. return false;
  22. }

2.3 二叉树的插入和中序遍历

int arr[] = {11, 7, 18, 9, 14, 4, 23, 8, 16, 10};

二叉树的插入操作其实步骤根查找类似,插入具体过程如下:

  • 函数的返回值也是布尔值。插入成功返回true,插入失败返回false。
  • 如果树为空,直接使用new创建新结点,赋值给root指针。
  • 如果树不为空,使用while循环查找新结点的位置,如果找到某个节点存储的值,与插入的值相同,就不需要插入,返回false。此外,还要新定义一个二叉树结点类值,此值用来存储当前结点的父亲结点。因为需要判断新增结点是他的父亲结点左节点还是右节点。
  1. bool Insert(const K& key)
  2. {
  3. if (_root == nullptr)
  4. {
  5. _root = new Node(key);
  6. }
  7. Node* parent = nullptr;
  8. Node* cur = _root;
  9. while (cur)
  10. {
  11. if (cur->_key < key)
  12. {
  13. parent = cur;
  14. cur = cur->_right;
  15. }
  16. else if (cur->_key > key)
  17. {
  18. parent = cur;
  19. cur = cur->_left;
  20. }
  21. else
  22. {
  23. return false;
  24. }
  25. }
  26. cur = new Node(key);
  27. if (parent->_key < key)
  28. {
  29. parent->_right = cur;
  30. }
  31. else
  32. {
  33. parent->_left = cur;
  34. }
  35. return true;
  36. }

二叉搜索树可以通过中序遍历得到有序的数据。在类中,定义一个中序遍历的子函数,再传入这棵树的根,进行遍历打印即可。

  1. class BSTree
  2. {
  3. //...
  4. public:
  5. //...
  6. void InOrder()
  7. {
  8. _InOrder(_root);
  9. cout << endl;
  10. }
  11. private:
  12. void _InOrder(Node* root)
  13. {
  14. if (root == nullptr)
  15. return;
  16. _InOrder(root->_left);
  17. cout << root->_key << " ";
  18. _InOrder(root->_right);
  19. }
  20. private:
  21. Node* _root = nullptr;
  22. };

我们写一个测试函数,测试一下插入函数。

  1. #include "BSTree.h"
  2. void Test1()
  3. {
  4. int arr[] = { 11, 7, 18, 9, 14, 4, 23, 8, 16, 10 };
  5. key::BSTree<int> tree;
  6. for (auto& e : arr)
  7. {
  8. tree.Insert(e);
  9. }
  10. tree.InOrder();
  11. tree.Insert(2);
  12. tree.Insert(13);
  13. tree.InOrder();
  14. }

运行结果如下:

 

2.4 二叉树的删除

二叉树的删除操作就有些麻烦。需要分几种情况:

  • 待删除结点没有孩子结点。
  • 待删除结点有一个孩子结点。
  • 待删除结点有左右孩子结点。

待删除结点没有孩子结点,可以直接释放该节点,使其父亲结点指向空即可。

待删除结点只有一个孩子结点。如下图,14结点有一个右孩子,左孩子为空。先释放14结点,再将其父亲结点的左指针指向16结点即可。如果待删除结点有一个左孩子,操作也是类似。

 不过这个有极端情况,如下面的第二张图,如果要删除的是根节点,并且根节点只有左孩子或者右孩子。此时,因为根结点没有父亲结点,所以直接释放根结点,让root指针指向他的孩子结点。

 

待删除结点有两个孩子结点的情况,就比较麻烦。如下图,思路是找到可以替换的结点,待删除结点的key值替换成该节点的key值,然后再释放这个替换结点,处理结点之间的连接关系。

  • 第一步需要查找待删除结点,如果没有找到,返回false。找到待删除结点,进行删除操作。
  • 待删除结点的左子树中的最右结点,是左子树中最大的结点,待删除结点的右子树中的最左结点是右子树中最小的结点。我们使用右子树中最左结点来替换待删除结点。
  • 我们先定义一个rightMin结点指针变量,用来找右子树中最小的结点,定义一个rightMinP来记录rightMin指向结点的父亲结点。
  • 其中rightMin一开始指向待删除结点的右孩子。rightMinP需要指向待删除节点,看第二张图片,如果右子树的最小结点就是待删除结点的右孩子,rightMInP不指向待删除节点,而是指向空,那么我们使用rightMinP这个空指针进行操作会有问题。

  1. bool Erase(const K& key)
  2. {
  3. Node* parent = nullptr;
  4. Node* cur = _root;
  5. while (cur)
  6. { //查找待删除结点
  7. if (cur->_key < key)
  8. {
  9. parent = cur;
  10. cur = cur->_right;
  11. }
  12. else if (cur->_key > key)
  13. {
  14. parent = cur;
  15. cur = cur->_left;
  16. }
  17. else//删除操作
  18. {
  19. // 0-1孩子
  20. if (cur->_left == nullptr)
  21. {
  22. if (parent == nullptr)//删除根节点的情况
  23. {
  24. _root = cur->_right;
  25. }
  26. else
  27. {
  28. if (parent->_left == cur)
  29. parent->_left = cur->_right;
  30. else
  31. parent->_right = cur->_right;
  32. }
  33. delete cur;
  34. return true;
  35. }
  36. else if (cur->_right == nullptr)
  37. {
  38. if (parent == nullptr)//删除根节点的情况
  39. {
  40. _root = cur->_left;
  41. }
  42. else
  43. {
  44. if (parent->_left == cur)
  45. parent->_left = cur->_left;
  46. else
  47. parent->_right = cur->_left;
  48. }
  49. delete cur;
  50. return true;
  51. }
  52. else
  53. {
  54. //两个孩子的情况
  55. // 右子树的最小节点作为替代节点
  56. Node* rightMinP = cur;//不可为空,特殊情况会访问空指针
  57. Node* rightMin = cur->_right;
  58. while (rightMin->_left)
  59. {
  60. rightMinP = rightMin;
  61. rightMin = rightMin->_left;
  62. }
  63. cur->_key = rightMin->_key;
  64. //需要判断右子树最小节点是父亲结点的左孩子还是右孩子
  65. if (rightMinP->_left == rightMin)
  66. rightMinP->_left = rightMin->_right;
  67. else
  68. rightMinP->_right = rightMin->_right;
  69. delete rightMin;
  70. return true;
  71. }
  72. }
  73. }
  74. return false;
  75. }

我们写一个测试函数,测试一些删除函数的功能:

  1. void Test2()
  2. {
  3. int arr[] = { 11, 7, 18, 9, 14, 4, 23, 8, 16, 10 };
  4. key::BSTree<int> tree;
  5. for (auto& e : arr)
  6. {
  7. tree.Insert(e);
  8. }
  9. tree.InOrder();
  10. for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
  11. {
  12. printf("第%-2d次:", i + 1);
  13. tree.Erase(arr[i]);
  14. tree.InOrder();
  15. }
  16. }

运行结果如下:

3. 二叉搜索树的应用

3.1 KV模型实现

上文有提到二叉搜索树有两种模型,其中在现实生活中比较常用的是KV模型。每个关键码key,都有对应的的值Value,即<Key,Value>键值对

下面是KV模型的代码实现:

  1. namespace keyValue
  2. {
  3. template<class K, class V>
  4. struct BSTNode
  5. {
  6. BSTNode(const K& key = K(), const V& value = V())
  7. :_key(key)
  8. ,_value(value)
  9. , _left(nullptr)
  10. , _right(nullptr)
  11. {}
  12. K _key;
  13. V _value;
  14. BSTNode<K, V>* _left;
  15. BSTNode<K, V>* _right;
  16. };
  17. template<class K, class V>
  18. class BSTree
  19. {
  20. typedef BSTNode<K, V> Node;
  21. public:
  22. BSTree() = default;
  23. BSTree(const BSTree<K, V>& t)
  24. {
  25. _root = Copy(t._root);
  26. }
  27. ~BSTree()
  28. {
  29. Destroy(_root);
  30. _root = nullptr;
  31. }
  32. bool Insert(const K& key, const V& value)
  33. {
  34. if (_root == nullptr)
  35. {
  36. _root = new Node(key, value);
  37. }
  38. Node* parent = nullptr;
  39. Node* cur = _root;
  40. while (cur)
  41. {
  42. if (cur->_key < key)
  43. {
  44. parent = cur;
  45. cur = cur->_right;
  46. }
  47. else if (cur->_key > key)
  48. {
  49. parent = cur;
  50. cur = cur->_left;
  51. }
  52. else
  53. {
  54. return false;
  55. }
  56. }
  57. cur = new Node(key, value);
  58. if (parent->_key < key)
  59. {
  60. parent->_right = cur;
  61. }
  62. else
  63. {
  64. parent->_left = cur;
  65. }
  66. return true;
  67. }
  68. Node* Find(const K& key)
  69. {
  70. Node* cur = _root;
  71. while (cur)
  72. {
  73. if (cur->_key < key)
  74. {
  75. cur = cur->_right;
  76. }
  77. else if (cur->_key > key)
  78. {
  79. cur = cur->_left;
  80. }
  81. else
  82. {
  83. return cur;
  84. }
  85. }
  86. return nullptr;
  87. }
  88. bool Erase(const K& key)
  89. {
  90. Node* parent = nullptr;
  91. Node* cur = _root;
  92. while (cur)
  93. {
  94. if (cur->_key < key)
  95. {
  96. parent = cur;
  97. cur = cur->_right;
  98. }
  99. else if (cur->_key > key)
  100. {
  101. parent = cur;
  102. cur = cur->_left;
  103. }
  104. else
  105. {
  106. // 0-1孩子
  107. if (cur->_left == nullptr)
  108. {
  109. if (parent == nullptr)
  110. {
  111. _root = cur->_right;
  112. }
  113. else
  114. {
  115. if (parent->_left == cur)
  116. parent->_left = cur->_right;
  117. else
  118. parent->_right = cur->_right;
  119. }
  120. delete cur;
  121. return true;
  122. }
  123. else if (cur->_right == nullptr)
  124. {
  125. if (parent == nullptr)
  126. {
  127. _root = cur->_left;
  128. }
  129. else
  130. {
  131. if (parent->_left == cur)
  132. parent->_left = cur->_left;
  133. else
  134. parent->_right = cur->_left;
  135. }
  136. delete cur;
  137. return true;
  138. }
  139. else
  140. {
  141. // 两个孩子的情况
  142. // 右子树的最小节点作为替代节点
  143. Node* rightMinP = cur;
  144. Node* rightMin = cur->_right;
  145. while (rightMin->_left)
  146. {
  147. rightMinP = rightMin;
  148. rightMin = rightMin->_left;
  149. }
  150. cur->_key = rightMin->_key;
  151. cur->_value = rightMin->_value;
  152. if (rightMinP->_left == rightMin)
  153. rightMinP->_left = rightMin->_right;
  154. else
  155. rightMinP->_right = rightMin->_right;
  156. delete rightMin;
  157. return true;
  158. }
  159. }
  160. }
  161. return false;
  162. }
  163. void InOrder()
  164. {
  165. _InOrder(_root);
  166. cout << endl;
  167. }
  168. private:
  169. void _InOrder(Node* root)
  170. {
  171. if (root == nullptr)
  172. return;
  173. _InOrder(root->_left);
  174. cout << root->_key << ":" << root->_value << " ";
  175. _InOrder(root->_right);
  176. }
  177. void Destroy(Node* root)
  178. {
  179. if (root == nullptr)
  180. return;
  181. Destroy(root->_left);
  182. Destroy(root->_right);
  183. delete root;
  184. }
  185. Node* Copy(Node* root)
  186. {
  187. if (root == nullptr)
  188. return nullptr;
  189. Node* newRoot = new Node(root->_key, root->_value);
  190. newRoot->_left = Copy(root->_left);
  191. newRoot->_right = Copy(root->_right);
  192. }
  193. private:
  194. Node* _root = nullptr;
  195. };
  196. };

3.2 应用

二叉搜索树KV模型的应用有许多

  • 最经典的就是双语词典,英汉词典中,每个英文都有对应的中文,构成<word, chinese>键值对。
  • 中国公民每个人都有对象的身份证号码,即<中国人,身份证号码>键值对。
  • 还可以用来统计词语出现的次数,词语和其出现的次数就构成<word,count>键值对。

  1. void TestBsTree3()
  2. {
  3. keyValue::BSTree<string, string> dict;
  4. dict.Insert("left", "左边");
  5. dict.Insert("right", "右边");
  6. dict.Insert("apple", "苹果");
  7. dict.Insert("sort", "排序");
  8. dict.Insert("love", "爱");
  9. string str;
  10. while (cin >> str)
  11. {
  12. auto ret = dict.Find(str);
  13. if (ret)
  14. {
  15. cout << "->" << ret->_value << endl;
  16. }
  17. else
  18. {
  19. cout << "重新输入" << endl;
  20. }
  21. }
  22. }

运行结果如下:

这是统计词语出现次数

  1. void TestBSTree4()
  2. {
  3. // 统计物品出现的次数
  4. string arr[] = { "书本", "笔", "书本", "笔", "书本", "书本", "笔", "书本", "橡皮", "书本", "橡皮" };
  5. keyValue::BSTree<string, int> countTree;
  6. for (const auto& str : arr)
  7. {
  8. // 先查找物品在不在搜索树中
  9. // 1、不在,说明物品第一次出现,则插入<物品, 1>
  10. // 2、在,则查找到的节点中水果对应的次数++
  11. auto ret = countTree.Find(str);
  12. if (ret == NULL)
  13. countTree.Insert(str, 1);
  14. else
  15. ret->_value++;
  16. }
  17. countTree.InOrder();
  18. }

运行结果如下:

4. 二叉搜索树分析

二叉搜索树的插入和删除操作,都需要先进行查找。查找操作一般最多查找这颗树的高度次,如果二叉搜索树是一个满二叉树或者完全二叉树,效率很高。可当二叉树退化成下图中右边这颗二叉树,基本上像链表的形态,那么查找的速度比原来慢了很多。

  • 最好的情况,二叉搜索树是接近一颗满二叉树,查找的时间复杂度是O(logN)。
  • 最坏的情况,二叉搜索树退化成只有单链,像链表的形态,查找的时间复杂度是O(N)。

因此,在二叉搜索树的基础上,又出现了平衡二叉搜索树,如AVL树和红黑树,会调整二叉树成为接近满二叉树的形态。


总结

通过本文的阐述,相信你应该对二叉搜索树的基本概念、特性以及操作方法已经有了一定的了解。不过想要掌握这个数据结构,还需要亲自上手编写一个二叉搜索树的代码。通过编码实践,才能更深刻体会到内部的工作机制。

创作不易,希望这篇文章能给你带来启发和帮助,如果喜欢这篇文章,请留下你的三连,你的支持的我最大的动力!!!

ee192b61bd234c87be9d198fb540140e.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小桥流水78/article/detail/882400
推荐阅读
相关标签
  

闽ICP备14008679号