平衡二叉树的插入与删除

Catalogue
  1. 1. 定义
  2. 2. 为什么有AVL树
  3. 3. 插入操作
    1. 3.1. 1.左-左情况
    2. 3.2. 2.左-右情况
    3. 3.3. 3.右-右情况
    4. 3.4. 4.右-左情况
    5. 3.5. 代码实现
  4. 4. 删除操作
    1. 4.1. 代码实现
  5. 5. 小结
  6. 6. 参考资料

定义

AVL树是带有平衡条件的二叉查找树。它要求在AVL树中任何节点的两个子树的高度(高度是指节点到一片树叶的最长路径的长) 最大差别为1,如下图所示:

AVL

为什么有AVL树

大多数BST操作,例如查找,找最大,最小值,插入,删除等操作,基本上消耗O(h)时间,h是BST的高度。对于倾斜的二叉树,这些操作的成本可能会变成O(n)。

如果我们在每次插入和删除之后确保树的高度保持O(logn),那么我们就可以保证所有这些操作的O(logn)的上界。而AVL树的高度总是O(logn),其中n是树中节点的数量。

所以AVL树很好的解决了二叉搜索树退化成链表的问题,把插入,查找,删除的时间复杂度的最好情况和最坏情况都维持在O(logn)

插入操作

当我们执行插入一个节点时,很可能会破坏AVL树的平衡特性,所以我们需要调整AVL树的结构,使其重新平衡,而调整的方式称之为旋转

这里我们针对父节点的位置分为左-左左-右右-右右-左这4类情况分析,而对左-左右-右情况进行单旋转,也就是一次旋转,对左-右右-左情况进行双旋转,两次旋转,最终恢复其平衡特性。

1.左-左情况

AVL1

2.左-右情况

AVL2

3.右-右情况

AVL3

4.右-左情况

AVL4

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package com.pjmike.tree.AVL;

/**
* AVL树
*
* @author pjmike
* @create 2018-08-09 17:57
*/
public class AVLTree {
private Node root;

/**
* 计算AVL节点的高度的方法
*
* @param node
* @return
*/
private int height(Node node) {
//如果为空,返回height为0
return node == null ? 0 : node.height;
}

/**
* 计算两个的最大值
*
* @param a
* @param b
* @return
*/
private int max(int a, int b) {
return a > b ? a : b;
}
/**
* 右旋转
*
* @param y
* @return
*/
Node rightRotate(Node y) {
Node x = y.left;
Node T1 = x.right;
x.right = y;
y.left = T1;
//更新高度
y.height = max(height(y.left), height(y.right)) + 1;
x.height = max(height(x.left), height(x.right)) + 1;
return x;
}

/**
* 左旋转
*
* @param x
* @return
*/
Node leftRotate(Node x) {

Node y = x.right;
Node T2 = y.left;
y.left = x;
x.right = T2;
//更新高度
x.height = max(height(x.left), height(x.right)) + 1;
y.height = max(height(y.left), height(y.right)) + 1;
return y;
}

/**
* 获取平衡因子
*
* @param node
* @return
*/
int getBalance(Node node) {
return node == null ? 0 : (height(node.left) - height(node.right));
}
/**
* 插入操作
*
* @param node
* @param val
* @return
*/
Node insert(Node node, int val) {
if (node == null) {
return new Node(val);
}
if (val < node.val) {
node.left = insert(node.left, val);
} else if (val > node.val) {
node.right = insert(node.right, val);
} else {
return node;
}
//更新节点高度
node.height = 1 + max(height(node.left), height(node.right));
//这是插入完毕后的
int balance = getBalance(node);
if (balance > 1 && val < node.left.val) {
//右旋
return rightRotate(node);
}
if (balance < -1 && val > node.right.val) {
//左旋
return leftRotate(node);
}
if (balance > 1 && val > node.left.val) {
//先左旋,再右旋
node.left = leftRotate(node.left);
return rightRotate(node);
}
if (balance < -1 && val < node.right.val) {
//先右旋再左旋
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}

public static void main(String[] args) {
AVLTree avlTree = new AVLTree();
//创造AVL树
avlTree.root = avlTree.insert(avlTree.root, 10);
avlTree.root = avlTree.insert(avlTree.root, 20);
avlTree.root = avlTree.insert(avlTree.root, 30);
avlTree.root = avlTree.insert(avlTree.root, 40);
avlTree.root = avlTree.insert(avlTree.root, 50);
avlTree.root = avlTree.insert(avlTree.root, 23);
avlTree.Preorder(avlTree.root);
}

void Preorder(Node node) {
if (node != null) {
System.out.println(node.val+" ");
Preorder(node.left);
Preorder(node.right);
}
}
}

class Node {
int val;
Node left;
Node right;
/**
* 节点高度,高度是指节点到一片树叶的最长路径的长
*/
int height;

public Node(int val) {
this.val = val;
height = 1;
}

}

删除操作

对于二叉查找树,我们都知道它的删除要分三种情况:

  • 删除的节点为叶子节点
  • 删除的节点左子树或右子树有一个为空
  • 删除的节点有两个子树

AVL本身是带有平衡性质的二叉搜索树,所以它的删除策略是与二叉查找树相似的,只不过删除节点后可能会造成树失去平衡性,所以需要做平衡处理

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
Node deleteNode(Node root, int val) {
if (root == null) {
return root;
}
if (val < root.val) {
root.left = deleteNode(root.left, val);
} else if (val > root.val) {
root.right = deleteNode(root.right, val);
} else {
//删除节点有两个孩子
if (root.left != null && root.right != null) {
root.val = findMin(root.right);
root.right = deleteNode(root.right, root.val);
} else {
//删除节点只有一个孩子或者没有孩子
root = (root.left != null) ? root.left : root.right;
}
}
//以下操作是为了恢复AVL树的平衡性
if (root == null) {
return root;
}
root.height = max(height(root.left), height(root.right)) + 1;
int balance = getBalance(root);
//左-左情况,这里使用>=而不是>就是为了保证这些情形下使用的是单旋转而不是双旋转
if (balance > 1 && getBalance(root.left) >= 0) {
return rightRotate(root);
}
//左-右情况
if (balance > 1 && getBalance(root.left) < 0)
{
root.left = leftRotate(root.left);
return rightRotate(root);
}

//右-右情况
if (balance < -1 && getBalance(root.right) <= 0) {
return leftRotate(root);
}
//右-左情况
if (balance < -1 && getBalance(root.right) > 0)
{
root.right = rightRotate(root.right);
return leftRotate(root);
}
return root;
}

private int findMin(Node root) {
if (root.left == null) {
return root.val;
} else {
return findMin(root.left);
}
}

这里的删除后的平衡操作实际上与插入后的平衡操作是不一样的,删除可能造成树的一边比另一边浅的情况,还可能造成整个二叉树中有两个子树一样深的情况。

小结

显然,AVL树的优势在于插入,删除,查找操作的平均时间复杂度都为O(logn),但是它的缺陷是为了保持AVL树的平衡性质,动态插入和删除的代价也是很大的

参考资料

Bagikan Komentar