From 2b3ce4d5c34b4beba9da851f9fdee7a742a89d24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=90=E6=B6=9B?= Date: Tue, 13 Apr 2021 12:53:52 +0800 Subject: [PATCH] =?UTF-8?q?post:=E5=A2=9E=E5=8A=A0=E8=87=AA=E5=B9=B3?= =?UTF-8?q?=E8=A1=A1=E4=BA=8C=E5=8F=89=E6=A0=91=E6=96=87=E7=AB=A0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/_posts/avl-tree.md | 217 ++++++++++++++++++++++++++ source/_posts/avl-tree/avl-insert.svg | 3 + source/_posts/binary-search-tree.md | 3 +- source/_posts/binary-tree-basic.md | 3 +- 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 source/_posts/avl-tree.md create mode 100644 source/_posts/avl-tree/avl-insert.svg diff --git a/source/_posts/avl-tree.md b/source/_posts/avl-tree.md new file mode 100644 index 0000000..6aebfec --- /dev/null +++ b/source/_posts/avl-tree.md @@ -0,0 +1,217 @@ +--- +title: 自平衡二叉树 +date: 2021-04-13 12:09:40 +tags: [架构知识, 算法设计, 树形结构, 数据结构, 二叉树, 平衡二叉树, Java] +categories: + - [架构知识, 数据结构] + - [架构知识, 算法设计] +mathjax: true +--- +普通的二叉查找树(BST)虽然已经实现了对于节点的快速查找,但是如果树的拓扑结构没有设计正确,例如将一个有序序列存入BST中,就会使BST的二分查找能力损失,也就是常说的失去了平衡。为了保证BST的查找能力,在BST形成过程中进行平衡调整,就形成了平衡二叉查找树,简称平衡二叉树(AVL-tree)。 + + +> 自平衡二叉树的英文缩写是采用其两位发明者的名字,所以与其直译英文Self-balanced Binary Search Tree不同。 + +AVL具备以下性质: + +1. 从任何一个节点出发,左子树和右子树的深度差的绝对值不超过1。这个深度差被称作**平衡因子**。 +1. 任何一个节点的左子树和右子树都是平衡二叉树。 + +通过AVL所实现的拓扑结构,就保证了树的二分查找能力,AVL在查找、插入和删除时的复杂度均为$O(logN)$。 + +## AVL树构建 + +要构建一棵AVL树,我们依旧可以使用前面定义的节点类,但是为了能够适应AVL树的一些额外属性和操作,我们还是需要将这个节点类扩展一下。 + +```java +public class AVLNode extends BSTNode { + private Integer depth; + + public AVLNode(Integer value) { + super(value); + this.depth = 0; + } + + public Integer getDepth() { + return this.depth; + } + + public Integer setDepth(Integer depth) { + this.depth = depth; + } + + public void increaseDepth() { + this.depth++; + } + + public void decreaseDepth() { + this.depth--; + } +} +``` + +实例中的AVL树节点类增加了一个用于保存节点深度的属性,这个深度属性可以用来比较得出是否平衡的结论。 + +## 树的旋转 + +向一个AVL中插入或者删除一个节点,会导致AVL的不平衡。例如以下示例。 + +{% asset_img avl-insert.svg 'AVL树插入节点导致不平衡' 400 %} + +左侧的AVL在插入一个新的节点5之后,就变得不平衡了。节点50的左子树的高度为4,右子树的高度为2,AVL已经失去了平衡。在这种失衡的情况下,AVL是通过旋转最小失衡子树来重新获取平衡的。那么这里就引入了一个新的名词:**最小失衡子树**。 + +**最小失衡子树**是指从新插入的节点向根查找,第一个平衡因子的绝对值超过1的节点即为最小失衡子树的根节点。一棵失衡的AVL树中,是可能会同时存在多棵失衡子树的,但是要使AVL树恢复平衡,只需要调整最小的失衡子树即可。 + +对失衡子树的调整是通过旋转来完成的,旋转子树的目的是降低树的高度。子树的旋转有两个方向:左旋和右旋,使用哪个方向是由子树的高度决定的。如果节点的右子树高度比较高,那么就采用左旋,降低右子树高度;反之,使用右旋可以降低左子树高度。 + +### 左旋 + +最小失衡子树的左旋可以遵循以下步骤处理: + +1. 子树根节点(ori)的右孩子(rc)替代根节点成为新的根节点。 +1. 右孩子(rc)的左子树变为原根节点(ori)的右子树。 +1. 原根节点(ori)变为右孩子(rc)的左子树。 + +具体操作实现可以参考以下例程。 + +```java +public void rotateLeft(AVLNode root) { + root.getRightChild().ifPresent(rightChild -> { + root.getParent().ifPresent(parent -> { + if (root.isLeftChild()) { + parent.attachLeftChild(rightChild); + } else { + parent.attachRightChild(rightChild); + } + }); + rightChild.getLeftChild().ifPresent(root::attachRightChild); + rightChild.attachLeftChild(root); + }); +} +``` + +## 右旋 + +最小失衡子树的右旋非左旋正好相反,可以遵循以下步骤: + +1. 子树根节点(ori)的左孩子(lc)替代根节点成为新的根节点。 +1. 左孩子(lc)的右子树变为原根节点(ori)的左子树。 +1. 原根节点(ori)变为左孩子(lc)的右子树。 + +具体操作实现可以参考以下例程。 + +```java +public void rotateRight(AVLNode root) { + root.getLeftChild().ifPresent(leftChild -> { + root.getParent().ifPresent(parent -> { + if (root.isLeftChild()) { + parent.attachLeftChild(leftChild); + } else { + parent.attachRightChild(leftChild); + } + }); + leftChild.getRightChild().ifPresent(root::attachLeftChild); + leftChild.attachRightChild(root); + }); +} +``` + +## 节点插入 + +向AVL中的某一个节点k的左右子树上插入一个新节点n,会有以下四种情况可以破坏原有AVL的平衡性: + +1. 在节点k的左子树根节点的左子树上插入节点n(简称LL插入)。要重新达到平衡需要执行右旋操作。 +1. 在节点k的右子树根节点的右子树上插入节点n(简称RR插入)。要重新达到平衡需要执行左旋操作。 +1. 在节点k的左子树根节点的右子树上插入节点n(简称LR插入)。要重新达到平衡需要先执行左旋操作,再执行右旋操作。 +1. 在节点k的右子树根节点的左子树上插入节点n(简称RL插入)。要重新达到平衡需要先执行右旋操作,再执行左旋操作。 + +由于新节点都是作为叶子节点插入,所以在完成节点插入以后,需要完成一项工作,就是更新所有父代节点的深度值。这个更新不需要遍历整棵树,只需要遍历新节点的所有父代路径即可。以下是深度值更新的示例。 + +```java +public void updateDepth(AVLNode node) { + if (node.isLeaf()) { + node.setDepth(0); + } else { + Integer leftChildDepth = node.getLeftChild().get(AVLNode::getDepth).orElse(0); + Integer rightChildDepth = node.getRightChild().get(AVLNode::getDepth).orElse(0); + node.setDepth(IntStream + .of(leftChildDepth, rightChildDepth) + .map(d -> d + 1) + .max() + .orElse(0)); + } + node.getParent().ifPresent(this::upateDepth); +} +``` + +虽然在节点插入后的重新平衡有四种情况,但是总结起来,只是一个不断遍历新插入节点的父级路径,使其父级路径重新平衡的过程。而在这个过程中,针对每一个节点,可以判断其左右子树的深度值,如果左侧的深度大于右侧的深度,那么就采用右旋进行平衡;反之采用左旋进行平衡。 + +以下是一个处理新插入节点并实现再平衡的示例。 + +```java +public void insert(Integer value) { + // 这个pointer十分有用,可以用来对树进行遍历和操作 + AVLNode pointer = this.root; + ANLNode newNode = new AVLNode(value); + // 首先完成节点的插入 + while (!pointer.isLeaf()) { + if (pointer.compareTo(newNode) > 0) { + pointer = pointer.getRightChild().get(); + continue; + } + if (pointer.compareTo(newNode) < 0) { + pointer = pointer.getLeftChild().get(); + continue; + } + if (pointer.compareTo(newNode) == 0) { + throw new RepeatValueException(); + } + } + if (newNode.compareTo(pointer) < 0) { + pointer.attachLeftChild(newNode); + } else { + pointer.attachRightChild(newNode); + } + this.updateDepth(newNode); + // 然后开始对新节点的父级路径进行再平衡 + pointer = newNode; + while (pointer.getParent().isPresent()) { + Integer leftChildDepth = pointer.getLeftChild().map(AVLNode::getDepth).orElse(0); + Integer rightChildDepth = pointer.getRightChild().map(AVLNode::getDepth).orElse(0); + if (Math.abs(leftChildDepth - rightChildDepth) > 1) { + if (leftChildDepth > rightChildDepth) { + this.rightRotate(pointer); + } else { + this.leftRotate(pointer); + } + this.updateDepth(pointer); + pointer = newNode; + } else { + pointer = pointer.getParent().get(); + } + } +} +``` + +实例代码中虽然不仅对仅需要再平衡的最小失衡子树进行了平衡,而且也同时扫描了新插入节点的所有父级路径。这样做的目的主要是把需要两次旋转的操作化简到了一个循环中。由于每次仅会扫描一条路径,所以增加的复杂度有限。 + +## 节点删除 + +AVL中的节点删除与BST中的节点删除操作是相同的,只是AVL在完成删除操作以后需要修正所有的不平衡节点。一般都会分为以下四种情况来处理。 + +1. 被删除节点是叶子节点。 +1. 被删除节点只有左子树。 +1. 被删除节点只有右子树。 +1. 被删除节点既有左子树又有右子树。 + +在完成节点的删除以后,可以选择被删除节点的左右子树中深度较大的那一支,来更新整条路径上的节点深度,并进行再平衡操作。 + +## AVL树的缺点 + +AVL在大量查找操作的情况下效率会更高,但是对于增删操作,AVL会进行大量的再平衡操作,这样就大大降低了AVL的性能。所以在查找操作远大于增删操作次数的时候,使用AVL会得到更高的效率。如果在树中的增删操作比较多,那么可以选择增删性能更高的红黑树。 + +## 系列文章 + +1. {% post_link binary-tree-basic %} +1. {% post_link binary-search-tree %} +1. {% post_link avl-tree %} diff --git a/source/_posts/avl-tree/avl-insert.svg b/source/_posts/avl-tree/avl-insert.svg new file mode 100644 index 0000000..702bcfc --- /dev/null +++ b/source/_posts/avl-tree/avl-insert.svg @@ -0,0 +1,3 @@ + + +
50
50
30
30
70
70
40
40
60
60
20
20
10
10
50
50
30
30
70
70
40
40
60
60
20
20
10
10
5
5
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/source/_posts/binary-search-tree.md b/source/_posts/binary-search-tree.md index e51680c..45b93c1 100644 --- a/source/_posts/binary-search-tree.md +++ b/source/_posts/binary-search-tree.md @@ -166,4 +166,5 @@ BST的缺点在于,如果没有能够很好的规划树的拓扑结构,那 ## 系列文章 1. {% post_link binary-tree-basic %} -1. 二叉搜索树 +1. {% post_link binary-search-tree %} +1. {% post_link avl-tree %} diff --git a/source/_posts/binary-tree-basic.md b/source/_posts/binary-tree-basic.md index 1163ef3..4ed1ead 100644 --- a/source/_posts/binary-tree-basic.md +++ b/source/_posts/binary-tree-basic.md @@ -193,5 +193,6 @@ public void breadthFirstTraversal(Node root) { ## 系列文章 -1. 二叉树基础 +1. {% post_link binary-tree-basic %} 1. {% post_link binary-search-tree %} +1. {% post_link avl-tree %}