数据结构(七):二叉树的删除和应用举例

今天这节课,我们讲一下与二叉树相关的最后一个问题,如何从二叉树里删除一个元素。以及举个例子来看怎么使用二叉树。本节课是小密圈《进击的Java新人》第五周的第四节课。

删除结点

第一种情况,如果待删除结点只有一个孩子,那么直接把这一个孩子挂接在待删结点的父亲结点上即可,无论这个孩子是左子树还是右子树都不会有区别。第二种情况,如果待删除结点有两个孩子,那么待删除结点的中序后继一定是在右子树上。而且后继结点最多只会有一个右孩子,一定不会有左孩子。把后继结点中的值拷贝到待删除结点后,就变成了第一种情况,这时再删除后继结点即可。如图2.3所示。(a)中蓝色表示待删除结点,因为它有两个孩子,所以无法将其直接从树中去掉,红色结点为蓝色结点的中序后继;(b)中把后继中的值拷贝到待删除结点中去;(c)只需要将红色结点删除即可。

然后看一下删除的代码。

    public void remove(Node<T> n) {
        Node<T> p = n.parent;
        Node<T> next, child;

        // 叶子结点,直接删除即可。要考虑待删除结点是root的情况。
        if (n.left == null && n.right == null) {
            if (n == root) {
                root = null;
                return;
            }

            if (n == p.left)
                p.left = null;
            else if (n == p.right)
                p.right = null;
        }
        // 内部结点,把它的后继的值拷进来,然后递归删除它的后继。
        else if (n.left != null && n.right != null) {
            next = successor(n);
            n.data = next.data;
            remove(next);
        }
        // 只有一个孩子的结点,把它的孩子交给它的父结点即可。
        else {
            if (n.left != null)
                child = n.left;
            else
                child = n.right;

            if (n == root) {
                child.parent = null;
                root = child;
                return;
            }

            if (n == p.left) {
                child.parent = p;
                p.left = child;
            }
            else {
                child.parent = p;
                p.right = child;
            }
        }
    }

好了。二叉树的基本操作,我们就介绍完了。接下来,我会介绍一下与二叉树相关的应用,以及与二叉树的性质相关的题目。

四则运算表达式的语法树

学完了二叉树,我们再回过头来看表达式求值。表达式求值,是个大宝库啊,我们通过它挖掘了很多知识。这一次,我们还是用递归下降的方法去处理,只是不再直接求值了,而是把它转化成一棵树。例如,(1+2)*3,我们把它转成下图这个样子:

后序遍历这棵树的结果是12+3*,这就是我们习题中所讲过的逆波兰表达式,也叫后缀表达式,大家还记得吗?实际上,我们在后序遍历这棵树的同时,也可以把表达式的值给求出来。但我们还有更远大的目标(不是星辰大海),我们能把这个翻译成Java字节码,最终变成class文件存起来,然后使用java程序去执行它。这是后话,我们今天先看,怎么得到这棵树,以及如何通过遍历树来计算表达式求值。

实际上,这棵树,就是大名鼎鼎的语法树。所有的编译器都会经过词法分析,文法分析,然后得到语法树,再在语法树上进行各种初步的优化,再由它生成机器码(或者是字节码)。我们以后还经常会和这棵树打交道。

其实,这个过程也很简单,那就是递归下降分析的时候,原来是求值,现在把值转成树上的结点就好了。代码很简单,如果做过前边的习题的同学,看一眼就知道,其实代码并没有发生太多变化,只是从原来的直接求值变成了返回一个二叉树结点。

public class BinaryTreeExpression {
    public TokenStream ts;

    public static void main(String args[]) {
        BinaryTreeExpression e = new BinaryTreeExpression();
        BinarySearchTree<Token> bst = new BinarySearchTree<Token>();
        bst.root = e.expression();
        bst.postOrder(bst.root);
    }

    public BinaryTreeExpression() {
        ts = new ExpressionTokenStream(new BufferedInputStream(System.in));
    }

    public Node<Token> expression() {
        Node<Token> left = term();

        try {
            if (match(PLUS)) {
                Node<Token> root = new Node<>(new Token(Token.TokenType.PLUS, "+"));
                root.left = left;
                root.right = expression();
                return root;
            }
            else if (match(MINUS)) {
                Node<Token> root = new Node<>(new Token(Token.TokenType.MINUS, "-"));
                root.left = left;
                root.right = expression();
                return root;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return left;
    }

    private Node<Token> term() {
        Node<Token> left = factor();

        try {
            if (match(MULT)) {
                Node<Token> root = new Node<>(new Token(Token.TokenType.MULT, "*"));
                root.left = left;
                root.right = term();
                return root;
            }
            else if(match(DIV)){
                Node<Token> root = new Node<>(new Token(Token.TokenType.DIV, "/"));
                root.left = left;
                root.right = term();
                return root;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return left;
    }

    private Node<Token> factor() {
        Token t = null;
        Node<Token> left;
        try {
            t = ts.getToken();

            if (t.tokenType == INT) {
                ts.consumeToken();
                return new Node<Token>(t);
            }
            else if (match(LPAR)) {
                Node<Token> v = expression();
                if (!match(RPAR))
                    assert false;
                return v;
            }
            else {
                throw new IOException("Illegal Expression!");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        // should not reach here
        return null;
    }

    private boolean match(Token.TokenType tt) throws IOException {
        if (ts.getToken().tokenType == tt) {
            ts.consumeToken();
            return true;
        }
        return false;
    }
}

这个程序里,我们先把一个表达式转成了一棵语法树。然后后序遍历这棵树,就得到了后缀表达式。今天的课程就到这里了。

作业:

1. 想想看,中序遍历这棵语法树,会得到什么结果?运行一下,看看是不是你期望的结果。尤其是括号。

2. 还是老问题,负数怎么处理?

3. 尝试修改一下postOrder,看看能不能在遍历语法树的同时对表达式进行求值。

作业的答案已经上传到小密圈里了。圈里的同学注意查看。

上一节课:Java中的设计模式之迭代器

下一节课:Java网络编程(一)

目录:课程目录

=========== 广告时间 ====================

课程内容都会发在知乎专栏里,不加小密圈也可以看。小密圈的作用,除了发布作业的答案,还有作业的讲解。小密圈是付费的,但是收费刚好是一顿必胜客套餐。持续一年的课程,只需要请我吃一顿饭哦 :)

 (二维码自动识别)

posted @ 2017-05-30 09:47  天涯海角路  阅读(463)  评论(0)    收藏  举报