概述
树 是一种经常用到的数据结构,用来模拟具有树状结构性质的数据集合。
树里的每一个节点有一个值和一个包含所有子节点的列表。
从图的观点来看,树也可视为一个拥有N 个节点和N-1 条边的一个有向无环图。
二叉树是一种更为典型的树状结构。
如它名字所描述的那样,二叉树是每个节点最多有两个子树的树结构,通常子树被称作“左子树”和“右子树”。
完成后,你将:
1、掌握树和二叉树的概念
2、熟悉不同的遍历方法
3、运用递归解决二叉树相关题目
树的遍历
树和二叉树的基本概念。
本章学习、理解和区分树的遍历方法
能够运用递归方法解决树的为前序遍历、中序遍历和后序遍历问题
能用运用迭代方法解决树的为前序遍历、中序遍历和后序遍历问题
能用运用广度优先搜索解决树的层序遍历问题
树的遍历介绍
前序遍历
前序遍历首先访问根节点,然后遍历左子树,最后遍历右子树。
root - left - right
请看下面的例子:
中序遍历
中序遍历是先遍历左子树,然后访问根节点,然后遍历右子树。
left - root - right
通常来说,对于二叉搜索树,我们可以通过中序遍历得到一个递增的有序序列。
我们将在另一张卡片(数据结构介绍 – 二叉搜索树)中再次提及。
让我们一起来看树的中序遍历:
后序遍历
后序遍历是先遍历左子树,然后遍历右子树,最后访问树的根节点。
left - right - root
我们一起来看后序遍历的动画演示:
值得注意的是,当你删除树中的节点时,删除过程将按照后序遍历的顺序进行。
也就是说,当你删除一个节点时,你将首先删除它的左节点和它的右边的节点,然后再删除节点本身。
您可以使用中序遍历轻松找出原始表达式。
但是程序处理这个表达式时并不容易,因为你必须检查操作的优先级。
如果你想对这棵树进行后序遍历,使用栈来处理表达式会变得更加容易。
每遇到一个操作符,就可以从栈中弹出栈顶的两个元素,计算并将结果返回到栈中。
递归和迭代
请练习文章后面习题中的三种遍历方法。
您可以通过递归或迭代方法实现算法,并比较它们之间的差异。
二叉树的前序遍历
递归算法 & 迭代算法
递归算法
使用前序遍历的特性:root-left-right
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)
迭代算法
使用stack来实现前序遍历
先将左节点加入list并push到栈
右节点,通过pop出,已经被add过的根节点的方式来获得。
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)
//树节点的基本数据结构
public class TreeNode
{
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val = 0, TreeNode left = null, TreeNode right = null)
{
this.val = val;
this.left = left;
this.right = right;
}
}
//144.二叉树前序遍历:递归方式
public IList<int> PreorderTraversal_1(TreeNode root)
{
IList<int> list = new List<int>();
if (root == null) return list;
Preorder(root,list);
return list;
}
//递归方法
private void Preorder(TreeNode root, IList<int> list)
{
list.Add(root.val);
if (root.left != null) Preorder(root.left,list);
if (root.right != null) Preorder(root.right,list);
}
//144.二叉树前序遍历:迭代方式
public IList<int> PreorderTraversal_2(TreeNode root)
{
IList<int> list = new List<int>();
if (root == null) return list;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode curNode = root; //当前节点
while (stack.Count!=0 || curNode !=null)
{
while (curNode!=null)
{
list.Add(curNode.val);
stack.Push(curNode); //压入栈
curNode = curNode.left;
}
//当前为左节点为空节点
curNode = stack.Pop(); //把上一个根节点压出栈
curNode = curNode.right; //获取右节点
}
return list;
}
二叉树的中序遍历
递归算法 & 迭代算法
递归算法
使用中序遍历的特性:left-root-right
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(log n),最坏情况下树呈现链状,为 O(n)
迭代算法
使用stack来实现中序遍历
现将左节点PUSH到栈里,获取下一个左节点
如果左节点为空,就将栈里POP出,赋值给list,然后获取到Right右节点,一次迭代
时间复杂度:O(n),其中 n是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
//94. 二叉树的中序遍历:递归算法
public IList<int> InorderTraversal_1(TreeNode root)
{
IList<int> list = new List<int>();
Inorder(root, list);
return list;
}
private void Inorder(TreeNode root, IList<int> list)
{
if (root == null) return; //空节点直接返回
Inorder(root.left, list);
list.Add(root.val);
Inorder(root.right, list);
}
//94. 二叉树的中序遍历:迭代算法
public IList<int> InorderTraversal_2(TreeNode root)
{
IList<int> list = new List<int>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode curNode = root;
while (stack.Any() || curNode != null)
{
while (curNode != null)
{
stack.Push(curNode); //压入栈
curNode = curNode.left; //左节点
}
root = stack.Pop();
list.Add(root.val); //出栈第一个
curNode = root.right; //右节点
}
return list;
}
二叉树的后序遍历
递归算法 & 迭代算法
时间复杂度空间复杂度都是O(n)
//145. 二叉树的后序遍历 :递归
public IList<int> PostorderTraversal_1(TreeNode root)
{
IList<int> list = new List<int>();
Postorder(root, list);
return list;
}
private void Postorder(TreeNode root, IList<int> list)
{
if (root == null) return; //空节点直接返回
Postorder(root.left, list);
Postorder(root.right, list);
list.Add(root.val);
}
//145. 二叉树的后序遍历 :迭代
public IList<int> PostorderTraversal_2(TreeNode root)
{
IList<int> list = new List<int>();
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode curNode = root;
TreeNode preNode = null; //代表此刻左节点
while (stack.Any() || curNode != null)
{
while (curNode != null)
{
stack.Push(curNode);
curNode = curNode.left; //判断还有没有左节点
}
curNode = stack.Pop(); //此节点没有左节点或者节点置空了,压出上一个赋值
if (curNode.right == null || curNode.right == preNode) //当前没有右节点,右节点为null
{
list.Add(curNode.val); //把节点添加list
preNode = curNode; //把当前节点给preNode
curNode = null; //当前根节点置空
}
else //当前有右节点,不为空,更节点再次入栈
{
stack.Push(curNode); //重新入栈
curNode = curNode.right;
}
}
return list;
}
总结
二叉树前中后序的递归版本属于easy题,而迭代版本通常是medium甚至是hard。
在做迭代版本之前,我建议大家先问问各类“遍历”算法的本质是什么?是最后输出的那一串有序的数字吗?数字的顺序是对的,遍历算法就是对的吗?
个人认为,以上问题的答案都应该是:否。“遍历”的本质是对内存的有序访问,失去了访问顺序,即便你用各种数据结构恢复了这个次序,遍历本身也显得毫无意义。常见的后序遍历写法中有一种已经出现在评论区了——它的思想也很简单,大家做过单词串翻转吗?
String in = “it is a word”
String out = “word a is it”
这个问题有一种很优雅的写法是先reverse(in),然后再逐词翻转——“两次反转”,最后得到的就是正确顺序。
回到二叉树后序遍历,你也可以利用这种思想,利用双向链表的addFirst,对外部次序和内含次序进行同时翻转,同样会得到一种非常”优雅”的解法,结构简单明晰,甚至还有点好背(狗头)。但是,它并没有真正实现“遍历”——仔细看会发现,该算法其实在使用一种异构的前序遍历:“中->右->左”,而非传统意义上的“中->左->右”,而这种异构也正是他的第一次反转。而第二次反转就在输出序列上。
所以本质上,这是一个“前序遍历”,而不是所谓的“后序遍历”。只有当你的各个节点以“左->右->中”的次序依次出现在迭代的loop当中时,它才是真正的后序遍历,就像官解那样。贴个我个人的版本。
class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> ans = new ArrayList<>();
Stack<TreeNode> s = new Stack<>();
Set<TreeNode> seen = new HashSet<>();
while (root != null || !s.isEmpty()) {
if (root == null && seen.contains(s.peek())) {
ans.add(s.pop().val);
} else if (root == null) {
seen.add(s.peek());
root = s.peek().right;
} else {
s.push(root);
root = root.left;
}
}
return ans;
}
}