树是常用的数据结构,树的遍历算法可以在多种许多情况中都有使用。接下来介绍树的遍历,常见的遍历右前序遍历、中序遍历、后序遍历、层序遍历。 树的遍历方式,会用到许多知识:
- 递归
- Stack(栈)
- Queue(队列)
前序遍历(深度优先遍历)
前序遍历的访问顺序为:
- 访问根节点
- 前序遍历访问左子树
- 前序遍历访问右子树 对于递归访问,我们先明确该函数表达的含义:按照前序遍历的顺序访问以root参数为根节点的树。那么根据前序遍历的定义我们得到如下的函数:
//递归版
private void selfPreorderTraversal(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
list.add(root.val);
selfPreorderTraversal(root.left, list);
selfPreorderTraversal(root.right, list);
}
对于前序遍历的非递归实现,我们先来思考前序遍历的过程 对树中的一个节点,对它的访问是:1)先访问当前节点;2)再访问其左子树;3)最后访问其右子树。现在的问题是:我们访问其左子树的结束后,如何继续访问其右子树。再递归的实现中,是利用系统分配的函数栈来记录还没有访问右子树的节点。那么在非递归实现中我们就需要Stack数据结构来辅助我们完成访问。
具体的做法:
- 从根节点开始访问
- 对每一个节点访问属性,为了以后能访问其右子树,将该节点入栈,接着指向左孩子
- 当指向的节点为空的时候表明前一个节点的左子树访问结束,此时从Stack中弹出元素,继续访问其右节点,进行第二个过程
private void selfPreorderTraversalIterSingle(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode iter = root;
while(iter != null || !stack.isEmpty()) {
//从stack提出下一个遍历点
if (iter == null) {
iter = stack.pop();
iter = iter.right;
} else {
//此时iter一定不为空
//入栈的时候访问数据
list.add(iter.val);
stack.push(iter);
iter = iter.left;
}
}
}
中序遍历
定义:
- 中序遍历访问左子树
- 访问根节点
- 中序遍历访问右子树
//递归版
private void selfInorderTraversal(TreeNode root, List<Integer> res) {
if (root == null) {
return;
}
selfInorderTraversal(root.left, res);
res.add(root.val);
selfInorderTraversal(root.right, res);
}
中序遍历的非递归实现与前序类似,区别在于前序遍历是在入栈的时候访问数据,中序遍历是在出栈时候访问数据
//非递归
private void selfInorderTraversalIter(TreeNode root, List<Integer> list) {
if (root == null) {
return;
}
Stack<TreeNode> stack = new Stack<>();
TreeNode iter = root;
while(iter != null || !stack.isEmpty()) {
if (iter == null) {
//从stack提出下一个遍历点
iter = stack.pop();
//出栈的时候访问数据
list.add(iter.val);
iter = iter.right;
} else {
stack.push(iter);
iter = iter.left;
}
}
}
后序遍历
定义
- 后序遍历访问左子树
- 后序遍历访问右子树
- 访问根节点
//递归
private void selfRecursionPostorderTraversal(TreeNode root, List<Integer> res) {
if (root == null) {
return;
}
selfRecursionPostorderTraversal(root.left, res);
selfRecursionPostorderTraversal(root.right, res);
res.add(root.val);
}
后序遍历的非递归实现:对于根节点的访问,后序遍历的访问顺序是:左-》右-》根,如果将这个顺序颠倒:根-》右-》左,这个顺序与前序遍历的根-》左-》右是否很相似。那么我们使用两个Stack,一个Stack用来实现后序遍历的逆序访问,一个栈(output)记录后序遍历逆序顺序,最后我们将output栈中的元素出栈,就是后序遍历的顺序。
//迭代
private void selfIterPostorderTraversal(TreeNode root, List<Integer> res) {
Stack<TreeNode> stack = new Stack<>();
Stack<TreeNode> output = new Stack<>();
TreeNode node = root;
/*
后序遍历对每一个节点的访问顺序是:左——》右——》中,其逆顺序就是中-》右-》左。
则根据其逆顺序入栈(有点类似于前序遍历),在从栈输出
*/
while (node != null || !stack.isEmpty()) {
if (node != null) {
stack.push(node);
output.push(node);
node = node.right;
} else {
node = stack.pop();
node = node.left;
}
}
while (output.size() > 0) {
TreeNode n = output.pop();
res.add(n.val);
}
}
层序遍历(广度优先遍历) 树的层序遍历就是广度优先遍历,可以利用Queue先进先出的特性。
- 先将根节点放入队列
- 接下来开始对队列左迭代操作,对与每一个Queue弹出的元素,访问其属性,并将其左孩子、右孩子以此放入队尾。直到队列为空。
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new LinkedList<>();
LinkedList<TreeNode> fifo = new LinkedList<>();
if (root == null) {
return result;
}
TreeNode temp;
fifo.add(root);
while (fifo.size() != 0) {
//每一回先将一层的结点放入队列,end作为每一层的结束标志
TreeNode end = fifo.getLast();
//存储每一层的结点值
List<Integer> storage = new LinkedList();
//以end作为上一层结点的结束标记,将队列中上一层的结点弹出,同时将下一层的结点放入队列
do {
temp = fifo.pop();
if (temp.left != null)
fifo.add(temp.left);
if (temp.right != null)
fifo.add(temp.right);
//System.out.println("value of node" + temp.val);
storage.add(temp.val);
}while(end != temp);
//将刚弹出的一层的结点存入结果
result.add(storage);
}
return result;
}