写一棵AVL
一、Node结点定义
可以自行设计
自己在这里仅增加了一项——平衡因子的记录,用于旋转时判断类型
public class Node {
int val;
int balance; // 左子树高 - 右子树高
Node left;
Node right;
public Node(int val) {
this.val = val;
}
public Node(int val, Node left, Node right) {
this.val = val;
this.left = left;
this.right = right;
}
}
二、逐步实现AVL
先定义一个类,定义出root结点,
此外,还定义了个保护结点,由于AVL旋转后根结点可能改变,root结点为其左儿子
以及一个HashSet
用以O(1)
判断防重,空间换时间
public class MyAVL {
Node root;
private Node protect; // 由于AVL需要旋转,则设置一个保护结点
private static final int protectVal = 0;
private HashSet<Integer> set;
public MyAVL(int rootVal) {
this.root = new Node(rootVal);
this.protect = new Node(protectVal, this.root, null);
this.set = new HashSet<>();
this.set.add(rootVal);
}
public MyAVL() {
this.protect = new Node(protectVal);
this.set = new HashSet<>();
}
}
1、BST
1)insert(int insertVal)方法
可以先练习这道算法题:LeetCode 701 二叉搜索树中的插入操作
HashSet
防重,则不会出现相同的val
- 若
insertVal < curNode.val
则往左子树走 - 若
insertVal > curNode.val
则往右子树走
递归实现:代码简洁,可读性高,易于维护
迭代实现:空间发杂度较低
这里自己使用了迭代实现,并且区分了内部外部方法
public boolean insert(int insertVal) {
if (this.set.contains(insertVal))
return false;
return insert(this.protect, this.root, insertVal, true);
}
// @param fa 此次插入结点的父结点,由于是迭代实现,需要记录父结点来实现插入
// @param cur 用于找到需要插入的位置,当cur为null时则找到了
// @param insertLeft 此次插入结点是其父结点的哪个儿子?初始假设为左儿子
private boolean insert(Node fa, Node cur, int insertVal, boolean insertLeft) {
// 找到插入位置
while (cur != null) {
fa = cur;
if (cur.val < insertVal) {
cur = cur.right;
insertLeft = false;
} else {
cur = cur.left;
insertLeft = true;
}
}
// 插入操作
if (insertLeft) fa.left = new Node(insertVal);
else fa.right = new Node(insertVal);
// 加入set
this.set.add(insertVal);
// 若初始化没有定义root
if (this.root == null)
this.root = this.protect.left;
}
2)remove(int removeVal)方法
可以先练习这道算法题:LeetCode 450 删除二叉搜索树中的结点
HashSet
判断removeVal
是否为有效的值- 若
removeVal < curNode.val
则往左子树走 - 若
removeVal > curNode.val
则往右子树走 - 若
removeVal == curNode.val
则进行删除: - 1、欲删除结点左右子树均空:直接删除
- 2、欲删除结点左右子树有一个为空:删除并以其非空子树取代
- 3、欲删除结点左右子树均非空:将当前结点替换为其前驱或后驱,并删除其前驱或后驱
递归实现:代码简洁,可读性高,易于维护
迭代实现:空间发杂度较低
这里自己仍使用迭代实现,以后驱代替欲删除结点,但仅是值的替换
public boolean remove(int removeVal) {
if (!set.contains(removeVal))
return false;
return remove(this.protect, this.root, removeVal, true);
}
// 其形参解释与insert()方法相同
// @param deleteLeft 初始时假设删除的是左节点
private boolean remove(Node fa, Node cur, int removeVal, boolean deleteLeft) {
// 找到删除位置
while (cur.val != removeVal) {
fa = cur;
if (cur.val < removeVal) {
cur = cur.right;
deleteLeft = false;
} else {
cur = cur.left;
deleteLeft = true;
}
}
// 删除操作
if (cur.left == null && cur.right == null) { // 都null
if (deleteLeft) fa.left = null;
else fa.right = null;
} else if (cur.left == null || cur.right == null) { // 一边null
if (deleteLeft) fa.left = cur.left == null ? cur.right : cur.left;
else fa.right = cur.left == null ? cur.right : cur.left;
} else { // 都不null,找后驱,此处是修改结点的值
Node nextDelete = cur.right;
fa = cur; // 此时fa的定义修改为:nextDelete的父结点
while (nextDelete.left != null) {
fa = nextDelete;
nextDelete = nextDelete.left;
}
if (fa.right == nextDelete) // 特殊情况:欲删除结点的后驱就是cur的右儿子
fa.right = nextDelete.right;
else // 一般情况:欲删除结点的后驱是 欲删除结点右儿子 的最左儿子
fa.left = nextDelete.right;
cur.val = nextDelete.val;
}
// 从set移除
this.set.remove(removeVal);
}
2、计算平衡因子函数calBalance()
可以先练习这道算法题:LeetCode 110 平衡二叉树
1、对于每一次计算而言,只需要旋转第一个不平衡结点即可,所以设置了全局变量
rotateFinished
来保证只需旋转一次,并且可用于提前返回
2、对于左右子树高度差大于1的结点则要调取rotate()
方法
3、对于形参fa_cur_isleftChild
,由于rotate()
可能导致当前结点的结构变化,所以需要存储一下,当前结点cur
与父结点fa
的关系
4、另外,由于旋转完毕之后root可能发生了改变,所以要重新获取root
// 做好旋转前的准别工作,并调用计算平衡因子的方法
private void calBalanceInit() {
this.rotateFinished = false;
calBalance(this.protect, this.root, true);
}
// @param fa_cur_isleftChild 反映了cur是否是fa的左儿子
private int calBalance(Node fa, Node cur, boolean fa_cur_isleftChild) {
if (cur == null)
return 0;
int l = calBalance(cur, cur.left, true);
int r = calBalance(cur, cur.right, false);
if (Math.abs(l - r) > 1) {
if (fa_cur_isleftChild) fa.left = rotate();
else fa.right = rotate();
// 由于旋转完毕之后root可能发生了改变,所以要重新赋予root
this.root = this.protect.left;
}
cur.balance = l - r;
return Math.max(l, r) + 1;
}
3、旋转方法rotate()
1)LL,RR的单旋
LL:第一个L指不平衡结点的左子树高度大于右子树高度,第二个L指其左子结点的左子树高度大于右子树高度
RR:第一个R指不平衡结点的右子树高度大于左子树高度,第二个R指其右子节点的右子树高度大于左子树高度
于是rotate()
函数便设计了四个形参
// @param cur_child_isLeft 不平衡结点是否是左子树不平衡,或者是child结点是否是cur的左儿子
// @param child_isLeft child结点是否是左子树高度大于右子树
private Node rotate(Node cur, Node child,
boolean cur_child_isLeft, boolean child_isLeft) {
if (cur_child_isLeft && child_isLeft) { // LL
cur.left = child.right;
child.right = cur;
} else if (!cur_child_isLeft && !child_isLeft) { // RR
cur.right = child.left;
child.left = cur;
}
return child;
}
3)LR,RL的双旋
LR:第一个L指不平衡结点的左子树高度大于右子树高度,第二个R指其左子结点的右子树高度大于左子树高度
RR:第一个R指不平衡结点的右子树高度大于左子树高度,第二个R指其右子节点的左子树高度大于右子树高度
LR可以视作grandchild
与child
之间的一次RR旋转 + grandchild
与cur
之间的一次LL旋转
RL可以视作grandchild
与child
之间的一次LL旋转 + grandchild
与cur
之间的一次RR旋转
所以直接递归实现即可
// @param cur_child_isLeft 不平衡结点是否是左子树不平衡,或者是child结点是否是cur的左儿子
// @param child_isLeft child结点是否是左子树高度大于右子树
private Node rotate(Node cur, Node child,
boolean cur_child_isLeft, boolean child_isLeft) {
if (cur_child_isLeft && child_isLeft) { // LL
cur.left = child.right;
child.right = cur;
} else if (!cur_child_isLeft && !child_isLeft) { // RR
cur.right = child.left;
child.left = cur;
} else if (cur_child_isLeft && !child_isLeft) { // LR
cur.left = rotate(child, child.right, false, false); // RR
return rotate(cur, cur.left, true, true); // LL
} else { // RL
cur.right = rotate(child, child.left, true, true); // LL
return rotate(cur, cur.right, false, false); // RR
}
return child;
}
3) 更新calBalance()
// rotateFinished 由于只需要第一个不平衡的结点旋转,用一个bool来控制是否已经旋转完毕
// @param fa_cur_isleftChild 反映了cur是否是fa的左儿子
private int calBalance(Node fa, Node cur, boolean fa_cur_isleftChild) {
if (cur == null || this.rotateFinished) // 增加强行终止
return 0;
int l = calBalance(cur, cur.left, true);
int r = calBalance(cur, cur.right, false);
if (!this.rotateFinished && Math.abs(l - r) > 1) {
this.rotateFinished = true;
Node child = l > r ? cur.left : cur.right;
Node finished = rotate(cur, child, l > r, child.balance > 0);
if (fa_cur_isleftChild) fa.left = finished;
else fa.right = finished;
// 由于旋转完毕之后root可能发生了改变,所以要重新赋予root
this.root = this.protect.left;
}
cur.balance = l - r;
return Math.max(l, r) + 1;
}
另外在insert()
函数和delete()
函数结束时调用计算平衡因子的方法
三、AVL源码
public class MyAVL {
Node root;
private Node protect; // 由于AVL需要旋转,则设置一个保护结点
private static final int protectVal = 0;
private HashSet<Integer> set;
private boolean rotateFinished;
public MyAVL(int rootVal) {
this.root = new Node(rootVal);
this.protect = new Node(protectVal, this.root, null);
this.set = new HashSet<>();
this.set.add(rootVal);
}
public MyAVL() {
this.protect = new Node(protectVal);
this.set = new HashSet<>();
}
public boolean insert(int insertVal) {
if (this.set.contains(insertVal))
return false;
return insert(this.protect, this.root, insertVal, true);
}
// @param insertLeft 假设初始时插入的是左结点
private boolean insert(Node fa, Node cur, int insertVal, boolean insertLeft) {
// 找到插入位置
while (cur != null) {
fa = cur;
if (cur.val < insertVal) {
cur = cur.right;
insertLeft = false;
} else {
cur = cur.left;
insertLeft = true;
}
}
// 插入操作
if (insertLeft) fa.left = new Node(insertVal);
else fa.right = new Node(insertVal);
// 加入set
this.set.add(insertVal);
// 若初始化没有加入root
if (this.root == null)
this.root = this.protect.left;
// ----此时插入已经完成----
// 计算平衡因子并旋转
calBalanceInit();
return true;
}
public boolean remove(int removeVal) {
if (!set.contains(removeVal))
return false;
return remove(this.protect, this.root, removeVal, true);
}
// @param deleteLeft 初始时假设删除的是左节点
private boolean remove(Node fa, Node cur, int removeVal, boolean deleteLeft) {
// 找到删除位置
while (cur.val != removeVal) {
fa = cur;
if (cur.val < removeVal) {
cur = cur.right;
deleteLeft = false;
} else {
cur = cur.left;
deleteLeft = true;
}
}
// 删除操作
if (cur.left == null && cur.right == null) { // 都null
if (deleteLeft) fa.left = null;
else fa.right = null;
} else if (cur.left == null || cur.right == null) { // 一边null
if (deleteLeft) fa.left = cur.left == null ? cur.right : cur.left;
else fa.right = cur.left == null ? cur.right : cur.left;
} else { // 都不null,找后驱,此处是修改结点的值
Node nextDelete = cur.right;
fa = cur; // 此时fa为nextDelete的父结点
while (nextDelete.left != null) {
fa = nextDelete;
nextDelete = nextDelete.left;
}
if (fa.right == nextDelete) // 也就是cur的后驱就是cur的右儿子
fa.right = nextDelete.right;
else // cur的后驱是cur右儿子的最左儿子
fa.left = nextDelete.right;
cur.val = nextDelete.val;
}
// 从set移除
this.set.remove(removeVal);
// ----此时删除已经完成----
// 计算平衡因子并旋转
calBalanceInit();
return true;
}
// 做好旋转前的准别工作,并调用计算平衡因子的方法
private void calBalanceInit() {
this.rotateFinished = false;
calBalance(this.protect, this.root, true);
}
// rotateFinished 由于只需要第一个不平衡的结点旋转,用一个bool来控制是否已经旋转完毕
private int calBalance(Node fa, Node cur, boolean fa_cur_isleftChild) {
if (cur == null || this.rotateFinished) // 增加强行终止
return 0;
int l = calBalance(cur, cur.left, true);
int r = calBalance(cur, cur.right, false);
if (!this.rotateFinished && Math.abs(l - r) > 1) {
this.rotateFinished = true;
Node child = l > r ? cur.left : cur.right;
Node finished = rotate(cur, child, l > r, child.balance > 0);
if (fa_cur_isleftChild) fa.left = finished;
else fa.right = finished;
// 由于旋转完毕之后root可能发生了改变,所以要重新赋予root
this.root = this.protect.left;
}
cur.balance = l - r;
return Math.max(l, r) + 1;
}
private Node rotate(Node cur, Node child,
boolean cur_child_isLeft, boolean child_isLeft) {
if (cur_child_isLeft && child_isLeft) { // LL
cur.left = child.right;
child.right = cur;
} else if (!cur_child_isLeft && !child_isLeft) { // RR
cur.right = child.left;
child.left = cur;
} else if (cur_child_isLeft && !child_isLeft) { // LR
cur.left = rotate(child, child.right, false, false);
return rotate(cur, cur.left, true, true);
} else { // RL
cur.right = rotate(child, child.left, true, true);
return rotate(cur, cur.right, false, false);
}
return child;
}
}