介绍
平衡二叉树简称AVL,是改进的二叉搜索树,拥有自平衡特性。
为什么需要AVL?
二叉搜索树出现的目的是为了帮助我们将普通二叉树的查找效率优化。但是可能会存在一种情况,如果深度扩展比较大,那么查找效率很低。
**故此在二叉搜索树的基础上,使树上的节点尽可能的分散一点,而不是"线性"的排列,这样才能更大程度的利用它二分法查找的功能。**由此把这样节点"分散"的二叉搜索树规定了标准,并把拥有这样特性的二叉搜索树叫做平衡二叉树(AVL树)。
特点
在满足二叉搜索树的性质基础上:任何一个节点的左右子树都是平衡二叉树,并且左右子树的高度之差的绝对值不能超过1(平衡因子)。
平衡二叉树的某节点的左右子树的高度差的时候习惯以平衡因子来代称。
平衡因子大小的含义:
- 0:左右子树登高。
- 1:左子树比右子树高1。
- -1:右子树比左子树高1。
平衡二叉树的失衡和调整
什么是失衡
向平衡二叉树插入一个新的节点,导致树中存在节点不满足平衡二叉树特性,则称该平衡二叉树失衡了。并且从新插入节点往上查找,第一个平衡因子的绝对值超过1的节点为根的子树,我们称为最小失衡子树。
调整操作
LL型(右旋)
**根节点的平衡因子>1,并且失衡原因出在根节点的左孩子的左子树,这种失衡属于LL型。**要解决LL型,需要左孩子的左子树上抬,原孩子替代根节点成为新的根节点,而原孩子的子节点也调整成旧根节点的子节点。这种由左边叶子节点向上旋转的方式叫做右旋。
解决LL型的冲突的旋转叫做右旋
java
private Node<T> llRotate(Node<T> avlNode) {
Node<T> node = avlNode.leftChild;
avlNode.leftChild = node.rightChild;
node.rightChild = avlNode;
return node;
}
RR型(左旋)
根节点的平衡因子>1,并且失衡原因出在根节点的右子节点的右子树,这种失衡属于RR型。解决RR型的冲突的旋转叫做左旋。
java
private Node<T> rrRotate(Node<T> avlNode) {
Node<T> node = avlNode.rightChild;
avlNode.rightChild = node.leftChild;
node.leftChild = avlNode;
return node;
}
LR型
根节点的平衡因子>1的,并且失衡原因出在根节点的左子节点的右子节点的子树。这种失衡情况叫做LR型。要解决这种失衡,需要先进行右旋,再执行左旋。
大致步骤就是把左子节点的右子节点提升为根节点,原有根节点下移,再把新根节点的左右叶子节点分配给自己的两个子节点。
java
// 等同于先对节点的左孩子为根进行RR调整,再对该节点进行LL调整
private Node<T> lrRotate(Node<T> avlNode) {
avlNode.leftChild = rrRotate(avlNode.leftChild);
return llRotate(avlNode);
}
RL型
根节点的平衡因子>1的,并且失衡原因出在根节点的右子节点的左子节点的子树。这种失衡情况叫做RL型。要解决这种失衡,需要先进行左旋,再执行右旋。
java
// 等同于先对节点的右孩子为根进行LL调整,再对该节点进行RR调整
private Node<T> rlRotate(Node<T> avlNode) {
avlNode.rightChild = llRotate(avlNode.rightChild);
return rrRotate(avlNode);
}
平衡二叉树的编码
平衡二叉树节点的定义
java
public class Node<T extends Comparable<T>> {
/**
* 数据域
*/
public T data;
/**
* 节点子树的高度,依据此计算平衡因子
*/
public int height;
/**
* 左孩子
*/
public Node<T> leftChild;
/**
* 右孩子
*/
public Node<T> rightChild;
public Node(T data) {
this(data, null, null);
}
public Node(T data, Node<T> leftChild, Node<T> rightChild) {
this.data = data;
this.leftChild = leftChild;
this.rightChild = rightChild;
}
}
计算节点深度
java
private int getAvlTreeHeight(Node<T> node) {
if (node == null) {
return 0;
} else {
int leftWeight = getAvlTreeHeight(node.leftChild);
int rightWeight = getAvlTreeHeight(node.rightChild);
return Math.max(leftWeight, rightWeight) + 1;
}
}
插入节点操作
java
//平衡调整
private Node<T> balance(Node<T> node) {
// 左子树比右子树高度大于1以上
if (getAvlTreeHeight(node.leftChild) - getAvlTreeHeight(node.rightChild) > 1) {
if (getAvlTreeHeight(node.leftChild.leftChild) >= getAvlTreeHeight(node.leftChild.rightChild)) {
// 执行LL型调整
node = llRotate(node);
} else {
// 执行LR型调整
node = lrRotate(node);
}
// 右子树比左子树高度大于1以上
} else if (getAvlTreeHeight(node.rightChild) - getAvlTreeHeight(node.leftChild) > 1) {
if (getAvlTreeHeight(node.rightChild.rightChild) >= getAvlTreeHeight(node.rightChild.leftChild)) {
// 执行RR型调整
node = rrRotate(node);
} else {
// 执行RL型调整
node = rlRotate(node);
}
}
return node;
}
private Node<T> insert(Node<T> root, T data) {
// 根节点为空,说明树为空,则创建一颗树
if (root == null) {
return new Node<>(data);
} else {
//左子树比右子树高1
if (compare(root, data) > 0) {
root.leftChild = insert(root.leftChild, data);
root = balance(root);
//右子树比左子树高1
} else if (compare(root, data) < 0) {
root.rightChild = insert(root.rightChild, data);
root = balance(root);
}
return root;
}
}
建树操作
java
public void createTree(T data) {
if (data == null) {
return;
}
root = insert(root, data);
}
删除节点操作
java
public Node<T> remove(Node<T> root, T data){
if(root ==null){
return null;
}
int result=data.compareTo(root.data);
//从左子树查找需要删除的元素
if(result<0){
root.leftChild=remove(root.leftChild, data);
root=balance(root);
}
//从右子树查找需要删除的元素
else if(result>0){
root.rightChild=remove(root.rightChild, data);
//检测是否平衡
root=balance(root);
}
//已找到需要删除的元素,并且要删除的结点拥有两个子节点
else if(root.rightChild!=null&&root.leftChild!=null){
//寻找替换结点
root.data=findMin(root.rightChild).data;
//移除用于替换的结点
root.rightChild = remove(root.rightChild ,root.data);
} else {
//只有一个孩子结点或者只是叶子结点的情况
root = (root.leftChild!=null)? root.leftChild:root.rightChild;
}
//更新高度值
if(root!=null){
root.height = Math.max(getAvlTreeHeight(root.leftChild), getAvlTreeHeight(root.rightChild)) + 1;
}
return root;
}
/**
* 查找最小值结点
* @param root 根
* @return
*/
private Node<T> findMin(Node<T> root){
if (root==null)//结束条件
return null;
else if (root.leftChild==null)//如果没有左结点,那么t就是最小的
return root;
return findMin(root.leftChild);
}
节点的类型有三种:
- 叶子节点;
- 只有左子树或只有右子树;
- 既有左子树又有右子树。
删除后调整:
- **当删除的节点是叶子节点: 将节点删除,**然后从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,此时到根节点还发现没有失衡,则说此时树是平衡的;如果中间过程发现失衡,则判断属于哪种类型 的失衡(左左,左右,右左,右右),然后进行调整。
- 删除的节点只有左子树或只有右子树: 这种情况其实就比删除叶子节点的步骤多一步,就是将节点删除,然后把仅有一支的左子树或右子树替代原有结点的位置,后面的步骤就一样了,从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,如果中间过程发现失衡,则根据失衡的类型进行调整。
- 删除的节点既有左子树又有右子树: 这种情况又比上面这种多一步,**就是中序遍历,找到待删除节点的前驱或者后驱都行(前驱为该节点左子树权最大节点,后驱为该节点右子树最小节点),然后与待删除节点互换位置,然后把待删除的节点删掉,**后面的步骤也是一样,判断是否失衡,然后根据失衡类型进行调整。
遍历
一种是深度优先遍历也就是我们常说的 DFS,另一种是广度优先遍历我们常用 BFS 来称呼。深度优先遍历实现的方法有俩种,一种是递归还有一种是迭代,而广度优先遍历则是利用队列来实现的,我们称之为层序遍历。
深度优先遍历又分为前序遍历,中序遍历和后序遍历。
广度遍历也就是层序遍历。
前序遍历
Java
class Solution {
public List<Integer> res;
//递归法
public List<Integer> preOrderTraveral(TreeNode root){
if (root == null) return new ArrayList();
res = new ArrayList();
// 前序
preOrder(root);
return res;
}
public void preOrder(TreeNode root){
if (root == null) return;
res.add(root.val); // 中
preOrder(root.left); // 左
preOrder(root.right); // 右
}
}
//迭代法
public List<Integer> preorderTraversal(TreeNode root) {
if (root == null) return new ArrayList<Integer>();
List<Integer> list = new ArrayList();
Stack<TreeNode> stack = new Stack();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
list.add(node.val);
// 关键代码
if(node.right != null) stack.push(node.right);
if(node.left != null) stack.push(node.left);
}
return list;
}
中序遍历
Java
//递归法
class Solution {
public List<Integer> res;
public List<Integer> inOrderTraveral(TreeNode root){
if (root == null) return new ArrayList();
res = new ArrayList();
// 中序
inOrder(root);
return res;
}
public void inOrder(TreeNode root){
if (root == null) return;
preOrder(root.left); // 左
res.add(root.val); // 中
preOrder(root.right); // 右
}
}
//迭代法
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null) return new ArrayList();
List<Integer> list = new ArrayList();
Stack<TreeNode> stack = new Stack();
TreeNode curr = root;
while(curr != null || !stack.isEmpty()){
// 关键代码
if(curr != null){
stack.push(curr);
curr = curr.left;
}else {
TreeNode node = stack.pop();
list.add(node.val);
curr = node.right;
}
}
return list;
}
后序遍历
Java
//递归法
class Solution {
public List<Integer> res;
public List<Integer> postOrderTraveral(TreeNode root){
if (root == null) return new ArrayList();
res = new ArrayList();
// 后序
postOrder(root);
return res;
}
public void postOrder(TreeNode root){
if (root == null) return;
preOrder(root.left); // 左
preOrder(root.right); // 右
res.add(root.val); // 中
}
}
//迭代法
public List<Integer> preorderTraversal(TreeNode root) {
if (root == null) return new ArrayList<Integer>();
List<Integer> list = new ArrayList();
Stack<TreeNode> stack = new Stack();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
list.add(node.val);
// 关键代码
if(node.left != null) stack.push(node.left);
if(node.right != null) stack.push(node.right);
}
return Collections.reverse(list);
}
层次遍历
层序遍历二叉树的方式,就是从左到右,一层一层的去遍历二叉树。这时候,就需要借助队列,队列先进先出的特性符合一层一层的去遍历二叉树。
java
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ret = new ArrayList<List<Integer>>();
if (root == null) {
return ret;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> level = new ArrayList<Integer>();
int currentLevelSize = queue.size();
for (int i = 1; i <= currentLevelSize; ++i) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
ret.add(level);
}
return ret;
}
总结
平衡二叉树是高度平衡的二叉树,查询的时间复杂度是O(logN) 。插入的话如果失衡则进行四种调整,最多也只需要调整2次,所以插入的时间复杂度是O(1) 。平衡二叉树的问题在于删除处理,删除节点可能失衡,导致需要从删除节点的父节点开始,不断回溯到根节点,如果平衡二叉树很高的话则需要判断多个节点。