基于Java的二叉树的三种遍历方式的递归与非递归实现

二叉树的遍历方式包括前序遍历中序遍历后序遍历,其实现方式包括递归实现非递归实现

前序遍历:根节点 | 左子树 | 右子树

中序遍历:左子树 | 根节点 | 右子树

后序遍历:左子树 | 右子树 | 根节点

1. 递归实现

递归方式实现代码十分简洁,三种遍历方式的递归实现代码结构相同,只是执行顺序有所区别。

前序遍历:

public class preOrderRecur {
    List<Integer> res = new ArrayList<>();
    public List<Integer> preOrderTraversal(TreeNode root) {
        if (root != null) {
            res.add(root.val); // 根节点
            preOrderTraversal(root.left); // 左子树
            preOrderTraversal(root.right); // 右子树
        }
        return res;
    }
}

中序遍历:

public class inOrderRecur {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inOrderTraversal(TreeNode root) {
        if (root != null) {
            inOrderTraversal(root.left); // 左子树
            res.add(root.val); // 根节点
            inOrderTraversal(root.right); // 右子树
        }
        return res;
    }
}

后序遍历:

public class inOrderRecur {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inOrderTraversal(TreeNode root) {
        if (root != null) {
            inOrderTraversal(root.left); // 左子树
            inOrderTraversal(root.right); // 右子树
            res.add(root.val); // 根节点
        }
        return res;
    }
}

2. 迭代实现

2.1 使用辅助栈——空间复杂度O(N)

2.1.1 中序遍历
  • 从当前结点一直向其最左孩子搜索,直到没有左孩子了停止,这个过程中将路程中的所有结点入栈;
  • 弹出栈顶元素,将其记录在答案中,并把当前结点置为弹出元素的右孩子并重复第一步过程。
public class inOrderIterator {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inOrderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        while (root != null || !stack.isEmpty()) {
            if (root != null) {
                stack.push(root);
                root = root.left;
            } else {
                TreeNode node = stack.pop();
                res.add(node.val);
                root = node.right;
            }
        }
        return res;
    }
}
2.1.2 前序遍历

方法1:因为前序遍历访问顺序是“中-左-右”,所以可以先将根结点压栈,然后按照下列步骤执行。

  • 如果栈不为空,则弹出栈顶元素存入结果中;
  • 如果弹出元素的右孩子不为空则将右孩子压栈,然后如果其左孩子也不为空将其左孩子压栈(因为栈是后入先出的,所以为了达到“中-左-右”的顺序,需要先压入右孩子,再压入左孩子)。
public class preOrderIterator {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inOrderTraversal(TreeNode root) {
        if (root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            root = stack.pop();
            res.add(root.val);
            // 右孩子压栈
            if (root.right != null) stack.push(root.right);
            // 左孩子压栈
            if (root.left != null) stack.push(root.left);
        }
        return res;
    }
}

方法2:根据中序遍历进行微调:

public class preOrderIterator {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inOrderTraversal(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        while (root != null || !stack.isEmpty()) {
            if (root != null) {
                res.add(root.val);
                stack.push(root);
                root = root.left;
            } else {
                TreeNode node = stack.pop();
                root = node.right;
            }
        }
        return res;
    }
}
2.1.3 后序遍历

因为前序遍历的顺序是“左-中-右”,而后序遍历顺序是“左-右-中”,不考虑左结点,区别只是在于中结点和右结点的顺序进行了反向而已,因此可以使用前序遍历的代码进行调整,只需要将前序遍历对左右孩子压栈的顺序反向即可,即先压入左孩子,再压入右孩子。除此之外,因为按照这种方法调整得到的遍历顺序为“中-右-左”,正好是后序遍历的反向顺序,因此在获得遍历序列后还需进行逆序操作。

public class postOrderIterator {
    List<Integer> res = new LinkedList<>();
    public List<Integer> postOrderTraversal(TreeNode root) {
        if (root == null) return res;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            root = stack.pop();
            // 头插法
            res.add(0, root.val);
            // 左孩子压栈
            if (root.left != null) stack.push(root.left);
            // 右孩子压栈
            if (root.right != null) stack.push(root.right);
        }
        return res;
    }
}

2.2 Morris遍历——空间复杂度O(1)

该方法的思路简单说就是,对于每一个结点,找到它左孩子的最右子结点,因为按照正常访问顺序,其左孩子的最有子节点访问完后就应该访问其本身了,因此将其左孩子最右子节点的右指针指向它。基本步骤如下:

  • 如果当前结点左孩子为空,说明最左边访问完毕,将其置为其右孩子
  • 如果当前结点左孩子不为空,那么开始尝试找到该结点左孩子的最右子节点,建立连接关系
    • 如果找到的当前结点的左孩子的最右子节点右指针为空,说明还未建立连接关系,是首次访问当前结点,那么将该最右结点的右指针指向当前结点,然后当前结点向左孩子走一步继续重复所有步骤。
    • 如果找到的当前结点的左孩子的最右子节点右指针不为空,说明已建立过连接关系,是第二次访问当前结点,这意味着当前结点的左子树应该已经全部遍历完了,此时应恢复连接关系重新置为空,然后当前结点向右孩子走一步继续重复所有步骤。

该方法虽然保证了O(1)的空间复杂度,但在遍历过程中改变了部分结点的指向,破坏了树的结构。

2.2.1 中序遍历
public class inOrderMorris {
    List<Integer> res = new ArrayList<>();
    public List<Integer> inOrderTraversal(TreeNode root) {
        TreeNode pre = null;
        TreeNode cur = root;
        while (cur != null) {
            if (cur.left == null) {
                res.add(cur.val);
                cur = cur.right;
            } else {
                pre = cur.left;
                while (pre.right != null && pre.right != cur) pre = pre.right;
                if (pre.right == null) {
                    pre.right = cur;
                    cur = cur.left;
                } else {
                    res.add(cur.val);
                    pre.right = null;
                    cur = cur.right;
                }
            }
        }
        return res;
    }
}
2.2.2 前序遍历
public class preOrderMorris {
    List<Integer> res = new ArrayList<>();
    public List<Integer> preOrderTraversal(TreeNode root) {
        TreeNode pre = null;
        TreeNode cur = root;
        while (cur != null) {
            if (cur.left == null) {
                res.add(cur.val);
                cur = cur.right;
            } else {
                pre = cur.left;
                while (pre.right != null && pre.right != cur) pre = pre.right;
                if (pre.right == null) {
                    res.add(cur.val);
                    pre.right = cur;
                    cur = cur.left;
                } else {
                    pre.right = null;
                    cur = cur.right;
                }
            }
        }
        return res;
    }
}
2.2.3 后序遍历

前序遍历反向的思想

public class postOrderMorris {
    List<Integer> res = new LinkedList<>();
    public List<Integer> postOrderTraversal(TreeNode root) {
        TreeNode pre = null;
        TreeNode cur = root;
        while (cur != null) {
            if (cur.right == null) {
                res.add(0, cur.val);
                cur = cur.left;
            } else {
                pre = cur.right;
                while (pre.left != null && pre.left != cur) pre = pre.left;
                if (pre.left == null) {
                    res.add(0, cur.val);
                    pre.left = cur;
                    cur = cur.right;
                } else {
                    pre.left = null;
                    cur = cur.left;
                }
            }
        }
        return res;
    }
}