二叉查找树 BST : https://blog.csdn.net/cj_286/article/details/90183298
二叉平衡树 AVL : https://blog.csdn.net/cj_286/article/details/90217072
红黑树 RBT : https://blog.csdn.net/cj_286/article/details/90245150
为什么需要AVL树
BST与TreeMap的效率对比
1.随机序列的存取 (他们的存取速度差不多)
2.升序或降序序列的存取 (两万的数据量TreeMap的存取(先存后取)速度是BST的四百倍左右)
BST TreeMap
随机序列 OK OK
升序或降序序列 Slow OK
为什么BST在极端情况下存取速度会如此的慢呢,因为在极端情况下,BST会退化为链表(升序或降序),时间复杂度会由原来的O(logN)退化为O(N),所以查询速度会变慢
4 1 7
/ \ \ /
2 6 O(logN)--> 2 O(N)--> 6 O(N)
/ \ / \ \ /
1 3 5 7 3 5
\ /
4 4
\ /
5 3
\ /
6 2
\ /
7 1
BST随机存储 BST升序存储 BST降序存储
在升序或降序的情况下BST明显是满足不了需求的,那么有没有哪种数据结构,对于任何插入节点或者删除节点的操作都能自动的保持树的平衡,这时AVL树就诞生了,AVL树它是一种自平衡的树。
性质
以下AVL的代码是基于BST代码的,只是添加了使其平衡的代码
在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis
AVL树本质上还是一棵二叉搜索树,它的特点是:
1.本身首先是一棵二叉搜索树。
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子Balance Factor)最多为1。
3.空树、左右子树都是AVL
也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)。
对比AVL树和非AVL树
由图可知,一棵AVL树不一定是完全二叉树,AVL树它的每个子节点的平衡因子的绝对值都是小于等于1的,它的每个子节点都是一个AVL树
非AVL树转为AVL树
为了简化操作,只考虑三个节点的情况
三个节点单旋转
以3为根节点顺时针旋转,旋转之后,原来的根节点3变成了原来的左子树2的右子树,原来的左子树2变成了根节点,这时二叉树就恢复平衡变成AVL树
三个节点双旋转
首先先处理节点1,将节点1进行左旋转,原来的右子树2变成了新的根节点,原来的1变成了2的左子树,这时的情况和上面的单旋转情况一样了,以3节点右旋就变成了一个AVL树了
JDK TreeMap右旋源码解析
红色节点是相对位置发生了改变,l原本是左子树,右旋过后,l代替了p,p变成了l的右子树,原本l.right是l的右孩子,右旋之后,l.right变成了p的左孩子。
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left; //取得p的左孩子l
p.left = l.right; //l的右孩子l.right变成p的左孩子
if (l.right != null) l.right.parent = p; //l.right的父节点设置为p
l.parent = p.parent; //l的父节点设置为p的父节点p.parent
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l; //p.parent的左孩子或者右孩子设置为l
else p.parent.left = l;
l.right = p; //l的右子树设置为p
p.parent = l;//p的父节点设置为l
}
}
JDK TreeMap左旋源码解析
红色节点是相对位置发生了改变,r原本是右子树,左旋过后,r替代了p,p变成了r的左孩子,原本的r.left是r的左孩子,左旋之后,r.left变成了p的右孩子
左旋和右旋完全对称
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
什么时候需要旋转
1,插入关键字key后,结点p的平衡因子由原来 的1或者-1,变成了2或者-2,则需要旋转:值考虑插入key到左子树left的情况,即平衡因子是2
情况1:key < left.key,即插入到left的左子树,需要进行单旋转,将结点p右旋 (图:avl-1-4-1)
情况2:key > left.key,即插入到left的右子树,需要进行双旋转,先将left左旋,再将p右旋 (图:avl-1-4-2 ,avl-1-4-3)
2,插入到右子树right、平衡因子为-2,完全对称
平衡因子是2的情况示图如下
情况1示图
情况2示图(1)
情况2示图(2)
平衡因子是-2的情况和2的情况正好相反
插入
AVL的插入与BST完全相同,都是自顶向下的
检测是否平衡并旋转的调整过程:
1.AVL性质2决定了在检测结点p是否平衡之前,必须先保证 左右子树已经平衡
2.子问题必须成立 推导出 总问题是否成立,则说明是自底向上(这个很重要,自底向上,在递归或者循环去实现左旋或者右旋都是自底向上的去计算高度(height),这样计算高度才不会出错,所以在代码中计算高度只右+1而没有-1,先增的叶子节点的高度都是固定的1)
3.有parent指针,直接向上回溯
4.无parent指针,后续遍历框架,递归
5.无parent指针,栈实现非递归
实现 (以下AVL的代码是基于BST代码的,只是添加了使其平衡的代码)
1.AVLEntry增加height属性,表示树的高度,平衡因子可以实时计算
2.单旋转:右旋rotateRight、左旋rotateLeft
3.双旋转:先左后右firstLeftThenRight、先右后左firstRightThenLeft
4.实现非递归,需要辅助栈Stack,将插入时候所经过的路径压栈
5.插入调整函数fixAfterInsertion
6.辅助函数checkBalance,断言AVL树的平衡性,检测算法的正确性
AVLMap中添加height属性,表示树的高度,添加获取节点高度的方法
/**
* 返回一个结点的高度
* @param p
* @return
*/
public int getHeight(AVLEntry<K,V> p){
return p == null ? 0 : p.height;
}
旋转调整
单旋转,右旋代码实现
/**
* 右旋(单旋转)
* 该方法需要右返回值,因为AVLEntry中没有parent指针(JDK中是有parent指针的,所以不需要有返回值),旋转之后它的新的根节点需要返回
* @param p
* @return
*/
private AVLEntry<K,V> rotateRight(AVLEntry<K,V> p) {
AVLEntry<K,V> left = p.left;
p.left = left.right;
left.right = p;
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
left.height = Math.max(getHeight(left.left),p.height) + 1;
return left;//新的根节点
}
单旋转,左旋代码实现(和右旋完全对称)
和图:avl-1-2-1完全对称
/**
* 左旋(单旋转)
* @param p
* @return
*/
private AVLEntry<K, V> rotateLeft(AVLEntry<K, V> p) {
AVLEntry<K,V> right = p.right;
p.right = right.left;
right.left = p;
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
right.height = Math.max(p.height,getHeight(right.right)) + 1;
return right;//新的根节点
}
双旋转,先左旋再右旋代码实现
/**
* 先左旋再右旋
* 先将p.left进行左旋,再将p进行右旋
* @param p
* @return
*/
private AVLEntry<K,V> firstLeftThenRight(AVLEntry<K,V> p) {
p.left = rotateLeft(p.left);
p = rotateRight(p);
return p;
}
双旋转,先右旋再左旋代码实现
和图:avl-1-2-2完全对称
/**
* 先右旋再左旋
* 先将p.right进行右旋,再将p进行左旋
* @param p
* @return
*/
private AVLEntry<K, V> firstRightThenLeft(AVLEntry<K, V> p) {
p.right = rotateRight(p.right);
p = rotateLeft(p);
return p;
}
旋转代码写完,下面实现插入平衡的代码,要实现插入调整树平衡,需要引入栈Stack,使用栈可以实现插入调整的非递归算法
private LinkedList<AVLEntry<K,V>> stack = new LinkedList<>();//用于实现插入调整的非递归算法
插入调整函数实现
插入的时候需要将其走的所有路径不断压栈
public V put(K key,V value) {
if (root == null) {
root = new AVLEntry<K,V>(key,value);
stack.push(root);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
size ++;
}else{
AVLEntry<K,V> p = root;
while (p != null) {
stack.push(p);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
int cmp = compare(key,p.key);
if (cmp < 0) {
if (p.left == null) {
p.left = new AVLEntry<K,V>(key,value);
stack.push(p.left);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
size ++;
break;
}else{
p = p.left;//再次循环比较
}
} else if (cmp > 0) {
if (p.right == null) {
p.right = new AVLEntry<K,V>(key,value);
stack.push(p.right);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
size ++;
break;
}else{
p = p.right;
}
}else{
p.setValue(value);//替换旧值
break;
}
}
}
fixAfterInsertion(key);
//不管是插入的是新值还是重复值,都返回插入的值,这个和JDK TreeMap不一样
return value;
}
/**
* 插入调整,使其二叉搜索树达到平衡
* @param key
*/
private void fixAfterInsertion(K key){
AVLEntry<K,V> p = root;
while (!stack.isEmpty()) {
p = stack.pop();//插入所走的路径不断弹栈
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);//计算平衡因子
if (Math.abs(d) <= 1) { //改树平衡无需调整(旋转)
continue;
}else{
if (d == 2) {
if (compare(key, p.left.key) < 0) { //插入到了左子树的左子树
p = rotateRight(p);//单旋转:右旋rotateRight
}else{//插入到了左子树的右子树
p = firstLeftThenRight(p); //双旋转:先左后右firstLeftThenRight
}
}else{ //d == -2
if (compare(key, p.right.key) > 0) { //插入到了右子树的右子树
p = rotateLeft(p);//单旋转:左旋rotateLeft
}else{//插入到了右子树的左子树
p = firstRightThenLeft(p);//双旋转:先右后左firstRightThenLeft
}
}
//旋转过后,需要判断走的是左子树还是右子树,也就是检测爷爷结点,也就是p.parent要设置左子树还是右子树
if (!stack.isEmpty()) {
if (compare(key, stack.peek().key) < 0) { //表明插入到了左子树
stack.peek().left = p;
}else{
stack.peek().right = p;
}
}
}
}
root = p;//重新设置根节点
}
插入调整插入调整优化
/**
* 插入调整,使其二叉搜索树达到平衡
* @param key
*/
private void fixAfterInsertion(K key){
AVLEntry<K,V> p = root;
while (!stack.isEmpty()) {
p = stack.pop();//插入所走的路径不断弹栈
//优化
//**************************************************************
int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
if (p.height > 1 /*保证p不是叶子节点*/ && newHeight == p.height/*高度没有改变*/) {
stack.clear();
return;
}
//**************************************************************
p.height = newHeight;//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);//计算平衡因子
if (Math.abs(d) <= 1) { //改树平衡无需调整(旋转)
continue;
}else{
if (d == 2) {
if (compare(key, p.left.key) < 0) { //插入到了左子树的左子树
p = rotateRight(p);//单旋转:右旋rotateRight
}else{//插入到了左子树的右子树
p = firstLeftThenRight(p); //双旋转:先左后右firstLeftThenRight
}
}else{ //d == -2
if (compare(key, p.right.key) > 0) { //插入到了右子树的右子树
p = rotateLeft(p);//单旋转:左旋rotateLeft
}else{//插入到了右子树的左子树
p = firstRightThenLeft(p);//双旋转:先右后左firstRightThenLeft
}
}
//旋转过后,需要判断走的是左子树还是右子树,也就是检测爷爷结点,也就是p.parent要设置左子树还是右子树
if (!stack.isEmpty()) {
if (compare(key, stack.peek().key) < 0) { //表明插入到了左子树
stack.peek().left = p;
}else{
stack.peek().right = p;
}
}
}
}
root = p;//重新设置根节点
}
AVL插入平衡算法改进与时间复杂度分析
1,弹栈的时候,一旦发现某个节点的高度未发生改变,则立即停止回溯
2,指针回溯次数,最坏情况O(logN),最好情况O(1),平均任然是O(logN)
3,旋转次数,无旋转O(0),单旋转O(1),双旋转O(2),不会超过两次,平均O(1) (AVL树插入旋转不会超过两次)
4,时间复杂度:BST的插入O(logN) + 指针回溯O(logN) + 旋转O(1) = O(logN)
5,空间复杂度:有parent为O(1),无parent为O(logN)
插入平衡练习
将给定的排序数组转化为平衡二叉树,左右子树高度差的绝对值不超过1
实现方式1:使用AVLMap中的put实现方式
时间复杂度O(NlogN),空间复杂度O(N)
/**
* 108(https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/)
* 给定排序数组,将它转化为平衡二叉树
* 要求左右子树高度差的绝对值不超过1(性质2)
*
* 实现方式1
* AVLMap的put实现方式
*
*/
public class ConvertSortedArrayToBinarySearchTree {
class LeetCodeAVL{
private int size;
private TreeNode root;
private LinkedList<TreeNode> stack = new LinkedList<>();
public LeetCodeAVL() {
}
public int size() {
return this.size;
}
public boolean isEmpty() {
return this.size == 0;
}
public void put(int key) {
if (root == null) {
root = new TreeNode(key);
stack.push(root);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
size ++;
}else{
TreeNode p = root;
while (p != null) {
stack.push(p);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
int cmp = key - p.val;
if (cmp < 0) {
if (p.left == null) {
p.left = new TreeNode(key);
size ++;
stack.push(p.left);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
break;
}else{
p = p.left;//再次循环比较
}
} else if (cmp > 0) {
if (p.right == null) {
p.right = new TreeNode(key);
size ++;
stack.push(p.right);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
break;
}else{
p = p.right;
}
}else{
break;
}
}
}
fixAfterInsertion(key);
}
private HashMap<TreeNode,Integer> heightMap = new HashMap<>();
/**
* 返回一个结点的高度
*/
public int getHeight(TreeNode p) {
return heightMap.containsKey(p) ? heightMap.get(p):0;
}
/**
* 右旋(单旋转)
* 该方法需要右返回值,因为AVLEntry中没有parent指针(JDK中是有parent指针的,所以不需要有返回值),旋转之后它的新的根节点需要返回
* @param p
* @return
*/
private TreeNode rotateRight(TreeNode p) {
TreeNode left = p.left;
p.left = left.right;
left.right = p;
heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
heightMap.put(left,Math.max(getHeight(left.left),getHeight(p)) + 1);
return left;//新的根节点
}
/**
* 左旋(单旋转)
* @param p
* @return
*/
private TreeNode rotateLeft(TreeNode p) {
TreeNode right = p.right;
p.right = right.left;
right.left = p;
heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
heightMap.put(right,Math.max(getHeight(p),getHeight(right.right)) + 1);
return right;//新的根节点
}
/**
* 先左旋再右旋
* 先将p.left进行左旋,再将p进行右旋
* @param p
* @return
*/
private TreeNode firstLeftThenRight(TreeNode p) {
p.left = rotateLeft(p.left);
p = rotateRight(p);
return p;
}
/**
* 先右旋再左旋
* 先将p.right进行右旋,再将p进行左旋
* @param p
* @return
*/
private TreeNode firstRightThenLeft(TreeNode p) {
p.right = rotateRight(p.right);
p = rotateLeft(p);
return p;
}
/**
* 插入调整,使其二叉搜索树达到平衡
* @param key
*/
private void fixAfterInsertion(int key){
TreeNode p = root;
while (!stack.isEmpty()) {
p = stack.pop();//插入所走的路径不断弹栈
int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
if (heightMap.containsKey(p) && getHeight(p) > 1 /*保证p不是叶子节点*/ && newHeight == getHeight(p)/*高度没有改变*/) {
stack.clear();
return;
}
heightMap.put(p,newHeight);//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);//计算平衡因子
if (Math.abs(d) <= 1) { //改树平衡无需调整(旋转)
continue;
}else{
if (d == 2) {
if (key - p.left.val < 0) { //插入到了左子树的左子树
p = rotateRight(p);//单旋转:右旋rotateRight
}else{//插入到了左子树的右子树
p = firstLeftThenRight(p); //双旋转:先左后右firstLeftThenRight
}
}else{ //d == -2
if (key - p.right.val > 0) { //插入到了右子树的右子树
p = rotateLeft(p);//单旋转:左旋rotateLeft
}else{//插入到了右子树的左子树
p = firstRightThenLeft(p);//双旋转:先右后左firstRightThenLeft
}
}
//旋转过后,需要判断走的是左子树还是右子树,也就是检测爷爷结点,也就是p.parent要设置左子树还是右子树
if (!stack.isEmpty()) {
if (key - stack.peek().val < 0) { //表明插入到了左子树
stack.peek().left = p;
}else{
stack.peek().right = p;
}
}
}
}
root = p;//重新设置根节点
}
}
public TreeNode sortedArrayToBST(int[] nums){
if (nums == null || nums.length == 0) { //边界检测
return null;
}
LeetCodeAVL avl = new LeetCodeAVL();
for (int num : nums) {
avl.put(num);
}
return avl.root;
}
}
实现方式2:递归构建AVL + BST
参考TreeMap中的buildFromSorted
时间复杂度O(N),空间复杂度O(logN)
二分快排归并的递归算法实现方式二
public class ConvertSortedArrayToBinarySearchTree {
/**
* 模仿TreeMap中的buildFromSorted
* 时间复杂度O(N)
* 空间复杂度O(logN)
* @param nums
* @return
*/
public TreeNode sortedArrayToBST(int[] nums){
if (nums == null || nums.length == 0) {
return null;
}
return buildFromSorted(0,nums.length - 1,nums);
}
private TreeNode buildFromSorted(int lo, int hi, int[] nums) {
if (hi < lo) {
return null;
}
int mid = (lo + hi) / 2;
TreeNode left = null;
if (lo < mid) {
left = buildFromSorted(lo, mid - 1, nums);
}
TreeNode middle = new TreeNode(nums[mid]);
if (left != null) {
middle.left = left;
}
if (mid < hi) {
TreeNode right = buildFromSorted(mid + 1, hi, nums);
middle.right = right;
}
return middle;
}
}
计算完整二叉树的高度
JDK TreeMap源码中的通过节点个数计算树的层数,实现原理使用的是二分法
时间复杂度O(logN)
private static int computeRedLevel(int sz) {
int level = 0;
for (int m = sz - 1; m >= 0; m = m / 2 - 1)
level++;
return level;
}
删除
AVL的删除
AVL的删除只需在BST的删除基础上加上删除平衡即可
1,类似插入,假设删除了p右子树的某个结点,引起了p的平衡因子d[p]=2,分析p的左子树left,三种情况如下:
情况1:left的平衡因子d[left]=1,将p右旋 (图:avl-1-6-1)
情况2:left的平衡因子d[left]=0,将p右旋 (图:avl-1-6-2)
情况3:left的平衡因子d[left]=-1,先左旋left,再右旋p (图:avl-1-6-3)
2,删除左子树,即d[p]=-2的情况,与d[p]=2对称
代码实现
删除节点后,调整该节点,使其整棵树保持平衡
/**
* 删除调整
* 1,类似插入,假设删除了p右子树的某个结点,引起了p的平衡因子d[p]=2,分析p的左子树left,三种情况如下:
* 情况1:left的平衡因子d[left]=1,将p右旋
* 情况2:left的平衡因子d[left]=0,将p右旋
* 情况3:left的平衡因子d[left]=-1,先左旋left,再右旋p
* 2,删除左子树,即d[p]=-2的情况,与d[p]=2对称
*
* 删除算法是递归的,所以该方法是在递归中调用的
* @param p
* @return
*/
private AVLEntry<K, V> fixAfterDeletion(AVLEntry<K, V> p) {
if (p == null) return null;
else{
p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
int d = getHeight(p.left) - getHeight(p.right);
if (d == 2) { //说明p.left一定不为null
if (getHeight(p.left.left) - getHeight(p.left.right) >= 0) {
p = rotateRight(p);
}else{
p = firstLeftThenRight(p);
}
} else if (d == -2) {//说明p.right一定不为null
if (getHeight(p.right.right) - getHeight(p.right.left) >= 0) {
p = rotateLeft(p);
}else{
p = firstRightThenLeft(p);
}
}
return p;
}
}
源码:
https://github.com/xiaojinwei/java-learning/blob/master/src/com/cj/learn/tree/avl/AVLMap.java