• Avl树(有详细图解)


    目录

    介绍

    引入

    概念 

    特点 

    模拟实现

    思路

    插入

    旋转 

    左旋

    无子树

    有子树

    右旋

    无子树

    有子树

    左右旋

    引入(也就是有子树版本的抽象图解)

    解决方法(也就是左右旋) 

    总结

    无子树(也就是curright的位置就是newnode)

    有子树 

    模型高度解释

    旋转 

    更新三个节点的bf

    右左旋

    无子树

    有子树

    旋转

    更新三个结点的bf

    注意点

    代码


    介绍

    引入

    map和set的底层都是按照二叉搜索树来实现的

    • 但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N)
    • 因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现

    概念 

    • AVL树是一种自平衡二叉搜索树,其名称源自其发明者Adelson-Velsky和Landis
    • AVL树通过确保各个节点之间的高度差不超过1,来保证在最坏情况下,树的高度仍为O(log n) (其中n是树中节点的数量),这样可以保持最好的效率

    特点 

    • 自平衡:在插入或删除节点时,AVL树自动执行旋转操作来保持平衡。这些旋转操作包括左旋、右旋、左右旋和右左旋等,通过这些旋转可以调整节点的平衡因子,使其满足平衡条件(不超过1)

    • 二叉搜索树性质:AVL树仍然是二叉搜索树,即左子树的所有节点的值都小于根节点的值,右子树的所有节点的值都大于根节点的值

    模拟实现

    思路

    插入

    • avl树中保持平衡的关键就在于平衡因子(bf)
    • 这里bf=右子树高度-左子树高度
    • 主要就是每次插入时,需要更新平衡因子 (但只需要沿着newnode的位置往上就行,因为它改变的只是newnode所在那一分支)

    • 只要当前结点bf=0,就可以不再更新了(它此时变为0,说明原来是1/-1)
    • 那么让1/-1变为0,这一分支的高度是不变的,那么以这个为子树的树,高度也不会变,平衡因子也就不变

    • 然后,如果结点bf=2/-2,就要开始旋转了
    • 而旋转分为4种,需要判断cur和cur父亲的bf,来区分使用哪种旋转方式 

    • 旋转后的子树,其根结点bf是0,那么他也是从在插入新结点前的1/-1变成了0,所以也就不需要往上更新了
    • 这样,插入步骤就结束了

    旋转 

    左旋
    无子树

    左旋实际上就是 -- 让p成为cur的左子树,cur的右边不受影响

    有子树

    • 虽然都有子树,但我们无法知道到底是多少个,到底是什么形态
    • (这些其实我们都不关心,我们只要知道一个相对大小就行,毕竟重点还是在p和cur上)
    • 有子树和无子树都是差不多的,只不过是多了子树,但是p和cur的bf仍然是2和1,只有这样才会触发左旋
    • 然后设cur左子树高度为h,那么可以推出curright高度就是h+1,p左树是h
    • 旋转后 -- cur的原左子树成为了p的右子树(因为本身curleft就处于p的右树范围),p左树不变,cur右树也不变
    • 只要记住 -- 左旋是p成为cur的左树,那么原左树就成为了p的右树
    • 旋转后的bf:(都为0)
    右旋

    和左旋非常像,只不过是换了个方向

    无子树

    旋转后,p成为cur的右子树

    有子树

    和左旋一个思路,最终得到了这么一副抽象图:

    然后就是旋转了 -- cur原右子树成为p的左子树(本身就是p左树范围内),p右子树不变

    计算bf后,两个结点的bf变成了0,其他的都不变

    两种旋转真的非常像,只要记住左旋是p成为cur的左子树,右旋是p成为cur的右子树,就行了

    左右旋
    引入(也就是有子树版本的抽象图解)

    上面有这样一种情况,它是需要被右旋的:

    但是如果这个新结点是在curright下呢???

    这种情况不能使用单独的右旋(因为单右旋没有用):

    会发现右旋后,这支子树的根结点的bf还是2(只是把原子树的左右颠倒了)

    解决方法(也就是左右旋) 

    然后,我们经过对比可以发现,实际上他和右旋之间就差一个拐拐 :

    所以可以考虑将那个拐角处掰正,也就是对应的将cur那支子树进行左旋:

    这样,就可以和原来的p拼接起来,达到右旋的条件

    最终经过右旋后,就可以完成我们的需求(这里只是抽象演示,并没有细画cur的情况,有子树中会有详细版)

    总结

    相当于对cur左旋是进行右旋的预热,最终其实还是经过对p右旋完成的 

    无子树(也就是curright的位置就是newnode)

    先对cur进行左旋,然后对p右旋:

    最终newnode(也就是curright成为了这一支子树的根结点)

    有子树 

    首先,在无子树情况中,我们会发现,最终cur的右结点成为了根,那么我们就需要额外画出这个结点

    其次,新结点放在curright的左/右子树都可以,只不过最后处理这三个重要结点的bf时,需要单另处理

    先抽象好模型(设curright右子树高度为h,从而可以推出其他高度)

    模型高度解释

    你可能会好奇,为什么curright的左右子树高度被写成是相等的? (至少我好奇了,然后我画了画,发现是有人家的理由的)

    • 这里还是设右子树高度为h
    • 如果原先左子树高度是h+1(高度差不能超过1嗷)
    • 那新加一个结点就会变成h+2,会让curright的bf变成-2,那么这里不会触发p的左右旋,而是curright的旋转(左旋或者左右旋,具体看新结点的位置)

    • 如果原先左子树高度是h-1(高度差不能超过1嗷)
    • 那新加一个结点就会变成h,会让curright的bf变成0,那bf的更新到curright就停止了,不会让p更新到2/-2,所以依然不可以

    那么就可以得到,curright的两个子树(如果有),那一定高度相等,才能触发p的左右旋

    旋转 

    继续接下来的步骤,抽象好模型后,对cur进行左旋

    不要忘记前面介绍的左旋了嗷(这里的cur成为curright的左子树,原左树成为cur的右子树)

    然后和p连接在一起(也就是修改p的指向和两个结点的_parent指向)

    接下来就是旋转的最后一步辣,对p进行右旋

    这里是将p作为curright的右子树,原右子树成为p的左树(都是之前的步骤)

    之后就是最重要的一步,对三个结点的bf重新赋值(虽然进行了旋转,但更新到的bf实际并不准确,因为右旋和左右旋用到的模型本身就不同)

    更新三个节点的bf

    这里的更新也得分情况

    像这里用到的图解,是新结点插入在curright的左树部分

    (下面是全部的旋转变换过程)

    • 那么由于要对cur左旋的缘故,curright中带有新结点的这一支给了cur的右树,那么cur右边和左边平衡了,cur的bf=0
    • 但p不一样,p的左树是被分配的curright的右树,其高度只有h(注意看图嗷看图),所以p的bf=1

    如果新结点插入在curright的右树部分

    • 道理和上面的一样
    • 因为curright的右树给了p的左边:
    • 所以p的bf=0(平衡了)
    • 而cur的右边是原curright的左边,高度是0,所以cur的bf=-1

    只要过程熟悉,知道是怎么变的,bf的更新也素手到擒来噜

    右左旋

    和左右旋非常像,只不过是换了个方向

    无子树

    同样是有拐,也是一样要把那个拐掰正,也就是对cur进行右旋(让cur成为curleft的右子树)

    最后就可以美美对p进行左旋噜

    然后curleft成为了这一支的根结点

    有子树

    由于在左右旋中,我们已经分析了模型高度,所以我们直接画出右左旋的抽象模型(本质一样的)

    旋转

    接下来就是相似的操作,对curleft进行右旋

    然后和p连接

    就变成了标准的左旋状态,对p进行左旋:

    最后就是:curleft的右结点通过右旋给了cur的左边,左结点经过左旋给了p的右边

    更新三个结点的bf

    这里用到的图解,是新结点插入在curleft的左树部分

    像一直在重复说的那样(臣妾已经说倦了)

    • 因为右旋,cueleft的右子树给了cur的左树,所以cur的左边是h,而右边是自己的h+1,所以cur的bf=1
    • 因为左旋,curleft的左子树给了p的右子树,而右子树正是新结点插入位置,所以p的左边是h+1,右边是原先自己的h+1,相等了,所以p的bf=0

    当新结点插入在curleft的右树部分

    经过旋转后:

    会发现:

    • 新结点所在的右子树,在右旋中给了cur的左子树,所以cur的左右相等了,cur的bf=0
    • 而给p的curleft右树高度仍是h,而p的左子树是自己的h+1,所以p的bf=-1​​​​​​​

    注意点

    一定要写对插入的逻辑,还有 各种结点指向 / bf数值 的更新,超级重要!!!

    • 亲身经历,本来以为对了,测试也是对的,过了几天加了点测试代码(因为实在是不好看这个代码到底写的对不对,又多又杂),结果就挂了
    • 找了半天的错,发现旋转代码没问题,是我插入里面的"往上更新bf"逻辑错了
    • 我是先一直更新到根结点,然后才找bf=2/-2的结点
    • 就导致,旋转完后子树的bf是对的,它父结点的bf错了,本来就不应该更新它的!(我哭死)

    然后就是旋转里面,"原子树根结点的父结点"的指向要注意不要漏掉,要更新啊!!!

    其他的好像就没啥了,只要多多写备注,就应该没啥问题

    代码

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. namespace my_AvlTree
    8. {
    9. template <class T>
    10. struct AvlTreeNode
    11. {
    12. AvlTreeNode(const T &data = T())
    13. : _left(nullptr), _right(nullptr), _parent(nullptr), _data(data), _bf(0)
    14. {
    15. }
    16. AvlTreeNode *_left;
    17. AvlTreeNode *_right;
    18. AvlTreeNode *_parent;
    19. T _data;
    20. int _bf; // 节点的平衡因子
    21. };
    22. // AVL: 二叉搜索树 + 平衡因子的限制
    23. template <class T>
    24. class AvlTree
    25. {
    26. typedef AvlTreeNode Node;
    27. public:
    28. AvlTree()
    29. : _root(nullptr)
    30. {
    31. }
    32. // 在AVL树中插入值为data的节点
    33. bool Insert(const T &data);
    34. // AVL树的验证
    35. bool IsAvlTree()
    36. {
    37. return _IsAvlTree(_root);
    38. }
    39. void levelOrder();
    40. size_t Height()
    41. {
    42. return _Height(_root);
    43. }
    44. private:
    45. // 根据AVL树的概念验证pRoot是否为有效的AVL树
    46. bool _IsAvlTree(Node *root);
    47. size_t _Height(Node *root);
    48. // 右单旋
    49. void RotateR(Node *parent);
    50. // 左单旋
    51. void RotateL(Node *parent);
    52. // 右左双旋
    53. void RotateRL(Node *parent);
    54. // 左右双旋
    55. void RotateLR(Node *parent);
    56. private:
    57. Node *_root;
    58. };
    59. template <class T>
    60. void AvlTree::levelOrder()
    61. {
    62. Node *root = _root;
    63. std::vector> arr;
    64. std::queue q;
    65. if (root != nullptr)
    66. {
    67. q.push(root);
    68. }
    69. while (!q.empty())
    70. {
    71. std::vector tmp; // 存储一层的结点
    72. int size = q.size(); // 此时队列内的元素就是上一层的结点个数
    73. for (int i = 0; i < size; ++i)
    74. {
    75. tmp.push_back(q.front()->_data);
    76. if (q.front()->_left)
    77. { // 有子树就放进队列中
    78. q.push(q.front()->_left);
    79. }
    80. if (q.front()->_right)
    81. {
    82. q.push(q.front()->_right);
    83. }
    84. q.pop(); // 出掉这个父结点
    85. }
    86. arr.push_back(tmp);
    87. }
    88. for (auto &i : arr)
    89. {
    90. for (auto &j : i)
    91. {
    92. std::cout << j << " ";
    93. }
    94. std::cout << std::endl;
    95. }
    96. }
    97. template <class T>
    98. bool AvlTree::Insert(const T &data)
    99. {
    100. if (_root == nullptr)
    101. {
    102. _root = new Node(data);
    103. }
    104. else
    105. {
    106. Node *cur = _root, *parent = cur, *newnode = nullptr;
    107. // 找到插入位置
    108. while (cur)
    109. {
    110. if (data > cur->_data)
    111. {
    112. parent = cur;
    113. cur = cur->_right;
    114. }
    115. else if (data < cur->_data)
    116. {
    117. parent = cur;
    118. cur = cur->_left;
    119. }
    120. else
    121. {
    122. return false;
    123. }
    124. }
    125. // 插入+改父亲bf
    126. newnode = new Node(data);
    127. if (data < parent->_data)
    128. {
    129. parent->_left = newnode;
    130. --parent->_bf;
    131. newnode->_parent = parent;
    132. }
    133. else
    134. {
    135. parent->_right = newnode;
    136. ++parent->_bf;
    137. newnode->_parent = parent;
    138. }
    139. // std::cout << "parent:" << parent->_data << " "
    140. // << "newnode:" << newnode->_data << std::endl;
    141. // 维护bf
    142. cur = parent; //注意,这里不能直接定义cur的父结点,因为不能保证cur不为空(如果此时只有俩结点,就为空了!!!)
    143. while (cur != _root)
    144. {
    145. Node *pnode = cur->_parent;
    146. // 更新bf
    147. if (pnode->_left == cur)
    148. {
    149. --pnode->_bf;
    150. }
    151. else
    152. {
    153. ++pnode->_bf;
    154. }
    155. // 判断是否继续往上更新
    156. if (cur->_bf == 0)
    157. {
    158. break;
    159. }
    160. else
    161. {
    162. if (pnode->_bf == 2 || pnode->_bf == -2)
    163. {
    164. // pnode就是不合法的结点,然后判断它该怎么调整
    165. if (pnode->_bf == -2 && cur->_bf == -1)
    166. {
    167. // 右单旋
    168. RotateR(pnode);
    169. }
    170. if (pnode->_bf == 2 && cur->_bf == 1)
    171. {
    172. // 左单旋
    173. RotateL(pnode);
    174. }
    175. if (pnode->_bf == -2 && cur->_bf == 1)
    176. {
    177. // 左右双旋
    178. RotateLR(pnode);
    179. }
    180. if (pnode->_bf == 2 && cur->_bf == -1)
    181. {
    182. // 右左双旋
    183. RotateRL(pnode);
    184. }
    185. break;
    186. }
    187. else
    188. {
    189. cur = pnode;
    190. pnode = pnode->_parent;
    191. }
    192. }
    193. }
    194. }
    195. return true;
    196. }
    197. template <class T>
    198. bool AvlTree::_IsAvlTree(Node *root)
    199. {
    200. if (root == nullptr)
    201. {
    202. return true;
    203. }
    204. int left_height = _Height(root->_left);
    205. int right_height = _Height(root->_right);
    206. if (right_height - left_height != root->_bf) //不要太相信自己写的bf计算方法
    207. {
    208. std::cout << right_height << std::endl;
    209. std::cout << left_height << std::endl;
    210. std::cout << root->_bf << std::endl;
    211. std::cout << "平衡因子异常" << std::endl;
    212. return false;
    213. }
    214. return abs(right_height - left_height) < 2 && _IsAvlTree(root->_left) && _IsAvlTree(root->_right);
    215. }
    216. template <class T>
    217. size_t AvlTree::_Height(Node *root)
    218. {
    219. if (root == nullptr)
    220. {
    221. return 0;
    222. }
    223. int leftsize = _Height(root->_left) + 1;
    224. int rightsize = _Height(root->_right) + 1;
    225. return leftsize > rightsize ? leftsize : rightsize;
    226. }
    227. template <class T>
    228. void AvlTree::RotateL(Node *parent)
    229. {
    230. Node *cur = parent->_right, *curleft = cur->_left, *ppnode = parent->_parent;
    231. // 连接cur上需要修改的子树(比如左旋就是让parent当cur的左子树,那么cur左树就得和parent右边相连)
    232. if (curleft) // 防止左树为空
    233. {
    234. curleft->_parent = parent;
    235. }
    236. parent->_right = curleft;
    237. // 连接cur和parent
    238. cur->_left = parent;
    239. // 修改parent父结点的指向
    240. if (ppnode == nullptr) // 如果此时parent是根结点,那么它没有父结点,且需要更新根结点
    241. {
    242. _root = cur;
    243. }
    244. else
    245. {
    246. if (ppnode->_left == parent)
    247. {
    248. ppnode->_left = cur;
    249. }
    250. else
    251. {
    252. ppnode->_right = cur;
    253. }
    254. }
    255. // 改父结点指向(千万别忘辣!!!)
    256. cur->_parent = ppnode;
    257. parent->_parent = cur;
    258. // 维护bf
    259. cur->_bf = parent->_bf = 0;
    260. }
    261. template <class T>
    262. void AvlTree::RotateR(Node *parent)
    263. {
    264. Node *cur = parent->_left, *curright = cur->_right, *ppnode = parent->_parent;
    265. // 连接cur上需要修改的子树(右旋就是让parent当cur的右子树,那么cur右树就得和parent左边相连)
    266. if (curright) // 防止右树为空
    267. {
    268. curright->_parent = parent;
    269. }
    270. parent->_left = curright;
    271. // 连接cur和parent
    272. cur->_right = parent;
    273. // 修改parent父结点的指向
    274. if (ppnode == nullptr) // 如果此时parent是根结点,那么它没有父结点,且需要更新根结点
    275. {
    276. _root = cur;
    277. }
    278. else
    279. {
    280. if (ppnode->_left == parent)
    281. {
    282. ppnode->_left = cur;
    283. }
    284. else
    285. {
    286. ppnode->_right = cur;
    287. }
    288. }
    289. // 改父结点指向
    290. cur->_parent = ppnode;
    291. parent->_parent = cur;
    292. // 维护bf
    293. cur->_bf = parent->_bf = 0;
    294. }
    295. template <class T>
    296. void AvlTree::RotateLR(Node *parent)
    297. {
    298. Node *cur = parent->_left, *curright = cur->_right;
    299. int bf_comp = curright->_bf; // 用于判断插入结点的左右位置
    300. RotateL(parent->_left); // 让cur成为curright的左子树
    301. RotateR(parent); // 让parent成为curright的右子树
    302. // curright是旋转后子树的根结点
    303. // 最终让curright的左子树给了cur的右子树,curright的右子树给了parent的左子树
    304. // if (_Height(curright) == 1)
    305. // {
    306. // curright->_bf = 0;
    307. // cur->_bf = 0;
    308. // parent->_bf = 0;
    309. // }
    310. if (bf_comp == 0)
    311. {
    312. curright->_bf = 0;
    313. cur->_bf = 0;
    314. parent->_bf = 0;
    315. }
    316. else
    317. {
    318. if (bf_comp == -1) // 在curright的左边
    319. {
    320. // 说明cur的右子树多了1,使其与原先的左子树高度相等
    321. // 而parent的左子树是h-1,右子树是原先的h
    322. curright->_bf = 0;
    323. cur->_bf = 0;
    324. parent->_bf = 1;
    325. }
    326. else if (bf_comp == 1) // 在curright的右边
    327. {
    328. // 说明parent的左子树多了1,使其与原先的右子树高度相等
    329. // 而cur的右子树是h-1,左子树是原先的h
    330. curright->_bf = 0;
    331. cur->_bf = -1;
    332. parent->_bf = 0;
    333. }
    334. else
    335. {
    336. assert(false);
    337. }
    338. }
    339. }
    340. template <class T>
    341. void AvlTree::RotateRL(Node *parent) // 插入结点在curleft的左右子树上其中一个位置
    342. {
    343. Node *cur = parent->_right, *curleft = cur->_left;
    344. int bf_comp = curleft->_bf; // 用于判断插入结点的左右位置
    345. RotateR(parent->_right); // 让cur成为curleft的右子树
    346. RotateL(parent); // 让parent成为curleft的左子树
    347. // curleft是旋转后子树的根结点
    348. // 最终让原curleft的右子树给了cur的左子树,curleft的左子树给了parent的右子树
    349. // if (_Height(curleft) == 1)
    350. // {
    351. // curleft->_bf = 0;
    352. // cur->_bf = 0;
    353. // parent->_bf = 0;
    354. // }
    355. if (bf_comp == 0)
    356. {
    357. curleft->_bf = 0;
    358. cur->_bf = 0;
    359. parent->_bf = 0;
    360. }
    361. else
    362. {
    363. if (bf_comp == -1) // 在curleft的左边
    364. {
    365. // 说明parent右子树多了1,使其与原先的左子树高度相等
    366. // 而cur的左子树是h-1,右子树是原先的h
    367. curleft->_bf = 0;
    368. cur->_bf = 1;
    369. parent->_bf = 0;
    370. }
    371. if (bf_comp == 1) // 在curleft的右边
    372. {
    373. // 说明cur的左子树多了1,使其与原先的右子树高度相等
    374. // 而parent的右子树是h-1,左子树是原先的h
    375. curleft->_bf = 0;
    376. cur->_bf = 0;
    377. parent->_bf = -1;
    378. }
    379. }
    380. }
    381. }

  • 相关阅读:
    WIFI7协议概述
    确定了,2022下半年软考报名8月开始
    什么是文档签名证书?PDF文档怎么签名?
    WPS转PDF怎么转换?接下来分享这三个方法和操作步骤给你
    【前端知识之JS】关于数据处理的手写代码汇总
    Java模拟实现无头节点的单链表
    数据可视化、BI和数字孪生软件:用途和特点对比
    C++ static使用 面试
    【入门篇】1.4 redis 客户端 之 Lettuce 详解
    (STM32)从零开始的RT-Thread之旅--SPI驱动ST7735(3)使用DMA
  • 原文地址:https://blog.csdn.net/m0_74206992/article/details/133188908