树之习题分析上——树的遍历
一、树的前序遍历
(一)、题目需求
给你二叉树的根节点 root
,返回它节点值的 前序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,2,3]
```
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
示例 4:
输入:root = [1,2]
输出:[1,2]
示例 5:
输入:root = [1,null,2]
输出:[1,2]
提示:
- 树中节点数目在范围
[0, 100]
内 -100 <= Node.val <= 100
(二)、递归解法
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
List<Integer> left = preorderTraversal(root.left);
List<Integer> right = preorderTraversal(root.right);
result.add(root.val);
result.addAll(left);
result.addAll(right);
return result;
}
(三)、非递归解法
public List<Integer> preorderTraversalByStack(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
LinkedList<TreeNode> stack = new LinkedList();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode treeNode = stack.pop();
result.add(treeNode.val);
if (treeNode.right != null) {
stack.push(treeNode.right);
}
if (treeNode.left != null) {
stack.push(treeNode.left);
}
}
return result;
}
(四)、代码分析
1、递归解法分析
递归解法相对于非递归解法而言较为简单。本质上是将一颗树划分为3个部分:根节点、左半部分、右半部分。对于每一个结点皆如此进行递归划分,最终在划分结束后,按照前序遍历的规则:先访问根结点、再访问根的左结点、最后是访问根的右结点的顺序进行添加进结果集中。
2、非递归解法分析
1、利用栈的先进后出的特性,进行树的非递归前序遍历;并将根节点push()进入栈中。
LinkedList<TreeNode> stack = new LinkedList();
stack.push(root);
2、由于树的前序遍历的规则为:先访问根结点、再访问根的左结点、最后是访问根的右结点。同时栈的特性是先进后出,因此需要先判断右结点是否为空,若不为空则加入栈,再判断左结点是否为空,若不为空则加入栈。在有新的结点进入栈后,再不断将其推出栈,进行其左右子结点的判断。
while (!stack.isEmpty()) {
TreeNode treeNode = stack.pop();
result.add(treeNode.val);
if (treeNode.right != null) {
stack.push(treeNode.right);
}
if (treeNode.left != null) {
stack.push(treeNode.left);
}
}
(五)、流程图分析
1、递归流程图
1、初始状态
2、将其分为根结点和左右两个部分
3、遍历“1”的左结点,同时将其分为3个部分
4、遍历“2”结点的左结点,同时将其分为3个部分。“4”结点无左右子结点
5、遍历“2”结点的右结点,同时将其分为3个部分。“5”结点无左右子结点
6、“2”结点遍历结束,得出2结点的结果集
7、遍历“1”结点的右节点,并将其分为3个部分
8、遍历“3”结点的左结点,并将其分为3个部分。“6”结点无左右子结点
9、“3”结点遍历结束,得出其结果集
10、遍历结束,得到“1”结点对应的结果集
2、非递归流程图
1、初始状态
2、根节点入栈
3、“1”结点的左右子节点进入栈内,同时“1”结点进入结果集
4、“2”结点出栈,其左右子节点入栈,同时“2”节点进入结果集
5、“4”节点无左右子节点,因此“4”节点直接出栈,同时“4”节点进入结果集
6、“5”节点无左右子节点,因此“5”节点直接出栈,同时“5”节点进入结果集
7、“3”节点出栈,同时“3”节点的左结点进栈,“3”节点进入结果集
8、“6”节点无左右子节点,因此“6”节点直接出栈,同时“6”节点进入结果集。遍历结束
二、树的中序遍历
(一)、题目需求
给定一个二叉树,返回它的中序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [1,3,2]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?
(二)、递归解法
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
List<Integer> left = inorderTraversal(root.left);
List<Integer> right = inorderTraversal(root.right);
result.addAll(left);
result.add(root.val);
result.addAll(right);
return result;
}
(三)、非递归解法
public List<Integer> inorderTraversalByStack(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode current = root;
while (current != null || !stack.isEmpty()) {
while (current != null) {
stack.push(current);
current = current.left;
}
current = stack.pop();
result.add(current.val);
current = current.right;
}
return result;
}
(四)、代码分析
- 由于中序遍历的递归解法与前序遍历的相差不大,因此可参考前序遍历
1、首先将cuurent定位至root节点的最左边节点
while (current != null) {
stack.push(current);
current = current.left;
}
2、将current推出栈,并将其加入结果集中,将current定位至其右子节点处,若current的右子节点不为空,则意味着current节点的下方仍有节点未被遍历,则重复上面步骤1。若current的右子节点为空,则继续遍历栈中的剩余节点。
current = stack.pop();
result.add(current.val);
current = current.right;
(五)、流程图分析
- 由于中序遍历的流程图分析与前序遍历的相差不大,因此可参考前序遍历
1、初始状态
2、设立结果集、栈、current指针
3、current遍历至root节点的最左节点,同时逐渐入栈
4、“4”节点出栈,同时“4”节点加入结果集合,由于“4”节点的右子节点为空,所以current为空
5、“2”节点出栈,同时“2”节点加入结果集合,由于“2‘节点的右子节点非空,因此current指向其右子节点
6、current非空,因此”5“节点入栈
7、”5“节点出栈,同时“5”节点加入结果集合
8、“1”节点出栈,同时“1”节点加入结果集合,由于“1‘节点的右子节点非空,因此current指向其右子节点
9、current指向”3“节点的最左边,同时逐渐入栈
10、”6“节点出栈,同时”6“节点加入结果集中,current为空
11、”3“节点出栈,”3“节点进入结果集中。由于current和栈均为空,因此退出循环
三、树的后序遍历
(一)、题目需求
给定一个二叉树,返回它的 后序 遍历。
示例:
输入: [1,null,2,3]
1
\
2
/
3
输出: [3,2,1]
(二)、递归解法
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
List<Integer> left = postorderTraversal(root.left);
List<Integer> right = postorderTraversal(root.right);
result.addAll(left);
result.addAll(right);
result.add(root.val);
return result;
}
(三)、非递归解法——破坏原有数据结构
public List<Integer> postorderTraversalByStack(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode treeNode = stack.peek();
if (treeNode.right == null && treeNode.left == null) {
result.add(stack.pop().val);
}
if (treeNode.right != null) {
stack.push(treeNode.right);
treeNode.right = null;
}
if (treeNode.left != null) {
stack.push(treeNode.left);
treeNode.left = null;
}
}
return result;
}
(四)、非递归解法——无破坏原有数据结构
public List<Integer> postorderTraversalByStack2(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode current = root;
TreeNode pre = null;
stack.push(root);
while (!stack.isEmpty()) {
current = stack.peek();
if (pre == null || pre.left == current || pre.right == current) {
if (current.left != null) {
stack.push(current.left);
} else if (current.right != null) {
stack.push(current.right);
}
} else if (current.left == pre) {
if (current.right != null) {
stack.push(current.right);
}
} else {
result.add(current.val);
stack.pop();
}
pre = current;
}
return result;
}
(五)、代码分析
- 由于后序遍历的递归解法与前序遍历的相差不大,因此可参考前序遍历
1、非递归解法——破坏原有数据结构
1、创建栈,利用栈的先进后出的特性,遍历树。同时将根结点入栈。
LinkedList<TreeNode> stack = new LinkedList<>();
stack.push(root);
2、由于栈具备先进后出的特性。同时树的后序遍历顺序为:先根的左子节点,再根的右子节点,最后为根节点。因此先判断节点的右子节点是否为空,若不为空,则将其入栈,同时切断两者的联系。再判断节点的左子节点是否为空,若不为空,则将其入栈,同时切断两者的联系。
if (treeNode.right != null) {
stack.push(treeNode.right);
treeNode.right = null;
}
if (treeNode.left != null) {
stack.push(treeNode.left);
treeNode.left = null;
}
3、首先获取栈顶元素,但栈顶元素不出栈。
若该节点的左右子节点皆为空,则说明该节点为叶子节点或该节点左右子节点皆曾经入过栈。
若该节点为叶子节点,则说明其无左右子节点需遍历,因此直接将其出栈并且将其加入结果集。
若该节点的节点的左右节点皆曾经入过栈,由于栈的特性为先进后出,则说明如今其左右子节点已经出栈,即左子节点与右子节点已进入结果集,因此直接将其出栈并将其加入结果集。
TreeNode treeNode = stack.peek();
if (treeNode.right == null && treeNode.left == null) {
result.add(stack.pop().val);
}
2、非递归解法——无破坏原有数据结构
1、设置栈、current指针、pre指针
LinkedList<TreeNode> stack = new LinkedList<>();
TreeNode current = root;
TreeNode pre = null;
2、假如pre指针为空,则说明current此时为root节点,其左右子树仍未遍历。
假如pre指针的左子节点或右子节点为current,则说明此时以current为根节点的子树,仍未遍历。
由于后序遍历的顺序为:先左、后右、最后根节点;因此在确定current的左右半边的子树未遍历时,首先判断其左子节点是否为空,若不为空,则先遍历其左半边子树。若左子节点为空,说明其无左子树,进而判断其右子节点是否为空,若不为空,则遍历其右半边子树。
if (pre == null || pre.left == current || pre.right == current) {
if (current.left != null) {
stack.push(current.left);
} else if (current.right != null) {
stack.push(current.right);
}
}
3、当current节点的左子节点为pre节点时,说明current节点的左半边子树皆已遍历结束,此时需要遍历current的右半边子树。因此假如current的右子节点非空,则开始将其加入栈中,接下来便重复步骤2,开始逐渐遍历current节点的右半子树。
else if (current.left == pre) {
if (current.right != null) {
stack.push(current.right);
}
}
4、此时说明pre节点等于current节点或者是current的右子节点为pre节点。
若pre节点等于current节点:说明current节点无左右子节点,此时pre节点才会下移至current处,因此可直接将current节点此时对应的值出栈,并加入结果集中。
若pre节点等于current节点的右子节点:说明current的左右子节点皆已遍历结束;根据栈的先进后出的特性,此时current节点的左右子节点皆已出栈且进入结果集中;而后序遍历的顺序为先左、后右、最后为根节点,因此此时可直接将current节点此时对应的值出栈,并加入结果集中。
else {
result.add(current.val);
stack.pop();
}
(六)、流程图分析
1、非递归解法——破坏原有数据结构
1、初始状态
2、根节点入栈
3、“1”节点的右节点非空,将“3”节点入栈,同时切断两者的关系
4、“1”节点的左节点非空,将“2”节点入栈,同时切断两者的关系
5、“2”节点的右节点非空,将“5”节点入栈,同时切断两者的关系
6、“2”节点的左节点非空,将“4”节点入栈,同时切断两者的关系
7、“4”节点的左右节点皆为空,因此直接将“4”节点出栈,并将“4”节点加入结果集中
8、“5”节点的左右节点皆为空,因此直接将“5”节点出栈,并将“5”节点加入结果集中
9、“2”节点的左右节点皆为空,因此直接将“2”节点出栈,并将“2”节点加入结果集中
10、“3”节点的左节点非空,因此将“6”节点入栈,并切断两者的关系
11、“6”节点的左右节点皆为空,因此直接将“6”节点出栈,并将“6”节点加入结果集中
12、“3”节点的左右节点皆为空,因此直接将“3”节点出栈,并将“3”节点加入结果集中
13、“1”节点的左右节点皆为空,因此直接将“1”节点出栈,并将“1”节点加入结果集中,遍历结束。
2、非递归解法——无破坏原有数据结构
1、初始状态
2、设置current、pre、栈、结果集,并将root节点入栈
3、pre为空,“1”节点的左子节点非空,因此“2“节点入栈;同时pre移至current指针的位置、current移至其左子节点处即”1“节点的左子节点”2“节点处。
4、pre的左子节点为current,“2”节点的左子节点非空,因此“4“节点入栈;同时pre移至current指针的位置、current移至其左子节点处即”2“节点的左子节点”4“节点处。
5、pre的左子节点为current,“4”节点的左右子节点皆为空,因此无新的节点入栈;同时pre移至current指针的位置。此时pre指针与current指针指向同一节点。
6、由于pre指针与current指针指向同一节点,即”4“节点。因此”4“节点出栈,并加入结果集。同时current节点指向栈顶节点即”2“节点。此时pre指针指向节点为current指针指向的节点的左子节点——因此此时需判断current节点的右子节点是否为空。
7、由于”2“节点的右子节点非空,因此”5“节点入栈。同时pre指针移至current指针处,current指针移至新的栈顶元素节点处即”5“节点。
8、pre的右子节点为current,“5”节点的左右子节点皆为空,因此无新的节点入栈;同时pre移至current指针的位置。此时pre指针与current指针指向同一节点。
9、由于pre指针与current指针指向同一节点,即”6“节点。因此”6“节点出栈,并加入结果集。同时current节点指向栈顶节点即”2“节点。此时pre指针指向节点为current指针指向的节点的右子节点——因此此时可判断得出以”2“节点的根节点的左右子树皆遍历完成,因此可将”2“节点出栈、加入结果集。
10、”2“节点出栈、加入结果集。pre指针移至current指针处,current指针移至栈顶元素节点”1“节点。此时pre指针指向节点为current指针指向的节点的左子节点——因此此时需判断current节点的右子节点是否为空。
11、"1"节点的右子节点非空,因此”3“节点入栈。同时pre指针移至current指针处,current指针移至栈顶元素”3“节点处。
12、pre的右子节点为current,“3”节点的左子节点非空,因此”6“节点入栈;同时pre移至current指针的位置,current指针移至栈顶元素”6“节点处。
13、pre的左子节点为current,“6”节点的左右子节点皆为空,因此无新的节点入栈;同时pre移至current指针的位置。此时pre指针与current指针指向同一节点。
14、”6“节点出栈,并加入结果集。同时current指针移至栈顶元素”3“节点
15、由于”3“节点的右子节点为空,所以以”3“节点为根节点的左右子树皆遍历完成。”3“节点出栈,并加入结果集。
16、pre和current指针指向同一节点,”1“节点出栈,并加入结果集。遍历结束。