• 数据结构之二叉搜索树


    1.二叉搜索树

    1.1 二叉搜索树概念

    二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
    ·若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
    ·若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
    ·它的左右子树也分别为二叉搜索树

     通过观察可以发现:搜索二叉树走一遍中序遍历可以排成升序.

    1.2 二叉搜索树操作

    1. 二叉搜索树的查找

    2. 二叉搜索树的插入

     插入的具体过程如下:
    a. 树为空,则直接插入

    b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

    3. 二叉搜索树的删除

    删除节点的原则是:必须保持搜索二叉树的特性.

    1.3 二叉搜索树的实现

    1. namespace K
    2. {
    3. template<class K>
    4. struct BSTreeNode//定义节点的结构体
    5. {
    6. BSTreeNode<K>* _left;//左孩子指针
    7. BSTreeNode<K>* _right;//右孩子指针
    8. K _key;//存的值
    9. BSTreeNode(const K& key)//节点的构造函数
    10. :_left(nullptr)
    11. ,_right(nullptr)
    12. ,_key(key)
    13. {}
    14. };
    15. template<class K>
    16. class BSTree
    17. {
    18. typedef BSTreeNode<K> Node;
    19. public:
    20. BSTree()//构造函数
    21. :_root(nullptr)
    22. {}
    23. BSTree(const BSTree<K>& t)
    24. {
    25. _root = Copy(t._root);//调用递归copy去拷贝构造
    26. }
    27. BSTree<K>& operator=(BSTree<K> t)//利用现代写法进行赋值拷贝
    28. {
    29. swap(_root, t._root);
    30. return *this;
    31. }
    32. ~BSTree()
    33. {
    34. Destory(_root);//调用Destory去析构
    35. _root = nullptr;//将root置空
    36. }
    37. bool Insert(const K& key)
    38. {
    39. if (_root == nullptr)//如果刚开始树为空树,那么直接给一个节点给root
    40. {
    41. _root = new Node(key);
    42. return true;
    43. }
    44. Node* cur = _root;//利用cur去循环迭代找到空位
    45. Node* parent = nullptr;//利用parent去记录cur的父亲节点,并且要注意这个刚开始给成nullptr
    46. while (cur)//利用循环去找适合key的空位,找到为空的位置就结束
    47. {
    48. if (key > cur->_key)//大于就去右子树
    49. {
    50. parent = cur;//进入语句先把父节点赋值
    51. cur = cur->_right;//然后再把cur迭代
    52. }
    53. else if (key < cur->_key)//小于就去左子树
    54. {
    55. parent = cur;
    56. cur = cur->_left;
    57. }
    58. else
    59. {
    60. return false;//如果找到与key值相等的节点那么就删除
    61. }
    62. }//出了while循环说明找到了合适的空位,此时可以开始插入新节点
    63. cur = new Node(key);//给cur申请一个节点
    64. if (key > parent->_key)//这里的if语句判断一定要注意,是用值判断而不是指针
    65. parent->_right = cur;
    66. else if (key < parent->_key)
    67. parent->_left = cur;
    68. return true;//插入成功返回
    69. }
    70. Node* Find(const K& key)
    71. {
    72. Node* cur = _root;
    73. while (cur)//利用循环去找
    74. {
    75. if (key > cur->_key)
    76. {
    77. cur = cur->_right;
    78. }
    79. else if (key < cur->_key)
    80. {
    81. cur = cur->_left;
    82. }
    83. else
    84. {
    85. return cur;
    86. }
    87. }
    88. return false;//出了循环还未找到就返回false
    89. }
    90. bool Erase(const K& key)
    91. {
    92. Node* cur = _root;
    93. Node* parent = nullptr;
    94. while (cur)
    95. {
    96. if (key > cur->_key)
    97. {
    98. parent = cur;
    99. cur = cur->_right;
    100. }
    101. else if (key < cur->_key)
    102. {
    103. parent = cur;
    104. cur = cur->_left;
    105. }//上面两段if语句都是去定位key的位置
    106. else//进入else语句则找到了需要删除的节点
    107. {
    108. if (cur->_left == nullptr)//如果需要删除的节点的左节点为空,即没有左孩子
    109. {
    110. if (cur == _root)//注:这种情况非常容易被忽略
    111. {
    112. _root = cur->_right;//如果cur就为根节点的话,也就是右单支的情况,那么把root赋值为cur的右节点
    113. }
    114. else//cur不为根节点的情况
    115. {
    116. if (cur == parent->_left)//需要判断cur为父亲的左孩子还是右孩子
    117. {
    118. parent->_left = cur->_right;//需要将cur的右孩子托孤给父亲
    119. }
    120. else if (cur == parent->_right)
    121. {
    122. parent->_right = cur->_right;
    123. }
    124. }
    125. delete cur;//最后删除cur节点
    126. }
    127. else if (cur->_right == nullptr)//如果需要删除的节点的右节点为空,即没有右孩子
    128. {
    129. if (cur == _root)//需要特别注意
    130. {
    131. _root = cur->_left;//如果cur就为根节点的话,也就是左单支的情况,那么把root赋值为cur的左节点
    132. }
    133. else//cur不为根节点的情况
    134. {
    135. if (cur == parent->_left)//需要判定的是cur节点为父节点的左孩子还是右孩子
    136. {
    137. parent->_left = cur->_left;//将cur节点的左孩子托孤给父亲
    138. }
    139. else if (cur == parent->_right)
    140. {
    141. parent->_right = cur->_left;
    142. }
    143. }
    144. delete cur;//最后删除cur节点
    145. }
    146. else
    147. {
    148. //这种情况最复杂:也就是需要删除的节点左孩子和右孩子都存在的情况
    149. //此时我们采用替代法删除
    150. Node* minRight = cur->_right;//去被删除的节点的右子树找最小节点,左子树找最大节点道理也是一样的
    151. Node* minParent = cur;//注意:这里一定不能给空节点,必须给cur节点
    152. //至于为什么不能给空节点,万一下面的while循环进不去,后面的if语句就会造成非法访问
    153. while (minRight->_left)//注意:括号里面是左节点不为空时为循环结束的条件,因为我们的目的是去找到最左节点,最左节点已经走到空了,就不要再进入循环了
    154. {
    155. minParent = minRight;
    156. minRight = minRight->_left;
    157. }
    158. cur->_key = minRight->_key;//将cur的值替换成右子树的最小节点值
    159. //走到这里就很容易知道最小节点的左子树一定为空了,但是右子树呢?
    160. //右子树如果不为空我们就需要进行托孤,这种托孤方式也就演变成了上面两种简单的方式
    161. //但是这一部很容易被遗忘:大家都会觉得minRight一定会是minParent的左孩子
    162. //这是错误的理解,这里一定要去判断minRight为minParent右孩子的情况,这里画图理解
    163. if (minRight == minParent->_left)//如果为父节点的左孩子,那么就把我的右孩子交给父节点的左孩子
    164. minParent->_left = minRight->_right;
    165. else if (minRight == minParent->_right)//如果为父节点的右孩子,那么就把我的右孩子交给父节点的右孩子
    166. minParent->_right = minRight->_right;
    167. delete minRight;
    168. }
    169. return true;//删除完节点过后返回true
    170. }
    171. }
    172. return false;//出了while循环还没有找到需要删除的节点返回false
    173. }
    174. bool InsertR(const K& key)//递归插入key
    175. {
    176. return _InsertR(_root, key);
    177. }
    178. Node* FindR(const K& key)//递归查找key
    179. {
    180. return _FindR(_root, key);
    181. }
    182. bool EraseR(const K& key)//递归删除key
    183. {
    184. return _EraseR(_root, key);
    185. }
    186. void Inorder()//中序遍历
    187. {
    188. _Inorder(_root);//把中序遍历的递归封装一层,传root指针,保护root不被改变
    189. cout << endl;
    190. }
    191. private:
    192. void Destory(Node* root)
    193. {
    194. if (root == nullptr)
    195. return;
    196. Destory(root->_left);
    197. Destory(root->_right);
    198. delete root;//利用后序遍历递归删除节点
    199. }
    200. Node* Copy(Node* root)
    201. {
    202. if (root == nullptr)
    203. return nullptr;
    204. Node* newroot = new Node(root->_key);//建立新节点
    205. newroot->_left = Copy(root->_left);//新节点的左右节点再去转换成子问题
    206. newroot->_right = Copy(root->_right);
    207. return newroot;//最后返回新节点
    208. }
    209. bool _InsertR(Node*& root, const K& key)//注意这里要用引用传参
    210. {
    211. if (root == nullptr)//root为空时new一个节点
    212. {
    213. root = new Node(key);
    214. return true;
    215. }
    216. if (key > root->_key)
    217. {
    218. return _InsertR(root->_right, key);
    219. }
    220. else if (key < root->_key)
    221. {
    222. return _InsertR(root->_left, key);
    223. }
    224. else
    225. {
    226. return false;
    227. }
    228. }
    229. Node* _FindR(Node* root, const K& key)
    230. {
    231. if (root == nullptr)
    232. {
    233. return nullptr;
    234. }
    235. if (key > root->_key)
    236. {
    237. return _FindR(root->_right, key);
    238. }
    239. else if (key < root->_key)
    240. {
    241. return _FindR(root->left, key);
    242. }
    243. else
    244. {
    245. return root;
    246. }
    247. }
    248. bool _EraseR(Node*& root, const K& key)//注意这里一定要使用引用传参
    249. {
    250. if (root == nullptr)//如果root为空则返回false
    251. {
    252. return false;
    253. }
    254. if (key > root->_key)
    255. {
    256. return _EraseR(root->_right, key);
    257. }
    258. else if (key < root->_key)
    259. {
    260. return _EraseR(root->_left, key);
    261. }
    262. else
    263. {
    264. //找到了该节点
    265. if (root->_left == nullptr)//左孩子为空,右孩子存在的情况
    266. {
    267. Node* del = root;//用del记录一下这个带删除的节点
    268. root = root->_right;//然后将root的右节点赋值给root
    269. //上面这一步非常巧,左边的root实际是上一层父节点的左孩子或右孩子的引用
    270. //这里就体现出了引用传参的作用
    271. delete del;//链接关系处理完过后,最后删除这个root节点
    272. }
    273. else if (root->_right == nullptr)
    274. {
    275. Node* del = root;
    276. root = root->_left;
    277. delete del;
    278. }
    279. else
    280. {
    281. Node* minRight = root->_right;
    282. while (minRight->_left)//递归去找root的右子树中的最左节点,也就是最小节点
    283. {
    284. minRight = minRight->_left;
    285. }
    286. K min = minRight->_key;//用变量min存一下这个节点
    287. //转换成在root的右子树删除min
    288. _EraseR(root->_right, min);//这里转换成子问题去删除min
    289. root->_key = min;//再将root的值替换成min
    290. }
    291. return true;
    292. }
    293. }
    294. void _Inorder(Node* root)
    295. {
    296. if (root == nullptr)
    297. return;
    298. _Inorder(root->_left);
    299. cout << root->_key << " ";
    300. _Inorder(root->_right);
    301. }
    302. Node* _root;
    303. };
    304. }

    二叉树实现的代码测试:

    1. void TestBSTree1()
    2. {
    3. K::BSTree<int> t;
    4. int a[] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };
    5. for (auto e : a)
    6. {
    7. t.InsertR(e);
    8. }
    9. t.Inorder();
    10. t.EraseR(5);
    11. t.Inorder();
    12. t.EraseR(8);
    13. t.Inorder();
    14. t.EraseR(7);
    15. t.Inorder();
    16. t.EraseR(5);
    17. t.Inorder();
    18. // 测试时,把树中的节点删除干净,确保没问题
    19. for (auto e : a)
    20. {
    21. t.EraseR(e);
    22. t.Inorder();
    23. }
    24. t.Inorder();
    25. }
    26. void TestBSTree2()
    27. {
    28. K::BSTree<int> t;
    29. int a[] = { 5, 3, 4, 1, 7, 8, 2, 6, 0, 9 };
    30. for (auto e : a)
    31. {
    32. t.InsertR(e);
    33. }
    34. t.Inorder();
    35. K::BSTree<int> copy = t;
    36. copy.Inorder();
    37. }

    1.4 二叉搜索树的应用

    1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。核心是想要体现在不在的问题。
    比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
    ·以单词集合中的每个单词作为key,构建一棵二叉搜索树
    ·在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
    2. KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。该种方式在现实生活中非常常见。核心是想要通过一个值去找另一个值。

    比如:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英
    文单词与其对应的中文就构成一种键值对;再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是就构成一种键值对。
    比如:实现一个简单的英汉词典dict,可以通过英文找到与其对应的中文,具体实现方式如下:

    ·<单词,中文含义>为键值对构造二叉搜索树,注意:二叉搜索树需要比较,键值对比较时只比较Key

    ·查询英文单词时,只需给出英文单词,就可快速找到与其对应的key

    KV模型的代码:

    1. namespace KV
    2. {
    3. template<class K, class V>
    4. struct BSTreeNode
    5. {
    6. BSTreeNode<K, V>* _left;
    7. BSTreeNode<K, V>* _right;
    8. K _key;
    9. V _value;
    10. BSTreeNode(const K& key, const V& value)
    11. : _left(nullptr)
    12. , _right(nullptr)
    13. , _key(key)
    14. , _value(value)
    15. {}
    16. };
    17. template<class K, class V>
    18. class BSTree
    19. {
    20. typedef BSTreeNode<K, V> Node;
    21. private:
    22. // 如果树中已经存在key,返回false
    23. bool _InsertR(Node*& root, const K& key, const V& value)
    24. {
    25. if (root == NULL) // 插入
    26. {
    27. root = new Node(key, value);
    28. return true;
    29. }
    30. if (root->_key < key)
    31. {
    32. return _InsertR(root->_right, key, value);
    33. }
    34. else if (root->_key > key)
    35. {
    36. return _InsertR(root->_left, key, value);
    37. }
    38. else
    39. {
    40. return false;
    41. }
    42. }
    43. Node* _FindR(Node* root, const K& key)
    44. {
    45. if (root == nullptr)
    46. {
    47. return nullptr;
    48. }
    49. if (root->_key < key)
    50. {
    51. return _FindR(root->_right, key);
    52. }
    53. else if (root->_key > key)
    54. {
    55. return _FindR(root->_left, key);
    56. }
    57. else
    58. {
    59. return root;
    60. }
    61. }
    62. // 如果树中不存在key,返回false
    63. // 存在,删除后,返回true
    64. bool _EraseR(Node*& root, const K& key)
    65. {
    66. if (root == NULL)
    67. {
    68. return false;
    69. }
    70. if (root->_key < key)
    71. {
    72. return _EraseR(root->_right, key);
    73. }
    74. else if (root->_key > key)
    75. {
    76. return _EraseR(root->_left, key);
    77. }
    78. else
    79. {
    80. // 找到了,root就是要删除的节点
    81. if (root->_left == nullptr)
    82. {
    83. Node* del = root;
    84. root = root->_right;
    85. delete del;
    86. }
    87. else if (root->_right == nullptr)
    88. {
    89. Node* del = root;
    90. root = root->_left;
    91. delete del;
    92. }
    93. else
    94. {
    95. Node* minRight = root->_right;
    96. while (minRight->_left)
    97. {
    98. minRight = minRight->_left;
    99. }
    100. K kmin = minRight->_key;
    101. V vMin = minRight->_value;
    102. // 转换成在root的右子树删除min
    103. _EraseR(root->_right, kmin);
    104. root->_key = kmin;
    105. root->_value = vMin;
    106. }
    107. return true;
    108. }
    109. }
    110. void _Destory(Node* root)
    111. {
    112. if (root == NULL)
    113. {
    114. return;
    115. }
    116. _Destory(root->_left);
    117. _Destory(root->_right);
    118. delete root;
    119. }
    120. Node* _Copy(Node* root)
    121. {
    122. if (root == nullptr)
    123. {
    124. return nullptr;
    125. }
    126. Node* copyNode = new Node(root->_key, root->_value);
    127. copyNode->_left = _Copy(root->_left);
    128. copyNode->_right = _Copy(root->_right);
    129. return copyNode;
    130. }
    131. public:
    132. BSTree()
    133. :_root(nullptr)
    134. {}
    135. BSTree(const BSTree<K, V>& t)
    136. {
    137. _root = _Copy(t._root);
    138. }
    139. // t1 = t2
    140. BSTree<K, V>& operator=(BSTree<K, V> t)
    141. {
    142. swap(_root, t._root);
    143. return *this;
    144. }
    145. ~BSTree()
    146. {
    147. _Destory(_root);
    148. _root = nullptr;
    149. }
    150. bool InsertR(const K& key, const V& value)
    151. {
    152. return _InsertR(_root, key, value);
    153. }
    154. Node* FindR(const K& key)
    155. {
    156. return _FindR(_root, key);
    157. }
    158. bool EraseR(const K& key)
    159. {
    160. return _EraseR(_root, key);
    161. }
    162. void _InOrder(Node* root)
    163. {
    164. if (root == nullptr)
    165. return;
    166. _InOrder(root->_left);
    167. cout << root->_key << ":" << root->_value << endl;
    168. _InOrder(root->_right);
    169. }
    170. void InOrder()
    171. {
    172. _InOrder(_root);
    173. cout << endl;
    174. }
    175. private:
    176. Node* _root;
    177. };
    178. }

    KV模型代码测试:
     

    1. void TestBSTree3()
    2. {
    3. KV::BSTree<string, string> dict;
    4. dict.InsertR("string", "字符串");
    5. dict.InsertR("tree", "树");
    6. dict.InsertR("left", "左边、剩余");
    7. dict.InsertR("right", "右边");
    8. dict.InsertR("sort", "排序");
    9. // ...插入词库中所有单词
    10. string str;
    11. while (cin >> str)
    12. {
    13. KV::BSTreeNode<string, string>* ret = dict.FindR(str);
    14. if (ret == nullptr)
    15. {
    16. cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
    17. }
    18. else
    19. {
    20. cout << str << "中文翻译:" << ret->_value << endl;
    21. }
    22. }
    23. }
    24. void TestBSTree4()
    25. {
    26. // 统计水果出现的次数
    27. string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
    28. KV::BSTree<string, int> countTree;
    29. for (const auto& str : arr)
    30. {
    31. // 先查找水果在不在搜索树中
    32. // 1、不在,说明水果第一次出现,则插入<水果, 1>
    33. //KV::BSTreeNode<string, int>* ret = countTree.FindR(str);
    34. auto ret = countTree.FindR(str);
    35. if (ret == NULL)
    36. {
    37. countTree.InsertR(str, 1);
    38. }
    39. else
    40. {
    41. ret->_value++;
    42. }
    43. }
    44. countTree.InOrder();
    45. }

    1.5 二叉搜索树的性能分析

    插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
    对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多
    但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树

    最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:log2N
    最差情况下,二叉搜索树退化为单支树,其平均比较次数为: N/2

    问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以是二叉搜索树的性能最佳?

    所以实际中搜索二叉树,极端情况下没有办法保证效率,所以后面会对搜索二叉树进一步扩展延申:AVLTree,红黑树.他们对搜索二叉树,左右高度提出了要求,非常接近完全二叉树,他们的效率可以达到O(logN)

  • 相关阅读:
    为什么2023年一定要用OKR?
    MySQL常见知识点(面试题)总结
    关于爬虫发展历史,价值,问题和应对恶意爬虫的策略
    Apache Doris 在小鹅通的应用实践
    ACM模式各种输入整理(C++)
    JMU 数科 数据库与数据仓库期末总结(2)选择题
    学生HTML个人网页作业作品 简单的IT技术个人简历模板html下载 简单个人网页设计作业 静态HTML个人博客主页
    【排序算法】选择排序(C语言)
    Fluorescein-PEG-DSPE 磷脂-聚乙二醇-荧光素荧光磷脂PEG衍生物
    uniapp解决scroll滑动之后被u-sticky挡住的问题
  • 原文地址:https://blog.csdn.net/weixin_56054625/article/details/126904922