在这里我整理了一下我之前学的AVL树的具体实现,还有就是后面会红黑树的具体实现通过Java代码来实现一下
AVL树是什么呢?
AVL树是一种自平衡的二叉树,也就是他避免了搜索二叉树遇到的问题,如果大量的数据在插入的时候给定的数字分布不均匀搜索二叉树很容易蜕变成链表,降低自己本身性能,还有一个很重要的性质就是任意一个节点,左子树和右子树的差不能够超过1。
有一个很重要的定义就是,平衡因子,什么叫做平衡因子呢?就是左子树的的高度-右子树的高度。如果平衡因子>2就表明这个树是不平衡的需要去进行一次转换。
旋转也是AVL树最难得部分,我在这里进行一次叙述。
什么叫右旋?就是当z插入的时候,y的平衡因子变成了2的时候,这个时候树是不平衡,所以我们要进行右旋。这里的T1234代表是一些子树,不只是代表一颗而是代表了很多的颗树。
左旋跟右旋是相反的,当y点的平衡因子从-1变成-2的时候,我们就要开始左旋。
对于这一个复杂的旋转我们是先对于x进行左旋,注意是对于x进行左变成了图二的这一种形状,这个形状和右转是一样的,我们在进行右旋。
我们还是对于这个图形先进性右转再进行左转,最终实现变化,我们完成了把这个变成了平衡的状态。
因为是实现了一颗AVL树所以我们要去烤炉每一节点的元素都有什么,首先是因为树,所以会有左右子树,我们一般是使用这个数据结构来进行存储一个map的,所以我们要要有他的键值对,最后的话我们还要去记录一下这颗树的高度。
private class Node{
public K key;
public V value;
public Node left, right;
public int height;
public Node(K key, V value){
this.key = key;
this.value = value;
left = null;
right = null;
height = 1;
}
}
包含了我之前说的一棵树的左右子树,还有键值对,还有就是一个记录高度的值。
还有就是在类中声明了呀个Node节点的root,还有一个是树的高度。
private Node root;
private int size;
public AVLTree(){
root = null;
size = 0;
}
public int getSize(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
// 判断该二叉树是否是一棵二分搜索树
public boolean isBST(){
ArrayList keys = new ArrayList<>();
inOrder(root, keys);
for(int i = 1 ; i < keys.size() ; i ++)
if(keys.get(i - 1).compareTo(keys.get(i)) > 0)
return false;
return true;
}
private void inOrder(Node node, ArrayList keys){
if(node == null)
return;
inOrder(node.left, keys);
keys.add(node.key);
inOrder(node.right, keys);
}
这些方法主要是拿到长度和判断是否为空。还有一个树中序遍历的还有就是判断这个树属否是二叉搜索树。
// 判断该二叉树是否是一棵平衡二叉树
public boolean isBalanced(){
return isBalanced(root);
}
// 判断以Node为根的二叉树是否是一棵平衡二叉树,递归算法
private boolean isBalanced(Node node){
if(node == null)
return true;
int balanceFactor = getBalanceFactor(node);
if(Math.abs(balanceFactor) > 1)
return false;
return isBalanced(node.left) && isBalanced(node.right);
}
// 获得节点node的高度
private int getHeight(Node node){
if(node == null)
return 0;
return node.height;
}
// 获得节点node的平衡因子
private int getBalanceFactor(Node node){
if(node == null)
return 0;
return getHeight(node.left) - getHeight(node.right);
}
判断我们是否需要去是否这一棵树需要去变化,就是通过判断平衡因子,平衡因子就是相当于左子树的的高度-右子树的高度,如果这个值大于1就说明这个点是不平衡的我们就要去进行旋转。
// 对节点y进行向右旋转操作,返回旋转后新的根节点x
// y x
// / \ / \
// x T4 向右旋转 (y) z y
// / \ - - - - - - - -> / \ / \
// z T3 T1 T2 T3 T4
// / \
// T1 T2
private Node rightRotate(Node y) {
Node x = y.left;
Node T3 = x.right;
// 向右旋转过程
x.right = y;
y.left = T3;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
// 对节点y进行向左旋转操作,返回旋转后新的根节点x
// y x
// / \ / \
// T1 x 向左旋转 (y) y z
// / \ - - - - - - - -> / \ / \
// T2 z T1 T2 T3 T4
// / \
// T3 T4
private Node leftRotate(Node y) {
Node x = y.right;
Node T2 = x.left;
// 向左旋转过程
x.left = y;
y.right = T2;
// 更新height
y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1;
x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1;
return x;
}
就拿左旋转为例,我们在这里使用一个节点x来承接y的右子树,使用一个节点T2来承接x的左子树,这样的操作以后我们就可以对y的右子树和x的左子树进行操作了,我们将y连接在x的左子树上,将之前的x的左子树放在y的右子树上,然后只需要去更新每一棵树的高度就可完成了,这就是使用代码来完成左转的代码的解释。
就是我们会遇到的四中的插入的模式,左转,右转,左右转,右左转。
// 向二分搜索树中添加新的元素(key, value)
public void add(K key, V value){
root = add(root, key, value);
}
// 向以node为根的二分搜索树中插入元素(key, value),递归算法
// 返回插入新节点后二分搜索树的根
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value);
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else // key.compareTo(node.key) == 0
node.value = value;
// 更新height
node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
// 计算平衡因子
int balanceFactor = getBalanceFactor(node);
// 平衡维护
if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0)
return rightRotate(node);
if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0)
return leftRotate(node);
if (balanceFactor > 1 && getBalanceFactor(node.left) < 0) {
node.left = leftRotate(node.left);
return rightRotate(node);
}
if (balanceFactor < -1 && getBalanceFactor(node.right) > 0) {
node.right = rightRotate(node.right);
return leftRotate(node);
}
return node;
}
这里面的左右转和右左转使用的不是固定的方法,而是通过我们将方法的组合完成的,并没有去完成专门的方法,我自己感觉得话这么做有好有坏,好消息就是我们可以充分地实现代码的复用,减少我们对于代码的得重复的书写,但是也会出现一个问题,如果我们最基础的带啊做出修改或者出现bug我们就会面临数不清的问题。
这也让我们在以后的写代码要注意书写的规范,注意注释代码和注意代码复用之前先去保障代码的健壮性。
// 从二分搜索树中删除键为key的节点
public V remove(K key){
Node node = getNode(root, key);
if(node != null){
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key){
if( node == null )
return null;
Node retNode;
if( key.compareTo(node.key) < 0 ){
node.left = remove(node.left , key);
retNode= node;
}
else if(key.compareTo(node.key) > 0 ){
node.right = remove(node.right, key);
retNode= node;
}
else{ // key.compareTo(node.key) == 0
// 待删除节点左子树为空的情况
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
retNode= rightNode;
}
// 待删除节点右子树为空的情况
else if(node.right == null){
Node leftNode = node.left;
node.left = null;
size --;
retNode= leftNode;
}
// 待删除节点左右子树均不为空的情况
// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
else {// 用这个节点顶替待删除节点的位置
Node successor = minimum(node.right);
successor.right = remove(node.right,successor.key);//避免之前的没有对于平衡性的维护
successor.left = node.left;
node.left = node.right = null;
retNode= successor;
}
}
if(retNode==null) {
return null;
}
// 更新height
retNode.height = 1 + Math.max(getHeight(retNode.left), getHeight(retNode.right));
// 计算平衡因子
int balanceFactor = getBalanceFactor(retNode);
// 平衡维护
//LL
if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0)
return rightRotate(retNode);
//RR
if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0)
return leftRotate(retNode);
if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0) {
retNode.left = leftRotate(retNode.left);
return rightRotate(retNode);
}
if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0) {
retNode.right = rightRotate(retNode.right);
return leftRotate(retNode);
}
return retNode;
}
删除也是一个样子的事情,我们将元素删除的时候也要去考虑一下平衡因子的问题,一旦平衡因子的绝对值>1的话我们既要判断形状进行变换位置了。
我们在这里去使用我们之前读文章的代码,我们根据单词里面的东西存储到树中
package AVLTree;
import java.util.ArrayList;
import java.util.Collections;
import SetAndMap.FileOperation;
public class AVLmain {
public static void main(String[] args) {
System.out.println("Pride and Prejudice");
ArrayList words = new ArrayList<>();
if(FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.size());
Collections.sort(words);
// Test BST
long startTime = System.nanoTime();
BST bst = new BST<>();
for (String word : words) {
if (bst.contains(word))
bst.set(word, bst.get(word) + 1);
else
bst.add(word, 1);
}
for(String word: words)
bst.contains(word);
long endTime = System.nanoTime();
double time = (endTime - startTime) / 1000000000.0;
System.out.println("BST: " + time + " s");
// Test AVL Tree
startTime = System.nanoTime();
AVLTree avl = new AVLTree<>();
for (String word : words) {
if (avl.contains(word))
avl.set(word, avl.get(word) + 1);
else
avl.add(word, 1);
}
for(String word: words)
avl.contains(word);
endTime = System.nanoTime();
time = (endTime - startTime) / 1000000000.0;
System.out.println("AVL: " + time + " s");
}
System.out.println();
}
}
这就是我是用来判断时间复杂度的代码,下面是我运行的结果。
在这里大大的显示出,BSTtree和AVLtree的差距,我们也是能够体现出现深深地差距,也是证明了AVL树的在插入和删除的时候回注重平衡的这一特点。