二叉树遍历-递归推导

710 阅读3分钟

前言

大学的知识还给老师了,不,根本就没有从老师那学到。一般对二叉树做前序、中序、后序遍历都会采用递归的方式,代码极简,逻辑清晰。这么优秀的代码难道是某位大神瞬间写出来的。我是菜鸟,想试试推导出来。

分析

概念

二叉树的概念想必大家都理解,一言以蔽之 ——二叉树是每个节点最多有两个子树的树结构,通常子树被称为左子树和右子树。

遍历

二叉树有几种遍历方式,前序、中序、后序还有层次遍历。本篇文章只考虑前序、中序、后序遍历。遍历顺序如下:

  • 前序遍历:根-左-右;
  • 中序遍历:左-根-右;
  • 后序遍历:左-右-根;

不知道有没有同学跟我一样经常把遍历顺序搞混,好吧,可能只有我。所以总结一句比较好记的话:前序、中序、后序,根节点的访问顺序;左节点、右节点,左节点优先右节点。

推导

为了方便理解二叉树的结构,我们引入一个概念子二叉树,我们可以认为每一个节点代表一棵子二叉树,这棵子二叉树可能具有左节点和右节点。

由此推之,一棵N个节点的二叉树由N棵子二叉树组成。每一棵子二叉树都是由当前根节点,左节点和右节点构成,它们的遍历方式一样,如果是前序遍历,遍历顺序是根-左-右。那么遍历N个节点的二叉树等于遍历N次子二叉树。

前序遍历一棵子二叉树

preOrder(TreeNode root, List<TreeNode> nodes) {
    // 遍历子二叉树根节点
    if (root != null) {
        nodes.add(root);
        
        // 遍历子二叉树左节点
        if (root.left != null) {
            nodes.add(root.left);
        }
        // 遍历子二叉树右节点
        if (root.right != null) {
            nodes.add(root.right);
        }
    }
}

观察上述代码发现,遍历左、右子节点的代码类似,抽取公共代码看看:

// 公共代码
travesalNode(TreeNode root, List<TreeNode> nodes) {
    if (root != null) {
        nodes.add(root);
    }
}

preOrder(TreeNode root, List<TreeNode> nodes) {
    // 遍历子二叉树根节点
    if (root != null) {
        nodes.add(root);
        
        // 遍历子二叉树左节点
        travesalNode(root.left, nodes);
        // 遍历子二叉树右节点
        travesalNode(root.right, nodes);
    }
}

继续观察,如果在子二叉树遍历中注释掉左节点和右节点的遍历,代码与抽取公共代码只有方法名不一样,尝试把方法替换为公共方法,代码如下:

// 比较,只有方法名不同
travesalNode(TreeNode root, List<TreeNode> nodes) {
    if (root != null) {
        nodes.add(root);
    }
}

preOrder(TreeNode root, List<TreeNode> nodes) {
    if (root != null) {
        nodes.add(root);
        
        <!--// 遍历子二叉树左节点-->
        <!--travesalNode(root.left);-->
        <!--// 遍历子二叉树右节点-->
        <!--travesalNode(root.right);-->
    }
}

// ---我是分割线---

// 替换结果:
TreeNode rootFlag = root;
travesalNode(TreeNode root, List<TreeNode> nodes) {
    if (root != null) {
        nodes.add(root);
        
        if (root == rootFlag) {
            // 遍历子二叉树左节点
            travesalNode(root.left);
            // 遍历子二叉树右节点
            travesalNode(root.right);
        }
    }
}

从上面的替换结果可以看到,子二叉树的前序遍历已经完成,它适用于任何一棵N个节点的二叉树的子二叉树,只有当节点为root时,才会去遍历左、右节点。那么如何把它应用在二叉树中呢?回顾上文所述——每一个节点代表一棵子二叉树。不仅只有当前子二叉树的根节点才需要遍历左、右子节点,左、右子节点分别也是一个子二叉树的根节点。尝试放开if (root == rootFlag){}限制,让每一个节点都有资格遍历它的左、右节点。代码如下:

travesalNode(TreeNode root, List<TreeNode> nodes) {
    if (root != null) {
        nodes.add(root);
        
        // 遍历子二叉树左节点
        travesalNode(root.left);
        // 遍历子二叉树右节点
        travesalNode(root.right);
    }
}

上述代码就是教科书式的递归前序遍历了。同理,中序遍历和后序遍历只需要改变遍历子二叉树根节点、左节点、右节点的顺序,网上都有这里就不赘述了。

总结感悟

任何一段优秀代码的背后都可能藏着缜密的逻辑推理和细节把控。编程的核心能力与数学同源——逻辑推理演绎。