【数据结构】4.4二叉检索树

目录

1、二叉检索树的定义

2、二叉检索树结点的定义

3、二叉检索树的实现

(1)查找

递归实现:

非递归实现:可将“尾递归”函数改为迭代函数

(2)查找与删除最小/最大元素

查找:

        1.递归实现

        2.非递归实现

删除

        1.递归实现

        2.非递归实现

(3)插入元素

1.递归实现:

2.非递归实现:

(4)删除结点

1.递归实现:

2.非递归实现:

4、操作的时间代价分析

5、AVL平衡二叉树

        1.LL型:

        2.LR型:

        3.RR型:

        4.RL型:

源代码

6、红黑树(Red-Black Tree,RBT)

7、二叉检索树的应用举例

(1)判断某一序列是否生成同一棵二叉检索树


1、二叉检索树的定义

        对于查找问题,可分为静态查找与动态查找,静态查找即二分查找,对于给定的有序数组,不会再增加或删去元素;而动态查找则会在给定数组中增加或删减元素。对于动态查找,则需要使用二叉检索树。
        二叉检索树(BST,Binary Search Tree),也称二叉排序树或二叉查找树。
        一棵二叉树,可以为空;如果不为空,其根节点的关键之为k,且满足以下性质:
        1. 非空左子树的所有键值小于其根结点的关键值k。
        2. 非空右子树的所有键值等于其根结点的关键值k。
        3. 左、右子树都是二叉搜索树。

2、二叉检索树结点的定义

       链式二叉树的结点类,此处采用key-value模型:对于每一个key值都有与之对应的唯一的value值,即key,value是配对出现的。
        这种做法在生活中也有类似的应用,例如:查找中英文字典时候,单词和其中文翻译就构成一种成对的结构;再比如统计某些情况单词字符的出现次数,也可以使用该模型。

public class BSTNode<Key extends Comparable<Key>, Value> { //泛型继承接口用的extends而不是 implements
    private Key key;
    private Value value;
    private BSTNode<Key, Value> left;
    private BSTNode<Key, Value> right;

    //constructor部分
    public BSTNode(Key key, Value value,BSTNode<Key, Value> left,BSTNode<Key, Value> right){
        this.key=key;
        this.value=value;
        this.left=left;
        this.right=right;
    }

    public boolean isLeaf() {
        return left == null && right == null;
    }

    //getter与setter部分
    public Key getKey() {
        return key;
    }
    public void setKey(Key key) {
        this.key = key;
    }

    public Value getValue() {
        return value;
    }
    public void setValue(Value value) {
        this.value = value;
    }

    public BSTNode<Key, Value> getLeft() {
        return left;
    }

    public void setLeft(BSTNode<Key, Value> left) {
        this.left = left;
    }

    public BSTNode<Key, Value> getRight() {
        return right;
    }

    public void setRight(BSTNode<Key, Value> right) {
        this.right = right;
    }
}

3、二叉检索树的实现

(1)查找

查找步骤:

        查找从根结点开始
若树为空,返回NULL:没有找到要匹配的元素
若树非空,则将根结点关键字和要查找的元素的key值进行比较,并进行不同处理:

  1. 若要查找的元素的key值=根结点的key值,找到匹配元素,返回指向此结点的value值
  2. 若要查找的元素的key值<根结点key值,只需在左子树中继续搜索;
  3. 若要查找的元素的key值>根结点的key值,在右子树中进行继续搜索;
  • 递归实现:

public Value find(Key key) {
    try {
        if (key == null)
            throw new Exception("key is null");
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
    return findHelp(root, key);//调用findHelp,从root开始,把根结点传入参数root(指定的某树root)
}

private Value findHelp(BSTNode<Key, Value> rt, Key key) {
    if (rt == null) return null;//查找失败,基准情形1
    if(key.compareTo(rt.getKey())==0)
        return rt.getValue();//找到值,基准情形2
    else{
        if (key.compareTo(rt.getKey()) < 0)
            return findHelp(rt.getLeft(), key);//小于当前key值则往左子树查找
        else
            return findHelp(rt.getRight(), key);//大于当前key值则往右子树查找
    }
}
  • 非递归实现:可将“尾递归”函数改为迭代函数

public Value find(Key key) {
    try {
        if (key == null)
            throw new Exception("key is null");
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
        
    BSTNode<Key, Value> rootTemp = root;//需要用存下当前指向的结点,不能直接对root修改!
    while(rootTemp!=null){
        if(key.compareTo(rootTemp.getKey()) == 0)
            return rootTemp.getValue();
        else{
            if(key.compareTo(rootTemp.getKey()) < 0)
                rootTemp=rootTemp.getLeft();
            else
                rootTemp=rootTemp.getRight();
        }
    }
    return null;//查找失败
}

(2)查找与删除最小/最大元素

  • 最大元素一定是在树的最右分枝的端结点上
  • 最小元素一定是在树的最左分枝的端结点上

        下文以最小元素为例说明该思想。

  • 查找:

思想:

       从根结点开始找,由于最小元素一定是在树的最左分枝的端结点上,因此一直向左走,直到当前结点的左子结点为空,当前结点元素即为最小元素,返回该结点。

代码实现:

        1️⃣递归实现

public BSTNode getMinNode() {
    if(rt == null) 
        return null;//空的二叉搜索树,返回null
    else
        return (getMinNodeHelp(root));//调用getMinNodeHelp,从root开始,把根结点传入参数root(指定的某树root)
}
private BSTNode getMinNodeHelp(BSTNode<Key, Value> rt) {
    //最小元素一定是在树的最左分枝的端结点上,因此一直向左走,直到左子结点为空
    if (rt.getLeft() == null)
        return rt;//找到最左叶结点并返回
    else
        return getMinNodeHelp(rt.getLeft());//沿左分支继续查找
}

        2️⃣非递归实现

public BSTNode getMinNodeWithoutR(){
    if(root == null)
        return null;//空的二叉搜索树,返回null
    BSTNode<Key, Value> rootTemp = root;//需要用存下当前指向的结点,不能直接对root修改!
    while(rootTemp.getLeft()!=null)
        rootTemp=rootTemp.getLeft();//沿左分支继续查找
    return rootTemp;//找到最左叶结点,退出循环,并返回
}
  • 删除

思路:

  1. 从给定结点开始,一路向左找最小结点,直至当前结点无左子节点(基准情形)
  2. 将当前结点父结点的左子结点置为当前结点的右子结点

分析:
        找到最小结点时,还需要对该位置的父结点的左子结点进行修改,因此,需要记忆父结点的位置。
        同时需要考虑若最小结点在给定结点的位置时如何处理!

        1️⃣采用递归实现:

        在非基准情形时做递归调用,期望子递归返回左子树在删除最小结点后的新树的根结点,将返回的根结点作为当前结点的左子结点。

        可以注意到,修改子结点的操作仅对于最小结点的父结点有实际作用,对于上层结点并未修改其左子结点。由于使用递归操作的简便性,该操作冗余在可接受范围内。

        2️⃣ 采用非递归实现:

        分析:对于给定的结点分2种情况:
        1.该结点就是根结点,此时parent=null。再分两种情况:

                1.没有左分支,故要删根结点,直接做root=root.getRight();

                2.有左分支,与非根节点的操作相同,只需要依次遍历下去找最小结点即可;
        2.非根结点,再分两种情况:

                1.该结点没有左分支,故要删该点,需要知道该结点的父结点;

                2.该结点有左分支,无需知道该结点的parent,只需要依次遍历下去找最小结点即可。

        步骤:因此,根据分析的情况可以将根结点+有左分支的情况与非根结点+有左分支的情况合并,算法步骤如下:

        判断给定结点有无左分支,

        1.若没有左分支则找该结点的父结点。根据父结点属性做如下操作:

                1.若得到的父节点为null,表明给定结点为根结点,直接删除根结点即可;

                2.若父结点存在,则将父节点的某子结点(根据查找时的方向)置为待删除结点的右节点。

        2.若存在左分支,以给定结点出发,一路向左找最小值,并记录当前结点的父结点,直到当前遍历的结点无左分支,退出循环。再将当前的父结点的左子节点置为当前结点的右子结点。

        返回当前结点。

         1️⃣递归实现

private BSTNode<Key, Value> deleteMinNode(BSTNode<Key, Value> current) {
    //递归写法,对于在给定结点为最小时需要修改父结点的操作,感觉不是很好,此写法参考非递归思路更清晰
    if (current.getLeft() == null) {
        BSTNode<Key, Value> rootTemp = root;
        BSTNode<Key, Value> parent = null;
        while (rootTemp != current) {
            parent = rootTemp;
            if (current.getKey().compareTo(rootTemp.getKey()) < 0)
                rootTemp = rootTemp.getLeft();
            else
                rootTemp = rootTemp.getRight();
        }
        if (parent == null)
            root = root.getRight();//若删除的是根结点
        else {//非根节点
            if (current.getKey().compareTo(parent.getKey()) < 0)
                parent.setLeft(current.getRight());
            else
                parent.setRight(current.getRight());
        }
        return current.getRight();//返回给定子树更新以后的根结点
    } else {
        return deleteMinNodeR(current);//调用递归函数
    }
}

private BSTNode<Key, Value> deleteMinNodeR(BSTNode<Key, Value> rt){
    if (rt.getLeft() == null)
        rt = rt.getRight();
    else
        rt.setLeft(deleteMinNode(rt.getLeft()));//冗余操作是可以接受的
    return rt;
}

        2️⃣非递归实现

public BSTNode deleteMinNodeWithoutR(BSTNode<Key, Value> rt) {
    //删除该结点下的最小结点,返回给定子树更新以后的根结点
    BSTNode<Key, Value> current = rt;
    BSTNode<Key, Value> parent;
    if (current.getLeft() == null) {//若给定结点即为该树的最小结点,找其父结点
        BSTNode<Key, Value> rootTemp = root;
        parent = null;
        while (rootTemp != current) {
            parent = rootTemp;
            if (current.getKey().compareTo(rootTemp.getKey()) < 0)
                rootTemp = rootTemp.getLeft();
            else
                rootTemp = rootTemp.getRight();
        }
        if (parent == null)
            root = root.getRight();//若删除的是根结点
        else {
            if (current.getKey().compareTo(parent.getKey()) < 0)
                parent.setLeft(current.getRight());
            else
                parent.setRight(current.getRight());
        }
        return current.getRight();//返回给定子树更新以后的根结点
    } else {
        BSTNode<Key, Value> res=current;//临时保存cur的值,返回给定子树更新以后的根结点
        while (current.getLeft() != null) {
            parent = current;
            current = current.getLeft();
        }
        parent.setLeft(current.getRight());
        return res;
    }
}

        同理,对于最大元素操作代码实现如下

    public BSTNode getMaxNode() {
        if(root == null)
            return null;//空的二叉搜索树,返回null
        else 
            return (getMaxNodeHelp(root));
    }

    private BSTNode getMaxNodeHelp(BSTNode<Key, Value> rt) {
        if (rt.getRight() == null)
            return rt;
        else
            return getMaxNodeHelp(rt.getRight());
    }

    private BSTNode<Key, Value> deleteMaxNode(BSTNode<Key, Value> rt) {
        //此处为在给定结点为最大时,未修改父结点的代码
        if (rt.getRight() == null)
            rt = rt.getLeft();
        else
            rt.setRight(deleteMaxNode(rt.getRight()));
        return rt;
    }

(3)插入元素

思路:

  1. 找应插入的位置(类似于find方法),即所属位置父结点的空子结点。
  2. 找到后用key值与value值创建结点(该结点无左右子树),返回一个结点的二叉搜索树
  3. 修改上一层父结点的某子结点

分析:
        找到插入位置时,还需要对该位置的父结点的某子结点进行修改,因此,需要记忆父结点的位置
1️⃣采用递归实现:
        在非基准情形时做递归调用,期望子递归
返回子树在插入待插结点后的新树的根结点,将返回的根结点作为当前结点的某子结点(与调用时方向相同)

        可以注意到,修改子结点的操作仅对于插入位置的父结点有实际作用,对于上层结点并未修改其子结点值。由于使用递归操作的简便性,该操作冗余在可接受范围内。
2️⃣ 采用非递归实现:
        若为空子树,直接插入当前元素作为根结点,退出。

        若为非空子树,需要设置parent与current指针,记录遍历到的父结点,寻找待插入位置:

        1. 若存在相同key值的结点,则只需要更新value值,无需修改任何结点的子结点,退出即可。

        2. 若不存在相同key值的结点,则继续找待插入位置,根据待插入结点的key值与当前current的key值的关系,确定向左向右查找,修改parent与current指针,直至找到待插入位置,此时current为null,退出循环。
        退出循环后,在current位置生成一个结点的二叉搜索树。
        修改父结点的某子结点,将current所在位置的二叉树设为parent的子结点,通过key值大小判断左右

1️⃣ 递归实现:

public void insert(Key key, Value value) {
    try { //检查传入的结点属性是否合法
        if (key == null) throw new Exception("Key is null, insert fault!");
        if (value == null) throw new Exception("Value is null, insert fault!.");
    } catch (Exception e) {
        e.printStackTrace();
    }
    调用insertHelp,从root开始,把根结点传入参数root(指定的某树root)
    root = insertHelp(root, key, value);
}

private BSTNode<Key, Value> insertHelp(BSTNode<Key, Value> rt, Key key, Value value) {
    //找到插入位置,生成并返回一个结点的二叉搜索树,作为上一层的子结点。基准情形1
    if (rt == null)
        return new BSTNode<Key, Value>(key, value, null, null);
    //否则继续找要插入元素的位置
    if (key.compareTo(rt.getKey()) < 0)
        rt.setLeft(insertHelp(rt.getLeft(), key, value));
    else if (key.compareTo(rt.getKey()) > 0)
        rt.setRight(insertHelp(rt.getRight(), key, value));
    else
        rt.setValue(value);//如果key值相同则更新value值。基准情形2
    return rt;
}

 2️⃣ 非递归实现:

public void insert(Key key, Value value) {
    if (root == null) {
        root = new BSTNode<Key, Value>(key, value, null, null);
        return;//空子树,直接插入,退出
    }
    BSTNode<Key, Value> parent = null;
    BSTNode<Key, Value> current = root;
    while (current != null) {
        if (key.compareTo(current.getKey()) == 0) {
            current.setValue(value);
            return;//如果key值相同则更新value值,无需修改任何结点的子结点,退出
        } else {//继续找待插入位置
            if (key.compareTo(current.getKey()) < 0) {
                parent = current;
                current = current.getLeft();
            } else {
                parent = current;
                current = current.getRight();
            }
        }
    }
    //退出循环表示已经找到待插入位置,在current位置生成一个结点的二叉搜索树
    current = new BSTNode<Key, Value>(key,value,null,null);
    //修改父结点的某子结点,通过key值大小判断左右
    if(key.compareTo(parent.getKey()) < 0)
        parent.setLeft(current);
    else
        parent.setRight(current);
}

        不同的插入顺序会生成不同的二叉树。
        应尽可能保证二叉树的平衡性:任意结点的左右子树的高度之差( h_{L}-h_{R},记为平衡因子BF,Balance Factor)的绝对值小于等于1,则说明这棵树是平衡的,称这种树为平衡二叉树(AVL树)。

        对于AVL树,有以下性质:给定结点数为n的AVL树的最大高度为ceil(log_{2}n)。

(4)删除结点

分析:考虑三种情况:

        1. 要删除的是叶结点:直接删除,并再修改其父结点的子结点(置为 null)

        2. 要删除的结点只有一个孩子结点: 将其父结点的指针指向要删除结点的孩子结点

        3. 要删除的结点有左、右两棵子树(包含了是根结点的情况,需要小心处理):用另一结点替代被删除结点:右子树的最小元素或者左子树的最大元素

        找最接近待删除结点key值的结点,使得需要变化的结点数目最少。
        由于待删除结点的左子树中所有结点的key值<待删除结点的key值<=待删除结点的右子树中所有结点的key值,可得到关系式:
        待删除结点的左子树中所有结点的key值<=左子树中最大结点key值<待删除结点的key值<=右子树中最小结点key值<=待删除结点的右子树中所有结点的key值
        故,找左子树上最大值/右子树上最小值结点,即可用于替代待删除的结点。由于左子树上最大值/右子树上最小值结点必定没有两个子结点,故替换后该问题转换为删除最小最大结点的问题
        在存在重复值的问题中(重复值只能挂在右子树中),采用选择右子树的最小值替换更为合理。

 步骤:
        先找到要删除的结点。若当前结点为null,则表示不存在待删除元素,则返回null;否则继续向下查找
        找到待删除元素后,根据该结点的度进行不同的处理:
        1.若待删除结点不存在左子节点/右子节点:直接将右子结点/左子结点作为当前根结点
        2.待删除结点有两个子结点:将当前结点的key和value更新为右子树中的最小结点的值,并将当前结点的右子结点进行更新,即删除右子树中的最小结点

        1️⃣采用递归实现:

        将问题转化为删除右子树最小结点后,返回待删除结点删除后的二叉树的根结点,将返回的根结点作为当前结点的某子结点(与调用时方向相同),一直返回到根结点处。

        故此时同样适用于删除根结点的操作,无需分类讨论修改根结点。

        (该操作同插入操作存在可接受的冗余)

        2️⃣ 采用非递归实现:

        使用parent与current指针记录当前位置的父结点与结点,分2种情况讨论:

        1.待删除结点为根结点时,对子结点个数不同情况做具体操作。

        2.待删除结点非根节点时,对子结点个数不同情况做具体操作。

        由于deleteHelp() 函数中已经有查找待删除结点的步骤,可记录其父结点,故若待删除结点的右子树中的最小值是该右子树的根结点时,在deleteMinNode()函数中无需修改其父结点的子结点值(常规操作中需要从根结点遍历到该结点的父结点),只需在deleteHelp() 函数中直接修改待删除结点的右子结点为deleteMinNode()函数的返回值(给定子树更新以后的根结点)即可。故对上述的deleteMinNode()函数做如下变式:

        递归实现:

private BSTNode<Key, Value> deleteMinNode(BSTNode<Key, Value> rt) {
    //返回给定子树更新以后的根结点
    if (rt.getLeft() == null)
        rt = rt.getRight();
    else
        rt.setLeft(deleteMinNode(rt.getLeft()));//冗余操作是可以接受的
    return rt;
}

        非递归实现:

public BSTNode<Key, Value> deleteMinNode(BSTNode<Key, Value> rt) {
    //返回给定子树更新以后的根结点
    BSTNode<Key, Value> current = rt;
    if (current.getLeft() == null)
        return current.getRight();//给定结点为最小结点,直接返回给定结点的右子结点即可
    //若给定结点非最小结点
    BSTNode<Key, Value> parent;
    BSTNode<Key, Value> res = current;//临时保存cur的值,返回给定子树更新以后的根结点
    do {
        parent = current;
        current = current.getLeft();
    } while (current.getLeft() != null);
    parent.setLeft(current.getRight());
    return res;
}
private BSTNode getMinNodeHelp(BSTNode<Key, Value> rt) {
    if (rt.getLeft() == null)
        return rt;//找到最左叶结点并返回
    else
        return getMinNodeHelp(rt.getLeft());//沿左分支继续查找
}

1️⃣递归实现:

private Value removeValue; 
public Value delete(Key key) {
    removeValue = null;
    try {
        if (key == null)
            throw new Exception("Key is null, delete failure");
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
    //调用deleteHelp,从root开始,把根结点传入参数root(指定的某树root)
    root = deleteHelp(root, key);
    return removeValue;
}

private BSTNode<Key, Value> deleteHelp(BSTNode<Key, Value> rt, Key key) {
    //返回该层递归生成的新子树的根结点
    if (rt == null) {
        System.out.println("No Such key");//没找到待删除元素
        return null;
    }
    if(key.compareTo(rt.getKey()) == 0){//找到待删除元素了
        removeValue = rt.getValue();
        if (rt.getLeft() == null)
            rt = rt.getRight();//左子结点为空,直接将右子结点作为当前根结点
        else if (rt.getRight() == null)
            rt = rt.getLeft();//右子结点为空,直接将左子结点作为当前根结点
        else {//待删除结点有两个子结点
            rt.setKey((Key) getMinNodeHelp(rt.getRight()).getKey());
            rt.setValue((Value) getMinNodeHelp(rt.getRight()).getValue());
            //将当前结点的key和value更新为右子树中的最小结点的值
            rt.setRight(deleteMinNode(rt.getRight()));
            //将当前结点的右子结点进行更新,即删除右子树中的最小结点
        }
    }else{//继续找待删除元素
        if (key.compareTo(rt.getKey()) < 0)
            rt.setLeft(deleteHelp(rt.getLeft(), key));
        else
            rt.setRight(deleteHelp(rt.getRight(), key));
        }
    return rt;
}

 2️⃣ 非递归实现:

public Value deleteWithoutR(Key key) {
    BSTNode<Key, Value> parent = null;
    BSTNode<Key, Value> current = root;
    while (current != null) {
        if (key.compareTo(current.getKey()) == 0) {//找到待删除元素了
        Value removeValue = current.getValue();
        if (parent == null) {//等价于root == current,待删除的是根结点
            if (root.getLeft() == null)
                root = root.getRight();//根结点的左子结点为空,直接将右子结点作为当前根结点
            else if (current.getRight() == null)
                root = root.getLeft();//根结点右子结点为空,直接将左子结点作为当前根结点
            else {//根结点有两个子结点
                root.setKey((Key) getMinNodeHelp(root.getRight()).getKey());
                root.setValue((Value) getMinNodeHelp(root.getRight()).getValue());
                //将当前结点的key和value更新为右子树中的最小结点的值
                root.setRight(deleteMinNode(root.getRight()));
                //将当前结点的右子结点进行更新,即删除右子树中的最小结点
                }
            } else {
                if (current.getLeft() == null) {//左子结点为空,直接将右子结点作为当前根结点
                    if (current.getKey().compareTo(parent.getKey()) < 0)
                        parent.setLeft(current.getRight());
                    else
                        parent.setRight(current.getRight());
                } else if (current.getRight() == null) {//右子结点为空,直接将左子结点作为当前根结点
                    if (current.getKey().compareTo(parent.getKey()) < 0)
                        parent.setLeft(current.getLeft());
                    else
                        parent.setRight(current.getLeft());
                } else {//待删除结点有两个子结点
                    current.setKey((Key) getMinNodeHelp(current.getRight()).getKey());
                    current.setValue((Value) getMinNodeHelp(current.getRight()).getValue());
                    //将当前结点的key和value更新为右子树中的最小结点的值
                    current.setRight(deleteMinNode(current.getRight()));
                    //将当前结点的右子结点进行更新,即删除右子树中的最小结点
                    if (current.getKey().compareTo(parent.getKey()) < 0)
                        parent.setLeft(current);
                    else
                        parent.setRight(current);
                    //更新父结点的某子结点
                }
            }
            return removeValue;
        } else {//继续找待删除元素
            if (key.compareTo(current.getKey()) < 0) {
                parent = current;
                current = current.getLeft();
            } else {
                parent = current;
                current = current.getRight();
            }
        }
    }
    System.out.println("No Such key");
    return null;
}

4、操作的时间代价分析

        1.搜索、插入、删除的时间代价:
●    平衡二叉树的操作代价为O(logn)
●    非平衡的二又树最差的代价为O(n)
        2.周游一个二叉树的代价为O(n)

       使一个二叉树保持平衡才能真正发挥二叉树的作用,平衡的两种方法:
        1. AVL平衡二叉树 (旋转)
        2. 在对一组数据构建二叉树时先将数据随机打乱

5、AVL平衡二叉树

       向AVL树插入结点或删除结点时可能会破坏树的平衡性,此时要调整树的结构,平衡化处理恢复平衡使之重新达到平衡且保持BST的结构性质,下文以插入操作为例分析。
        用紫色方格表示新插入的结点,又称为“麻烦结点”,A表示离新插入结点最近的且平衡因子变为±2的祖先结点,又称为“发现者”。
        在每插入/删除结点时均需要更新所有结点的BF值并判断当前结点是否平衡:新增结点在其父结点结点的右边,父结点BF值+1;否则-1。
        依次往上更新,若当前结点的BF值为0则其上层结点必定平衡[分析:由于每次更新只会+1、-1或不操作,故对于该结点为根的树来说,插入或删除结点只会把空位填补或删除多余的结点,故趋于平衡,整体即平衡];
        若该结点BF值为±1,有继续向上更新的必要;
        若该结点BF值变为±2,则需要进行旋转处理。
        因此,可以注意到,当平衡被破坏时,A结点在“麻烦结点”一侧子结点的BF值必定为±1,而不会为0。
        因此,分析平衡打破的4种情况,可用4种旋转进行平衡化处理:
        ●  LL型: 新结点被插入到 A 的左子树的左子树上,BF(curNode)=2,BF(curNode.left)=1→顺(左单旋)
        ●  LR型:新结点被插入到 A 的左子树的右子树上,BF(curNode)=2,BF(curNode.left)=-1→先逆后顺(右左双旋)
        ●  RR型:新结点被插入到 A 的右子树的右子树上,BF(curNode)=-2,BF(curNode.right)=-1 →逆(右单旋)
        ●  RL型:新结点被插入到 A 的右子树的左子树上,BF(curNode)=-2,BF(curNode.right)=1 →先顺后逆(左右双旋)

        1.LL型:

        新结点被插入到 A 的左子树的左子树上,导致A结点的BF=A的左子树的高度-A的右子树的高度=2,且A的左子结点B的BF=1。
        根据各结点的键值大小关系:B的左子树上所有结点<B<B的右子树上所有结点<A<A的右子树上所有结点
        因此,可做下图所示的调整:以B结点为该树的根结点,构造[B的左子树] - B结点 - [ [B的右子树] - A结点 - [A的右子树] ]的结构。

        此时可做顺时针旋转(左单旋):
        先记录下当前根结点A的值(以原根结点的值创建新的结点);A新的左子结点为B结点的右子结点;右子树仍为A结点的右子树。
        再将该树的根结点修改为B结点;新的左子树仍为B的左子树,一并旋转过去;新的右子结点为创建的新结点(修改后的A结点)。
        注意!不可直接把传入的形参指向新的B结点!
        (代码举例:rt=new AVLTNode(rt.left.key,rt.left.left,newNode))
        如此操作只是把形参的指针指向了B结点,实际上不会修改传入的实参指向的结点内容。

        2.LR型:

        新结点被插入到 A 的左子树的右子树上(有两种可能的情形:①在C的左子树上,②在C的右子树上),导致A结点的BF=A的左子树的高度-A的右子树的高度=2,且A的左子结点B的BF=-1。
        根据各结点的键值大小关系:B的左子树上所有结点<B<C的左子树上所有结点<C<C的右子树上所有结点<A<A的右子树上所有结点
        因此,可做下图所示的调整:以C结点为该树的根结点,构造[ [B的左子树] - B结点 - [C的左子树] ] - C结点 - [ [C的右子树] - A结点 - [A的右子树] ]的结构。
        此时可做先逆时针后顺时针的旋转(右左双旋),即先对B做逆时针旋转,后对A做顺时针旋转:
        Step1.对B结点做逆时针旋转:对以B为根结点的树操作
        先记录下当前根结点B的值(以原根结点的值创建新的结点);B左子树仍为B结点的左子树;新的右子结点为C结点的左子结点。
        再将该树的根结点修改为C结点;新的左子结点为创建的新结点(修改后的B结点);新的右子树仍为C的右子树,一并旋转过去。
        Step2.再对A结点做顺时针旋转:对以A为根结点的树操作
        先记录下当前根结点A的值(以原根结点的值创建新的结点);A新的左子结点为C结点(A的新左子树)的右子结点;右子树仍为A结点的右子结点。
        再将该树的根结点修改为C结点(A的新左子树的根结点);新的左子树仍为C的左子树,一并旋转过去;新的右子结点为创建的新结点(修改后的A结点)。

        3.RR型:

        新结点被插入到 A 的右子树的右子树上,导致A结点的BF=A的左子树的高度-A的右子树的高度=-2,且A的右子结点B的BF=-1。
        根据各结点的键值大小关系:A的左子树上所有结点<A<B的左子树上所有结点<B<B的右子树上所有结点
        因此,可做下图所示的调整:以B结点为该树的根结点,构造[ [A的左子树] - A结点 - [B的左子树] ] - B结点 - [B的右子树]的结构。
        此时可做逆时针旋转(右单旋):
        先记录下当前根结点A的值(以原根结点的值创建新的结点);A左子树仍为A结点的左子树;新的右子结点为B结点的左子结点。
        再将该树的根结点修改为B结点;新的左子结点为创建的新结点(修改后的A结点);新的右子树仍为B的右子树,一并旋转过去。

        4.RL型:

        新结点被插入到 A 的右子树的左子树上(有两种可能的情形:①在C的左子树上,②在C的右子树上),导致A结点的BF=A的左子树的高度-A的右子树的高度=-2,且A的右子结点B的BF=1。
        根据各结点的键值大小关系:A的左子树上所有结点<A<C的左子树上所有结点<C<C的右子树上所有结点<B<<B的右子树上所有结点
        因此,可做下图所示的调整:以C结点为该树的根结点,构造[ [A的左子树] - A结点 - [C的左子树] ] - C结点 - [ [C的右子树] - B结点 - [B的右子树] ]的结构。
        此时可做先顺时针后逆时针的旋转(左右双旋),即先对B做顺时针旋转,后对A做逆时针旋转:
        Step1.对B结点做顺时针旋转:对以B为根结点的树操作
        先记录下当前根结点B的值(以原根结点的值创建新的结点);B新的左子结点为C结点的右子结点;右子树仍为B结点的右子树。
        再将该树的根结点修改为C结点;新的左子树仍为C的左子树,一并旋转过去;新的右子结点为创建的新结点(修改后的B结点)。
        Step2.再对A结点做逆时针旋转:对以A为根结点的树操作
        先记录下当前根结点A的值(以原根结点的值创建新的结点);A左子树仍为A结点的左子树;新的右子结点为C结点(A的新右子树的根结点)的左子结点。
        再将该树的根结点修改为C结点(A的新右子树的根结点);新的左子结点为创建的新结点(修改后的A结点);新的右子树仍为C的右子树,一并旋转过去。

         删除与插入操作是对称的(镜像,互逆的):
1.删除右子树结点导致失衡时,相当于在左子树插入导致失衡,即LL或LR;
2.删除左子树结点导致失衡时,相当于在右子树插入导致失衡,即RR或RL;
        删除操作可能需要多次平衡化处理:由于平衡化不会增加子树的高度,但可能会减少子树的高度,所以在有可能使树增高的插入操作中,一次平衡化能抵消掉树增高;但在有可能使树减低的删除操作中,平衡化可能会带来祖先结点的不平衡。因此,删除操作可能需要多次平衡化处理。

源代码

        ● 结点的定义

public class AVLTNode {
    int key;
    AVLTNode left;
    AVLTNode right;
    public AVLTNode(int key,AVLTNode left,AVLTNode right) {
        this.key = key;
        this.left=left;
        this.right=right;
    }
    public String toString() {
        return "Node [key=" + key + "]";
    }
}

         ● 树的实现

public class AVLTree {
    private AVLTNode root;
    public AVLTNode getRoot() {
        return root;
    }

    public void insert(int key) {
        root = insertHelp(root, key);//调用insertHelp,从root开始,把根结点传入参数root(指定的某树root)
        balanced(root);
    }
    private AVLTNode insertHelp(AVLTNode rt, int key) {
        if (rt == null)
            return new AVLTNode(key, null, null);//找到插入位置,生成并返回一个结点的二叉搜索树,作为上一层的子结点。基准情形1
        //否则继续找要插入元素的位置
        if (key < rt.key)
            rt.left = insertHelp(rt.left, key);// 递归的向左子树添加
        else if (key > rt.key)
            rt.right = insertHelp(rt.right, key);// 递归的向右子树添加
        //如果key值相同,无需操作。返回即可。基准情形2
        balanced(rt);//添加结点之后做平衡调整
        return rt;
    }

    public void delete(int key) {
        //调用deleteHelp,从root开始,把根结点传入参数root(指定的某树root)
        root = deleteHelp(root, key);
        balanced(root);
    }
    private AVLTNode deleteHelp(AVLTNode rt, int key) {
        //返回该层递归生成的新子树的根结点
        if (rt == null) {
            System.out.println("No Such key");//没找到待删除元素
            return null;
        }
        if (key == rt.key) {//找到待删除元素了
            if (rt.left == null)
                rt = rt.right;//左子结点为空,直接将右子结点作为当前根结点
            else if (rt.right == null)
                rt = rt.left;//右子结点为空,直接将左子结点作为当前根结点
            else {//待删除结点有两个子结点
                rt.key = getMinNodeHelp(rt.right).key;
                //将当前结点的key更新为右子树中的最小结点的值
                rt.right = deleteMinNode(rt.right);
                //将当前结点的右子结点进行更新,即删除右子树中的最小结点
            }
        } else {//继续找待删除元素
            if (key < rt.key)
                rt.left = deleteHelp(rt.left, key);
            else
                rt.right = deleteHelp(rt.right, key);
        }
        balanced(rt);
        return rt;
    }
    private AVLTNode getMinNodeHelp(AVLTNode rt) {
        //最小元素一定是在树的最左分枝的端结点上,因此一直向左走,直到左子结点为空
        if (rt.left == null)
            return rt;//找到最左叶结点并返回
        else
            return getMinNodeHelp(rt.left);//沿左分支继续查找
    }
    private AVLTNode deleteMinNode(AVLTNode rt) {
        // 没有对父结点进行修改,和delete操作一起看是可以的
        // 返回给定子树更新以后的根结点
        if (rt.left == null)
            rt = rt.right;
        else
            rt.left = deleteMinNode(rt.left);//冗余操作是可以接受的
        return rt;
    }

    private void balanced(AVLTNode rt) {
        //当添加或删除完一个结点后BF==2, 表明造成失衡是在当前结点的左侧,属于LL或LR的一种。
        if (getBF(rt) >= 2) {
            // 左子结点的BF值==1,则为LL型;左子结点的BF值==-1,为LR型
            if (getBF(rt.left) >= 1) {//LL型
                clockwiseRotate(rt);//直接对该结点进行顺时针旋转即可
            } else {//LR型
                counterclockwiseRotate(rt.left);//先对该结点的左子结点逆时针旋转
                clockwiseRotate(rt);//再对该结点进行顺时针旋转
            }
        }
        //当添加或删除完一个结点后BF==-2, 表明造成失衡是在当前结点的右侧,属于RR或RL的一种。
        if (getBF(rt) <= -2) {
            // 右子结点的BF值==1,则为RL型;右子结点的BF值==-1,为RR型
            if (getBF(rt.right) >= 1) {//RL型
                //rt.right != null && getHeight(rt.right.left) > getHeight(rt.right.right)这句有什么必要吗
                clockwiseRotate(rt.right);//先对该结点的右子结点顺时针旋转
                counterclockwiseRotate(rt); //再对该结点进行逆时针旋转
            } else {//RR型
                counterclockwiseRotate(rt);//直接对该结点进行逆时针旋转即可
            }
        }
    }
    private void counterclockwiseRotate(AVLTNode rt) {
        //以原根结点的值创建新的结点,左子树不改变仍为原根结点的左子树,右子树修改为原根结点的右子结点(新根结点)的左子结点。
        AVLTNode newNode = new AVLTNode(rt.key, rt.left, rt.right.left);
        rt.key = rt.right.key; //新根结点为原根结点的右子结点
        rt.left = newNode; //新根结点的左子结点为创建的新结点(键值为原根结点)
        rt.right = rt.right.right; //新根结点的右子树仍为新根结点的右子树,一并旋转过去。
        //不能写作rt=new AVLTNode(rt.right.key,rt.right.right,newNode)!rt是形参,此代码只是把形参的指针指向了B结点,实际上不会修改传入的实参指向的结点内容。
    }
    private void clockwiseRotate(AVLTNode rt) {
        //以原根结点的值创建新的结点,左子树修改为原根结点的左子结点(新根结点)的右子结点,右子树不改变仍为原根结点的右子树。
        AVLTNode newNode = new AVLTNode(rt.key, rt.left.right, rt.right);
        rt.key = rt.left.key; //新根结点为原根结点的左子结点
        rt.left = rt.left.left; //新根结点的左子树仍为新根结点的左子树,一并旋转过去。
        rt.right = newNode; //新根结点的右子结点为创建的新结点(键值为原根结点)
        //不能写作rt=new AVLTNode(rt.left.key,rt.left.left,newNode)!rt是形参,此代码只是把形参的指针指向了B结点,实际上不会修改传入的实参指向的结点内容。
    }

    public int getHeight(AVLTNode rt) {
//        int height = 0;
//        if (rt == null) return 0;
//        height = Math.max(getHeight(rt.left), getHeight(rt.right)) + 1;
//        return height;
        if (rt==null) return 0;
        return Math.max(rt.left == null ? 0 : getHeight(rt.left), rt.right == null ? 0 : getHeight(rt.right)) + 1;
    }
    public int getBF(AVLTNode rt) {
        return getHeight(rt.left) - getHeight(rt.right);
    }

    void infixOrderByRecursion(AVLTNode root) {
        if (root == null) return;//空树
        infixOrderByRecursion(root.left);
        System.out.println(root); //(访问)打印结点
        infixOrderByRecursion(root.right);
    }
}

        ● 测试代码

public class TestAVLTree {
    public static void main(String[] args) {
        int[] arr = {25, 27, 30, 12, 11, 18, 14, 20, 15, 22};
        //创建一个 AVLTree对象
        AVLTree avlTree = new AVLTree();
        for (int i = 0; i < arr.length; i++){
            avlTree.insert(arr[i]);
        }
        //遍历
        System.out.println("中序遍历");
        avlTree.infixOrderByRecursion(avlTree.getRoot());
        System.out.println("树的高度=" + avlTree.getHeight(avlTree.getRoot())); //3
        System.out.println("树的左子树高度=" + avlTree.getHeight(avlTree.getRoot().left)); // 2
        System.out.println("树的右子树高度=" + avlTree.getHeight(avlTree.getRoot().right)); // 2
        System.out.println("当前的根结点=" + avlTree.getRoot());//8
        avlTree.delete(100);//输出NoSuchKey
        //遍历
        System.out.println("中序遍历");
        avlTree.infixOrderByRecursion(avlTree.getRoot());
        System.out.println("树的高度=" + avlTree.getHeight(avlTree.getRoot())); //3
        System.out.println("树的左子树高度=" + avlTree.getHeight(avlTree.getRoot().left)); // 2
        System.out.println("树的右子树高度=" + avlTree.getHeight(avlTree.getRoot().right)); // 2
        System.out.println("当前的根结点=" + avlTree.getRoot());//8

    }
}

6、红黑树(Red-Black Tree,RBT)

7、二叉检索树的应用举例

(1)判断某一序列是否生成同一棵二叉检索树

        给定一个插入序列就可以唯一确定一棵二叉搜索树。然而,一棵给定的二叉搜索树却可以由多种不同的插入序列得到。
        例如,按照序列{2, 1, 3}和{2, 3, 1}插入初始为空的二叉搜索树,都得到一样的结果。
        问题:对于输入的各种插入序列,需要判断它们是否能生成一样的二叉搜索树。
        给出输入样例,第一行输入两个整数n和d,n表示树的结点个数,d表示待判断的序列个数。第二行输入基准的二叉检索树,后续d行输入需要和基准二叉树比较的序列。依次判断d个序列生成二叉检索树是否和基准序列相同。

例如:
4 2
3 1 4 2
3 4 1 2
3 2 4 1

思路:
        解法1.不建树的判别方法:(使用递归的方式完成判断)
        Step1 以第一个结点为基准结点,判断待比较的两序列基准节点是否相同,不同则表示生成的二叉检索树不同,返回;若相同则按照原序列顺序,将后续结点根据大于、小于基准结点分成两个子序列。
        Step2 对于两个子序列,继续重复上一步,直到子序列中仅一个结点,返回。

        解法2.建一棵树,再判别其他序列是否与该树一致
        Step1 建树,树的参数有键值、左子结点、右子结点、判断是否被遍历过的flag标志
        Step2 判别一序列是否与基准树一致
● 判别方法:在基准树中按顺序搜索待比较序列中的每个数
        如果每次搜索所经过的结点在前面均出现过,则一致;
        否则(某次搜索中遇到前面未出现的结点),则不一致,退出判断。
注意:在遍历下一序列时需要抹去基准树中被此次遍历的痕迹。
● 代码实现:构造judge方法,判断在搜索当前结点时所经过的结点是否在前面均出现过:
        若遍历过当前结点(即该结点的flag为true),则值为key应该继续往下插入,判断key值与当前结点key值的关系
                1.若相等,由于K型二叉检索树中不可能出现重复的结点,当前序列出错
                2.若不相等,判断值为key的结点的插入方向
        若当前结点没遍历过:要么是当前结点的插入位置,将该结点的flag置为true,要么是序列不同,返回false。

import java.util.Scanner;

public class BSTJudgeSame {
    private class BSTNode {
        int key;
        BSTNode left;
        BSTNode right;
        boolean flag;

        BSTNode(int key) {
            this.key = key;
            this.left = null;
            this.right = null;
            this.flag = false;
        }
    }

    public static void main(String[] args) {
        BSTJudgeSame bst = new BSTJudgeSame();
        int n, d;
        Scanner in = new Scanner(System.in);
        do {
            System.out.println("输入检索树结点数目:");
            n = in.nextInt();
        } while (n <= 0);
        do {
            System.out.println("输入需要与基准二叉树比较的二叉树序列数目:");
            d = in.nextInt();
        } while (d <= 0);

        int[] baseNode = new int[n];
        for (int i = 0; i < n; i++) {
            baseNode[i] = in.nextInt();
        }
        BSTNode root = bst.makeTree(n, baseNode);

        for (int i = 0; i < d; i++) {
            int[] judgeNode = new int[n];
            for (int j = 0; j < n; j++) {
                judgeNode[j] = in.nextInt();
            }
            boolean isSame = true;
            for (int j = 0; j < n && isSame; j++) {
                if (!bst.judge(root, judgeNode[j])) isSame = false;
            }
            if (isSame)
                System.out.println("is Same!");
            else
                System.out.println("not Same!");
            bst.resetTree(root);//清除标记的flag
        }
    }

    void preOrderByRecursion(BSTNode root) {
        if (root == null) return;//空树
        System.out.println(root.key);
        preOrderByRecursion(root.left);
        preOrderByRecursion(root.right);
    }

    private BSTNode makeTree(int nodeNum, int[] baseNode) {
        BSTNode root = new BSTNode(baseNode[0]);
        for (int i = 1; i < baseNode.length; i++) {
            root = insert(root, baseNode[i]);
        }
        return root;
    }

    private BSTNode insert(BSTNode rt, int key) {
        if (rt == null)
            return new BSTNode(key);//找到插入位置,生成并返回一个结点的二叉搜索树,作为上一层的子结点。
        //否则继续找要插入元素的位置
        if (key < rt.key)
            rt.left = insert(rt.left, key);
        else if (key > rt.key)
            rt.right = insert(rt.right, key);
        return rt;
    }

    private boolean judge(BSTNode rt, int key) {
        if (rt.flag) {//遍历过当前结点,故值为key应该继续往下插入
            if (key == rt.key) return false; //K型二叉检索树中不可能出现重复的结点,当前序列出错
            else { //判断值为key的结点的插入方向
                if (key < rt.key) return judge(rt.left, key);
                else return judge(rt.right, key);
            }
        } else {//当前结点没遍历过:要么是当前结点的插入位置,要么是序列不同
            if (rt.key == key) {
                rt.flag = true;
                return true;
            } else
                return false;
        }
    }

    private void resetTree(BSTNode rt) {
        if (rt.left != null) resetTree(rt.left);
        if (rt.right != null) resetTree(rt.right);
        rt.flag = false;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值