vlambda博客
学习文章列表

算法笔记-7:平衡二叉树(代码篇)

        还记得二叉搜索树的代码怎么写吗?我当时是每个功能分开写的。现在我将他们整合在一起,一定要区分出哪些代码属于Node类,哪些属于Tree类。

//节点类class Node {    //值、左子节点、右子节点 private int value; private Node left; private Node right;  public Node(int value) { this.value = value; } public int getValue() { return this.value; }   public Node getLeft() { return left; } public void setLeft(Node left) { this.left = left; }  public Node getRight() { return right; } public void setRight(Node right) { this.right = right; }    //搜索节点     public Node search(int value) { if (value == this.value) { return this; } else if (value < this.value) { if (this.left == null) { return null; } return this.left.search(value); } else { if (this.right == null) { return null; } return this.right.search(value); } } }    //添加节点     public void add(Node node) { if (node == null) { return; } if (node.value < this.value) { this.left = node; } else { this.left.add(node); } } else if (node.value > this.value) { if (this.right == null) { this.right = node; } else { this.right.add(node); } } else { return; } }    //删除节点     public void delete() { if (this == null) { return; } if (this.left == null) { if (this.right == null) { this = null; return; } else {                this.value = this.right.getValue();                this.left = this.right.left();                this.right = this.right.right(); return; } } else { if(this.right == null) { this.value = this.left.getValue();                this.left = this.left.left();                this.right = this.left.right(); return; } else { Node aimNode = this.right.minNode(); this.value = aimNode.getValue(); aimNode.delete(); return; } } }    //搜索最小节点     Public Node minNode() { if (node == null) { return null; } if (node.left == null) { return this; } else { return this.left.minNode(); } }}//树类class Tree{ private Node root; public Node getRoot() { return root;    }    //搜索节点          public Node search(int value) { if (root == null) { return null; } else { return root.search(value); } } //添加节点  public void addNode(Node node) { if (root == null) { root = node; } else { root.add(node); } } //删除节点  public void delNode(int value) { if (root == null) { return; } else { Node targetNode = search(value); targetNode.delete();  }  }}    


        现在,我们要在二叉搜索树代码的基础上构建平衡二叉树的代码。首先,我们在二叉搜索树中忽略的“树高”这个概念,需要体现在平衡二叉树的代码中了。

        另外,代码中的树高,要在《离散数学结构》中定义的树高上+1。在《离散数学结构》中,空树意味着不存在树高,一个根节点视为树高为0。

图1-《离散数学》中定义的树高

        但是,要比较左右子树的高度差。肯定是要区分子节点只有一个节点和子节点为空的区别的。因此,定义空树高为0,单节点树高为1。

        

算法笔记-7:平衡二叉树(代码篇)

图2-实际应用中的树高

        这种为了方便实际使用,把一些概念定义得与数学书不一样的情况还有很多。例如红黑树会将空节点定义成叶子,那样每个非空节点都一定有左右子节点。这里就不展开讲了。

        记住空树为0,单根树为1,之后往上加码就好。

        然后我们在Node类里写获取高度及平衡因子的方法:

Node类:

    public int height() {                     return Math.max(this.leftH(), this.rightH()) + 1;         }     public int leftH() { if(this.left == null) {            return 0;         } else {            return this.left.height();         }        }        public int rightH() { if(this.right == null) {            return 0;         } else { return this.right.height();  }        }        public int avlE() {        return this.leftH() - this.rightH();                }

        在这里我们把求节点高的方法和求左右子树的方法采用了嵌套调用。这样使代码更加简洁了。但要理解代码就得花费一番功夫了。


        二是只写一个按照节点的值获取父节点的方法。每次都从root开始搜索值的位置,找到节点后返回其父节点对象。这样做的话,会牺牲一些程序效率,但是无需更改太多代码。

        

        我们采用第二种途径,先在Node类里写一个从结点开始向下搜索父节点的方法:

Node类:

    //这个searchParent方法,是放在Node类里的 public Node searchParent(int value) { if ((this.left != null && this.left.value == value) || (this.right != null && this.right.value == value)) { return this; } else { if (value < this.value && this.left != null) { return this.left.searchParent(value); } else if (value >= this.value && this.right != null) { return this.right.searchParent(value); } else { return null; } } }

        之后再从Tree类里调用这个方法,把根节点作为参数:

Tree类:

 //这个SearchParent方法,是放在Tree类里的  public Node searchParent(int value) { if (root == null) { return null; } else { return root.searchParent(value); } }


        接下来就是重头戏:节点旋转代码的编写。

        考查左旋过程,失衡节点40为主要节点Node,它以及与它相关的35、45、50、60均有位置变化,如下图:

算法笔记-7:平衡二叉树(代码篇)

图3-左旋过程


        当Node变成了Node.left。我们可以直接写一句代码Node.left=Node吗?这样做会带来可怕的后果。

算法笔记-7:平衡二叉树(代码篇)

图4-Node.left = Node

        Node.left指向了Node自己,这意味着它原来的左子树会从整棵树中脱离,而向左子树的访问形成了不断访问自己的死循环。

        同样地,Node = Node.right的做法,是将Node.right的指针赋给了Node。但是Node的父节点指向原Node的指针没有变。所以Node.right实际上并没有取代Node原来的位置。

        正确的做法是,创建一个新的newNode,将Node的值和左子树赋给它。然后将Node.right节点的值和右子树赋给Node,如下表:

算法笔记-7:平衡二叉树(代码篇)

图5-将Node值赋给newNode

        写成代码:

Node类:

 private void leftRotate() { Node newNode = new Node(this.value);        newNode.left = this.left; newNode.right = this.right.left; this.value = this.right.value;        this.right = this.right.right; this.left = newNode; }


        同样的,右旋过程可以如下:

算法笔记-7:平衡二叉树(代码篇)

图6-右旋过程

        写成代码:

Node类:

 private void rightRotate() { Node newNode = new Node(value); newNode.right = this.right;        newNode.left = this.left.right; this.value = this.left.value;        this.left = this.left.left;        this.right = newNode; }



        有了左旋和右旋的代码,我们需要在插入和删除过程中增加对失衡情况的判断和相应旋转操作,如下:

Node类:

 public void add(Node node) { if (node == null) { return; } if (node.value < this.value) { this.left = node; } else { this.left.add(node); } } else if (node.value > this.value) { if (this.right == null) { this.right = node; } else { this.right.add(node); } } else { return;        }
//新增失衡调整代码               if (this.avlE()>1) {    //平衡因子大于1,为LL或LR类型            if (this.left != null && this.left.avlE() < 0) {    //LR类型             this.left.leftRotate();                this.rightRotate();             } else {    //LL类型                this.rightRotate();            }        } else if (this.avlE()<-1) {    //平衡因子小于-1,为RR或RL类型             if (this.right != null && this.right.avlE() > 0) { //RL类型 this.right.rightRotate(); this.leftRotate(); } else { //RR类型 this.leftRotate(); }        }       }

        在增加节点的add方法中,递归调用了add方法,这意味着每次调用add方法都会检查当前节点的平衡因子,并在失衡时做出旋转操作。

        而因为检查和调整平衡性的代码放在递归调用add后面。这意味着平衡性检查是自下而上的——即先完成叶子节点的添加,再一步步返回父节点检查平衡因子。

        

图7-add方法的递归与回溯过程

        因为Tree类中addNode会从根节点开始,所以回溯也会检查到根节点,完成整棵树的平衡性检查。但是在Tree类中的delNode方法中,是先从根节点搜索到被删节点,然后针对被删节点进行删除操作的。这就无法利用递归方法遍历每个父节点的平衡因子了,该怎么办呢?

        一种做法是将delete方法从Node类里搬出去,这样相当于要重新写delNode方法,其中包含删除节点和遍历父节点的平衡性与调整。

        可如果我们不想扔掉delete方法的代码,就需要花点心思了。

        前面我们反复说了删除一共三种情况:

        1、被删节点没有子节点时,不需要替代节点;

        2、被删节点只有一个子节点时,用该子节点直接替代;

        3、被删除节点有两个子节点时,寻找替代节点。

图8-寻找替代节点的删除情况

        前两种情况中,可以直接从被删节点的父节点开始检查平衡性,但是遇到需要寻找替代节点的情况时,需要从替代节点的父节点开始检查平衡性。

        那么,我在delNode方法中,

        1、先找到目标父节点(被删节点或替代节点的父节点);

        2、然后用原来的代码完成删除过程;

        3、最后从目标父节点开始递归调用检查平衡性和调整平衡性的方法。

        这样就可以实现AVL树的删除。

Tree类:

 public void delNode(int value) { if (root == null) { return; } else { //寻找目标父节点  Node targetNode = search(value); Node targetParent = searchParent(value);            if (targetNode = null) { return;            } else if (targetNode.left != null && targetNode.right != null) {                                Node minNode = targetNode.right;                while (minParent.left != null) {                 targetParent = minParent;                    minNode = minParent.left;                }                            } //执行删除              targetNode.delete(); //从目标父节点开始检查平衡性  do {                if (targetParent.avlE()>1) { //平衡因子大于1,为LL或LR类型 if (targetParent.left != null && targetParent.left.avlE() < 0) { //LR类型 targetParent.left.leftRotate(); targetParent.rightRotate(); } else { //LL类型 targetParent.rightRotate(); } } else if (targetParent.avlE()<-1) { //平衡因子小于-1,为RR或RL类型  if (targetParent.right != null && targetParent.right.avlE() > 0) { //RL类型  targetParent.right.rightRotate(); targetParent.leftRotate(); } else { //RR类型  targetParent.leftRotate(); } }                targetParent = searchParent(targetParent.value);                             } while (targetParent!= null)         }  }


        至此,我们就完成了平衡二叉树代码的构建。各位需要多在自己的IDE上实践这段代码。