普通二叉查找树存在的缺陷
如上图,如果树的结构是这样一种形式,那么他就会失去二分查找这种高效率的特性。所以基于此才有了AVL树。
AVL树(平衡二叉查找树)
该树的一个特点就是首先他是一个二叉查找树,在此之上,该树中任何一个节点的左右子树的高度差都不超过1。也就是它时刻保持着左右子树的高度平衡。
如何保持高度平衡(旋转)
最熟悉的左旋右旋,左旋可以降低右子树的高度。与之相反右旋可以降低左子树的高度。文字表达太过生硬。左旋右旋如下图。
下面是以节点E为基准的左旋示意图:
如上图这颗树的右子树的高度由2降低为1
下面是以节点S为基准的右旋示意图:
如上图这颗树左子树高度右2降低为1
要正确使用旋转时的AVL树时刻保持高度平衡,首先得知道它的左右子树的高度。可以使用递归法求一颗树的高度。
public class AVLTree {
private TreeNode root;
public AVLTree() {}
//递归求以curNode节点为根的树的高度
private int getMaxDepth(TreeNode curNode) {
if (curNode == null) {
return 0;
}else {
int left = getMaxDepth(curNode.leftChild);
int right = getMaxDepth(curNode.rightChild);
return 1 + Math.max(left, right);
}
}
class TreeNode{
private int value;
private TreeNode leftChild;
private TreeNode rightChild;
public TreeNode(int value) {
this.value = value;
}
}
}
求二叉树宽度的方法:
//求二叉树最大宽度
private int getMaxWidth(TreeNode curNode) {
if (curNode == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
int maxWitdth = 1;
queue.add(curNode);
while (true) {
// 本层的节点个数
int len = queue.size();
//当本层节点数为0时说明树遍历完了
if (len == 0) {
break;
}
// 如果当前层,还有节点
while (len > 0) {
//poll 检索并删除此队列的头,如果此队列为空,则返回 null 。
TreeNode tmp = queue.poll();
len--;
if (tmp.leftChild != null) {
// 下一层节点入队
queue.add(tmp.leftChild);
}
if (tmp.rightChild != null) {
// 下一层节点入队
queue.add(tmp.rightChild);
}
}
maxWitdth = Math.max(maxWitdth, queue.size());
}
return maxWitdth;
}
首先介绍四种需要旋转的情况:
左单旋:
右单旋:
双旋:左右旋
双旋:右左旋
我定义的树节点只有指向其左右孩子的索引,没有指向其父节点的索引。所以要基于此左旋的话可以利用如下方法:
右旋也是一样的道理。
左旋和右旋的代码实现:
//以curNode为基准左旋
private void leftRotate(TreeNode curNode) {
//创建一个新的与当前节点值相同的节点
TreeNode newNode = new TreeNode(curNode.value);
//让新节点的左孩子指向当前节点的左孩子
newNode.leftChild = curNode.leftChild;
//让新节点的右孩子指向当前节点右孩子的左孩子
newNode.rightChild = curNode.rightChild.leftChild;
//将当前节点的值变为它的右孩子的值
curNode.value = curNode.rightChild.value;
//将当前节点的右孩子指向它右孩子的右孩子
curNode.rightChild = curNode.rightChild.rightChild;
//将当前节点的左孩子指向新节点
curNode.leftChild = newNode;
}
//以curNode为基准右旋
private void rightRotate(TreeNode curNode) {
//创建一个新的节点
TreeNode newNode = new TreeNode(curNode.value);
//新节点的左孩子指向当前节点的左孩子的右孩子
newNode.leftChild = curNode.leftChild.rightChild;
//新节点的右孩子指向当前节点的右孩子
newNode.rightChild = curNode.rightChild;
//将当前节点的值替换成其左孩子的值
curNode.value = curNode.leftChild.value;
//当前节点的左孩子指向其左孩子的左孩子
curNode.leftChild = curNode.leftChild.leftChild;
//当前节点的右孩子指向新节点
curNode.rightChild = newNode;
}
基于上述分析,在可以判断其高度和左旋右旋之后。我们应该在添加节点的同时判断如果插入一个新节点会不会打破AVL树的高度平衡,如果打破平衡则可以按照上述几种情况进行相应的旋转来调整高度。添加节点的逻辑如下:
我是利用堆栈来实现的
public void addNode(int value) {
TreeNode newNode = new TreeNode(value);
if(root == null) {
root = newNode;
return;
}
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode curNode = root;
while(curNode != null) {
stack.push(curNode); //当前节点入栈
if(value <= curNode.value) {
if(curNode.leftChild == null) {
curNode.leftChild = newNode;
break;
}
curNode = curNode.leftChild;
}else{
if(curNode.rightChild == null) {
curNode.rightChild = newNode;
break;
}
curNode = curNode.rightChild;
}
}
//添加完之后判断各节点的左右子树高度,进行相应的旋转。
while(!stack.isEmpty()) {
curNode = stack.pop();
//如果当前节点的左子树高度大于右子树高度,则需要右旋(顺时针旋转),降低其左子树高度
if(getMaxDepth(curNode.leftChild) - getMaxDepth(curNode.rightChild) > 1) {
/**
* 如果当前节点左子树的高度大于右子树的高度。
* 以当前节点的左子节点为一个子树的根("子树根"),判断如果该“子树”的右子树高度比左子树高度高
* 则先以该"子树的根"为基准左旋
*/
TreeNode leftChildRoot = curNode.leftChild;
if(leftChildRoot != null && (getMaxDepth(leftChildRoot.rightChild) > getMaxDepth(leftChildRoot.leftChild))) {
leftRotate(leftChildRoot);
}
rightRotate(curNode);
}
//如果当前节点的右子树高度大于左子树高度,则需要左旋(逆时针旋转),降低其右子树高度
if(getMaxDepth(curNode.rightChild) - getMaxDepth(curNode.leftChild) > 1) {
/**
* 如果当前节点右子树的左子树高度大于当前节点右子树的右子树高度。则
* 以当前节点的右子树树根为基准,进行右旋。接着再以当前节点为基准进行左旋
*/
TreeNode rightChildRoot = curNode.rightChild;
if(rightChildRoot != null && (getMaxDepth(rightChildRoot.leftChild) > getMaxDepth(rightChildRoot.rightChild))) {
rightRotate(rightChildRoot);
}
leftRotate(curNode);
}
}
}