常用的查找算法简介以及AVL树

平衡二叉搜索树——AVL树

——平衡二叉搜索树(Self-balancing binary search tree)

Ⅰ 搜索/查找——相关的算法及数据结构
(1)遍历查找

比较方式:equals()
时间复杂度:O(n)

public static int Search(int[] a, int key) {
	for (int i = 0; i < a.length; i++) {
		if (a[i] == key)
			return i;
	}
	return -1;
}
(2)二分查找

比较方式:compare()
时间复杂度:O(log n)

对数据要求强约束:

  • 必须是数组(采用顺序存储)
  • 数据必须有序

二分查找是基于比较的查找(小于中间值的往左边找,大于中间值的往右边找),只适合于静态数据(数据不会发生变化),因为插入或者删除可能会破坏数组的有序性,而为了维护约束往往需要付出O(n)的时间复杂度。


public static int binarySearch(int[] a, int key) {
	int low, mid, high;
	low = 0;// 最小下标
	high = a.length - 1;// 最大小标
	while (low <= high) {
		mid = (high + low) / 2;// 中间下标
		if (key > a[mid]) {
			low = mid + 1; // 关键字比中间值大
		} else if (key < a[mid]) {
			high = mid - 1;// 关键字比中间值小
		} else {
			return mid; // 当 key == a[mid] 返回目标值
		}
	}
	return -1;
}
(3)适合动态数据的查找算法(能够很好的支持insert/update/delete)
1)哈希表

HashMap/HashSet
比较方式:equals()
时间复杂度:O(1)

2)搜索树(平衡搜索树)

TreeMap/TreeSet
比较方式:compare()
时间复杂度:O(log n)

3)分组查找(Redis的跳表可以看作是这种结构的变形)

在这里插入图片描述
要求A<B<C,查找时可以很快找到在哪个组中查找,组内部的数据维护的比较松散(通过牺牲一定的查找性能,换来增删改性能的提升。)

Ⅱ 二叉搜索树

二叉搜索树又称二叉排序树,它是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树
    在这里插入图片描述

二叉搜索树具有以下特性:

  • 二叉搜索树中最左侧的节点是树中最小的节点,最右侧节点一定是树中最大的节点
  • 采用中序遍历遍历二叉搜索树,可以得到一个有序的序列

二叉搜索树最主要的作用是进行查询,插入和删除操作,也都是建立在查找的基础之上的。
在这里插入图片描述
总能在node==null的位置找到插入位置。

二叉树查询性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
在这里插入图片描述
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:O(log(n))
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:O(n)
如果退化成单支树,二叉搜索树的性能就失去了,所以需要对其进行改进。

Ⅲ AVL树——平衡二叉搜索树

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序,二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

一棵AVL树是空树,或者是具有以下性质的二叉搜索树:

  • 它的左右子树都是AVL树,左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
    在这里插入图片描述
    如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O(log n),搜索时间复杂度O(log n)
(1) AVL树性能分析

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(log(n))。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合

(2) AVL树节点的定义

为了AVL树实现简单,AVL树节点在定义时维护一个平衡因子,具体节点定义如下:

class AVLTreeNode
{
   public AVLTreeNode(int val)
  {
       this.val = val;
  }
   public AVLTreeNode left = null;    // 节点的左孩子
   public AVLTreeNode right = null;   // 节点的右孩子
   public AVLTreeNode parent = null;  // 节点的双亲
   public int val = 0;
   public int bf = 0;   // 当前节点的平衡因子
}

平衡因子:BF(node) = H(node.left) - H(node.right)

(3) AVL树的查找:

和普通搜索树的查找方式相同。

(4) AVL树的插入:

插入之前:整棵树满足AVL的特征
插入期间:AVL的特征可能会被破坏掉,所以在插入过程中需要修复AVL树的特征
插入之后:整棵树满足AVL的特征

AVL树就是在二叉搜索树的基础上引入了平衡因子,因此AVL树也可以看成是二叉搜索树。那么AVL树的插入过程可以分为两步:

1). 按照二叉搜索树的方式插入新节点

在这里插入图片描述

2). 调整节点的平衡因子

在这里插入图片描述
在这里插入图片描述
更新后我们需要考虑两个点:

  • parent的BF值是否破坏了AVL树的特征
  • parent树的高度H(parent)是否发生了变化

情况一: 插入后BF=0,AVL特征没有被破坏,所以本次插入结束。

情况二: 插入后BF=1或-1,BF满足AVL,但是H(parent)的高度发生了变化,所以调节过程要向根的方向蔓延。
在这里插入图片描述
情况三: BF=2或-2,BF(parent)不满足AVL树,发生了失衡(在首次调节的过程中,或蔓延的过程中发生了失衡)。
在这里插入图片描述
此时需要对红色范围内的节点进行调整,使树满足AVL,高度变为原来的高度。

AVL树的旋转(情况三)
如果在一棵原本是平衡的AVL树中插入一个新节点,造成了不平衡,此时必须调整树的结构,使之平衡化。根据节点插入位置的不同,AVL树的旋转分为四种:

1)左左(插入点.parent在失衡点的左边,插入点在parent的左边):右旋
在这里插入图片描述
2)左右(插入点.parent在失衡点的左边,插入点在parent的右边):先左旋再右旋
在这里插入图片描述
3) 右右(插入点.parent在失衡点的右边,插入点在parent的右边):左旋
在这里插入图片描述
4) 右左(插入点.parent在失衡点的右边,插入点在parent的左边):先右旋再左旋
在这里插入图片描述

3) 失衡修复后平衡因子的计算

左左失衡:
在这里插入图片描述
假设A子树是插入点,R节点是失衡点,
左左失衡BF®=2,即H(L)-H(C) =2;设H(C) = x; 则H(L) = x+2;
因为(H(L) = max(H(A),H(B))+1),那么A子树和B子树一定有一颗树的高度为x+1; 如果B子树的高度为x+1,那么L树在A子树插入前就是x+2了,所以不成立。

推出H(A)=x+1; H(B)=x, 因为若H(B)=x-1,那么失衡点就是L了,而不会是R了。
在这里插入图片描述
推出A,B,C子树的高度,即可得到失衡点和parent的BF了,即BF(R)=0; BF(L)=0。

左右失衡:
要求得各个节点的BF是多少,知道A,B,C,D四颗子树的高度即可。
在这里插入图片描述
左右失衡,BF(R) = 2;
设H(D) =x;则H(L) = x+2
因为(H(L) = max(H(A),H(P))+1),那么A子树和B子树一定有一颗树的高度为x+1;
如果A子树的高度为x+1,那么L树在P子树插入前就是x+2了,所以不成立。推出H(P)=x+1;
H(A)=x。因为若H(A)=x-1,那么失衡点就是L了,而不会是R了。
1)如果插入点来自B子树,H(B)=x, H(C) = x-1;
2)如果插入点来自C子树,H(C)=x, H(B) = x-1;
3)如果插入点来自P子树,H(C)=0, H(B) = 0;x=0;

  • BF(P)==1,插入点来自B子树,H(B)=x, H(C) = x-1; 此时H(A,B,D)=X, H(C)=x-1,则H(L)=x+1, H(R) = x+1,
    推出BF(L) =0, BF(R)=-1, BF(P) = 0;
  • BF(P)==-1,插入点来自C子树,H(C)=x, H(B) = x-1;此时H(A,C,D)=X, H(B)=x-1,则H(L)=x+1, H(R) = x+1,
    推出BF(L) =1, BF(R)=0, BF(P) = 0;
  • BF(P)==0,插入点就是P子树,H(C)=0, H(B) = 0;此时H(A,B,C,D)=0,则H(L)=1, H(R) =1,
    推出BF(L) =0, BF(R)=0, BF(P) = 0;

右左失衡:
在这里插入图片描述
右左失衡,BF(R) = -2;
H(A) =x; H(A)-H(L)=-2;则H(L) = 2+x
因为(H(L) = max(H(D),H(P))+1),那么P子树和D子树一定有一颗树的高度为x+1;
如果D子树的高度为x+1,那么L树在P子树插入前就是x+2了,所以不成立。推出H(P)=x+1;
H(D)=x。因为若H(D)=x-1,那么失衡点就是L了,而不会是R了。
1)如果插入点来自B子树,H(B)=x,H(C) =x-1;
2)如果插入点来自C子树,H(C)=x,H(B) = x-1;
3)如果插入点来自P子树,则H(C)=0,H(B) = 0; x=0;

  • BF(P)==1,插入点来自B子树,H(B)=x, H(C) =x-1;此时H(A,B,D)=x, H(C)=x-1,则H(L)=x+1, H(R) = x+1,
    推出BF(L) =-1, BF(R)=0, BF(P) = 0;
  • BF(P)==-1,插入点来自C子树,H(C)=x,H(B) = x-1;此时H(A,C,D)=x,H(B)=x-1,则H(L)=x+1, H(R) = x+1
    推出BF(L) =1, BF(R)=0, BF(P) = 0;
  • BF(P)==0,插入点就是P子树,H(C)=0,H(B) = 0;此时H(A,B,C,D)=0,则H(L)=1, H(R) =1,
    推出BF(L) =0, BF(R)=0, BF(P) = 0;
(5) 调整为AVL树(举例)

在这里插入图片描述

Ⅳ 插入过程整理:

按照普通搜索树的方式进行插入,插入后需要调整平衡因子:
需要调整BF的节点为parent,如果插入点来自parent.left,则BF(parent)++;如果插入点来自parent.right,则BF(parent)--.

针对BF调整之后的结果,进行相应的处理:
在这里插入图片描述
(1)处理失衡的情况:

  • 左左失衡,对右旋
  • 左右失衡,先左旋后右旋
  • 右右失衡,左旋
  • 右左失衡,先右旋后左旋

(2)更新旋转后的BF:
1)左左失衡: BF(失衡点) = BF(失衡点.left) = 0

2)右右失衡BF(失衡点) = BF(失衡点.right) = 0

3)左右失衡(分情况讨论)

  • 如果原BF(失衡点.left.right)==1,推出:BF(失衡点.left) =0, BF(失衡点)=-1, BF(失衡点.left.right) = 0;
  • 如果原BF(失衡点.left.right)==-1,推出:BF(失衡点.left) =1, BF(失衡点)=0, BF(失衡点.left.right) = 0;
  • 如果原BF(失衡点.left.right)==0,推出:BF(失衡点.left) =0, BF(失衡点)=0, BF(失衡点.left.right) = 0;

4)右左失衡(分情况讨论):

  • 如果原BF(失衡点.right.left)==1,推出:BF(失衡点.right) =-1, BF(失衡点)=0, BF(失衡点.right.left) = 0;
  • 如果原BF(失衡点.right.left)==-1,推出:BF(失衡点.right) =1, BF(失衡点)=0, BF(失衡点.right.left) = 0;
  • 如果原BF(失衡点.right.left)==0,推出:BF(失衡点.right) =0, BF(失衡点)=0, BF(失衡点.right.left) = 0;
Ⅴ AVL树实现:
(1)Node.java
public class Node {
    int key;

    /**
     * 平衡因子
     */
    int bf;

    Node left;
    Node right;

    /**
     * 记录结点的父结点,如果结点是根结点,则 parent == null
     */
    Node parent;

    Node(int key, Node parent) {
        this.key = key;
        this.bf = 0;
        this.left = null;
        this.right = null;
        this.parent = parent;
    }
}
(2)AVLTree.java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

// 实现 纯 key 模型的 AVL 树
// 如果要实现 key-value 模型,只需要在结点中,多保存一个 value 即可
public class AVLTree {
    /**
     * 记录树的根结点,如果是空树,则 root == null
     */
    private Node root = null;

    /**
     * 插入 AVL 树
     * @param key 要插入的关键字
     * @throws RuntimeException 如果 key 重复了
     */
    public void insert(int key) {
        if (root == null) {
            // 空树的插入,单独处理即可
            root = new Node(key, null);
            return;
        }

        // 走到这里,肯定不是空树
        Node parent = null;
        Node cur = root;

        while (cur != null) {
            if (key == cur.key) {
                throw new RuntimeException("key(" + key + ") 已经重复了");
            } else if (key < cur.key) {
                parent = cur;
                cur = cur.left;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }

        // 直到找到 null 的位置,才真正开始插入
        if (key < parent.key) {
            cur = parent.left = new Node(key, parent);
        } else {
            // 这里只能是 key > parent.key,因为如果相等
            // 刚才循环中就抛异常了
            cur = parent.right = new Node(key, parent);
        }

        // 上面的过程是,正常搜索树的插入过程
        // parent 是要调整 BF 的结点
        // cur 是破坏源所在的根结点
        while (true) {
            // 根据情况,更新平衡因子
            if (cur == parent.left) {
                parent.bf++;
            } else {
                // cur == parent.right
                parent.bf--;
            }

            // 分情况处理
            if (parent.bf == 0) {
                break;
            } else if (parent.bf == 2) {
                // 进行失衡的修复
                // 左左失衡 OR 左右失衡
                if (cur.bf == 1) {
                    // 左左失衡
                    fixLeftLeftLoseBalance(parent);
                } else {
                    // -1
                    // 左右失衡
                    fixLeftRightLoseBalance(parent);
                }
                break;
            } else if (parent.bf == -2) {
                // 进行失衡的修复
                // 右右失衡 OR 右左失衡
                if (cur.bf == -1) {
                    // 右右失衡
                    fixRightRightLoseBalance(parent);
                } else {
                    // 1
                    // 右左失衡
                    fixRightLeftLoseBalance(parent);
                }
                break;
            } else if (parent == root){
                // -1/1 已经到根的位置了
                break;
            }

            // 如果需要继续(parent.bf==1/-1)
            Node parentOfParent = parent.parent;
            cur = parent;
            parent = parentOfParent;
        }
    }
    
    //左旋
    private void leftRotate(Node parent) {
        // 如果前面实现都正确,并且已经走到这个位置时,说明 parent 一定不是 null
        // 并且 parent.right 也一定不是 null
        Node parentOfParent = parent.parent;
        Node right = parent.right;
        Node leftOfRight = right.left;
        // parentOfParent 和 leftOfRight 可能是 null

        right.parent = parentOfParent;
        // 需要明确越来 parent 是 parentOfParent 的左还是右
        if (parentOfParent == null) {
            // 原来的根是 parent
            // 现在的根是 right
            root = right;
        } else if (parent == parentOfParent.left) {
            parentOfParent.left = right;
        } else {
            parentOfParent.right = right;
        }

        right.left = parent;
        parent.parent = right;

        parent.right = leftOfRight;
        if (leftOfRight != null) {
            leftOfRight.parent = parent;
        }
    }

    //右旋
    private void rightRotate(Node parent) {
        Node parentOfParent = parent.parent;
        Node left = parent.left;
        Node rightOfLeft = left.right;

        left.parent = parentOfParent;
        if (parentOfParent == null) {
            root = left;
        } else if (parent == parentOfParent.left) {
            parentOfParent.left = left;
        } else {
            parentOfParent.right = left;
        }

        left.right = parent;
        parent.parent = left;

        parent.left = rightOfLeft;
        if (rightOfLeft != null) {
            rightOfLeft.parent = parent;
        }
    }

    private void fixRightLeftLoseBalance(Node parent) {
        Node rightOfNode = parent.right;
        Node leftOfRightOfNode = rightOfNode.left;

        rightRotate(rightOfNode);
        leftRotate(parent);

        if (leftOfRightOfNode.bf == -1) {
            parent.bf = 1;
            rightOfNode.bf = leftOfRightOfNode.bf = 0;
        } else if (leftOfRightOfNode.bf == 1) {
            rightOfNode.bf = -1;
            parent.bf = leftOfRightOfNode.bf = 0;
        } else {
            parent.bf = rightOfNode.bf = leftOfRightOfNode.bf = 0;
        }
    }

    private void fixRightRightLoseBalance(Node parent) {
        Node rightOfNode = parent.right;

        leftRotate(parent);

        parent.bf = rightOfNode.bf = 0;
    }

    private void fixLeftRightLoseBalance(Node parent) {
        // 没必要进行 null 比较,已经走到这里来,如果外面的方法实现没问题,肯定不是出现 null
        Node leftOfNode = parent.left;
        Node rightOfLeftOfNode = leftOfNode.right;

        leftRotate(leftOfNode);//左旋
        rightRotate(parent);//右旋

        // 根据之前的计算结果,填写 BF 即可
        if (rightOfLeftOfNode.bf == 1) {
            parent.bf = -1;
            leftOfNode.bf = 0;
            rightOfLeftOfNode.bf = 0;
        } else if (rightOfLeftOfNode.bf == -1) {
            parent.bf = 0;
            leftOfNode.bf = 1;
            rightOfLeftOfNode.bf = 0;
        } else {
            parent.bf = leftOfNode.bf = rightOfLeftOfNode.bf = 0;
        }
    }

    private void fixLeftLeftLoseBalance(Node parent) {
        Node leftOfNode = parent.left;

        // 左左失衡,对失衡结点右旋
        rightRotate(parent);

        // 根据计算的结果,更新 BF
        parent.bf = leftOfNode.bf = 0;
    }

    /**
     * 在 AVL 树中查找对应的 key
     * @param key 要查找的关键字
     * @return AVL 是否包含这个 key
     */
    public boolean contains(int key) {
        Node cur = root;

        while (cur != null) {
            if (key == cur.key) {
                return true;
            } else if (key < cur.key) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }

        return false;
    }

    public void verify() {
        List<Integer> inOrderList = new ArrayList<>();
        // 计算该数的中序遍历
        inOrder(inOrderList, root);
        // 如何判断其是否有序呢?  把得到中序序列排序,如果排序后的结果和原来的结果一样,说明原来就有序
        List<Integer> inOrderListCopy = new ArrayList<>(inOrderList);
        Collections.sort(inOrderListCopy);

        if (!inOrderListCopy.equals(inOrderList)) {
            throw new RuntimeException("AVL 树规则不满足:中序遍历无序");
        }
        System.out.println("中序有序:验证OK");

        // 验证每个结点的 BF 是否计算正确
        preOrderAndCalcBF(root);
        System.out.println("BF 计算正确性: 验证OK");

        // 验证每个结点的 BF 是否都是 (-1, 0, 1)
        preOrderAndVerifyBF(root);
        System.out.println("BF 满足AVL特性: 验证OK");
    }

    private static void preOrderAndVerifyBF(Node node) {
        if (node != null) {
            if (node.bf != -1 && node.bf != 0 && node.bf != 1) {
                throw new RuntimeException("结点(" + node.key + ")的 BF 是 " + node.bf);
            }

            preOrderAndVerifyBF(node.left);
            preOrderAndVerifyBF(node.right);
        }
    }

    private static int height(Node node) {
        if (node == null) {
            return 0;
        }

        int left = height(node.left);
        int right = height(node.right);
        return Math.max(left, right) + 1;
    }

    private static void preOrderAndCalcBF(Node node) {
        if (node != null) {
            int left = height(node.left);
            int right = height(node.right);
            if (left - right != node.bf) {
                throw new RuntimeException("结点(" + node.key + ")的 BF 计算有错误");
            }

            preOrderAndCalcBF(node.left);
            preOrderAndCalcBF(node.right);
        }
    }

    private static void inOrder(List<Integer> list, Node node) {
        if (node != null) {
            inOrder(list, node.left);
            // 处理 node
            list.add(node.key);
            inOrder(list, node.right);
        }
    }
}
(3) Main.java
import java.util.Random;

public class Main {
    public static void main(String[] args) {
        Random random = new Random(20200705);
        AVLTree tree = new AVLTree();

        for (int i = 0; i < 1000; i++) {
            int r = random.nextInt(10000);
            try {
                tree.insert(r);
            } catch (RuntimeException e) {
                System.out.println(e.getMessage());
            }
        }
        tree.verify();
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值